Claude-Agent

 view release on metacpan or  search on metacpan

lib/Claude/Agent/Query.pm  view on Meta::CPAN

            $buffer_threshold, $min_threshold, $min_threshold));
        $buffer_threshold = $min_threshold;
    }
    elsif ($buffer_threshold > $max_threshold) {
        $log->debug(sprintf("CLAUDE_AGENT_JSONL_BUFFER_MAX=%d exceeds maximum (%d), using %d",
            $buffer_threshold, $max_threshold, $max_threshold));
        $buffer_threshold = $max_threshold;
    }

    # Check buffer size regardless of decode success to prevent unbounded growth
    if ($self->_jsonl->remaining && length($self->_jsonl->remaining) > $buffer_threshold) {
        $log->debug(sprintf("JSON::Lines buffer overflow detected (size: %d, threshold: %d), reinitializing",
            length($self->_jsonl->remaining), $buffer_threshold));
        $self->_jsonl(JSON::Lines->new(
            utf8     => 1,
            error_cb => sub {
                my ($action, $error, $data) = @_;
                $log->trace(sprintf("JSON::Lines %s error: %s", $action, $error));
                return;
            },
        ));
    }

    return unless @decoded;

    for my $data (@decoded) {
        if ($log->is_trace) {
            $log->trace(sprintf("Decoded item ref type: %s", ref($data) // "not a ref"));
            if (ref $data eq 'HASH') {
                $log->trace(sprintf("Hash keys: %s", join(", ", keys %$data)));
            }
        }
        next unless defined $data && ref $data eq 'HASH';
        $log->trace(sprintf("Message type in data: %s", $data->{type} // "undef"))
            if $log->is_trace;
        next unless exists $data->{type};  # Skip malformed/partial JSON data

        $log->debug(sprintf("Query: Received message type=%s", $data->{type}));

        my $msg = Claude::Agent::Message->from_json($data);

        # Log message subtype for more detail
        if ($msg->can('subtype') && $msg->subtype) {
            $log->debug(sprintf("Query: Message subtype=%s", $msg->subtype));
        }

        # Capture session_id from init message
        if ($msg->isa('Claude::Agent::Message::System')
            && $msg->subtype eq 'init') {
            $self->_session_id($msg->get_session_id);
            $log->debug(sprintf("Query: Session ID captured: %s", $self->_session_id // 'none'));
            # Update hook executor with session_id
            if ($self->_hook_executor) {
                $self->_hook_executor->session_id($self->_session_id);
            }
        }

        # Execute Perl hooks for tool use messages
        $msg = $self->_execute_hooks_for_message($msg);

        # Skip if message was blocked by hooks
        if (!defined $msg) {
            $log->debug(sprintf("Query: Message type=%s blocked by hooks", $data->{type}));
            next;
        }

        # Re-entrancy guard: if already processing, queue for later
        if ($self->_processing_message) {
            push @{$self->_messages}, $msg;
            next;
        }
        $self->_processing_message(1);

        if (@{$self->_pending_futures}) {
            my $future = shift @{$self->_pending_futures};
            $log->debug(sprintf("Query: Delivering message type=%s to waiting future", $data->{type}));
            $future->done($msg);
        }
        else {
            $log->debug(sprintf("Query: Queuing message type=%s (queue size=%d)", $data->{type}, scalar(@{$self->_messages}) + 1));
            push @{$self->_messages}, $msg;
        }

        $self->_processing_message(0);
    }
    return;
}

# Execute hooks for messages containing tool use/result blocks
# PreToolUse hooks are awaited (blocking), PostToolUse hooks are fire-and-forget
sub _execute_hooks_for_message {
    my ($self, $msg) = @_;

    return $msg unless $self->_hook_executor;

    # Handle assistant messages with tool_use blocks (PreToolUse)
    if ($msg->isa('Claude::Agent::Message::Assistant')) {
        my $tool_uses = $msg->tool_uses;
        if ($tool_uses && @$tool_uses) {
            $log->debug(sprintf("Query: Processing %d tool_use block(s)", scalar(@$tool_uses)));
            for my $tool_use (@$tool_uses) {
                my $tool_name = $tool_use->can('name') ? $tool_use->name : $tool_use->{name};
                my $tool_input = $tool_use->can('input') ? $tool_use->input : $tool_use->{input};
                my $tool_use_id = $tool_use->can('id') ? $tool_use->id : $tool_use->{id};

                next unless $tool_name && $tool_use_id;

                $log->trace(sprintf("Query: Tool use: name=%s id=%s", $tool_name, $tool_use_id));

                # Track this tool use for PostToolUse hooks
                $self->_pending_tool_uses->{$tool_use_id} = {
                    tool_name  => $tool_name,
                    tool_input => $tool_input,
                };

                # Run PreToolUse hooks (must await result for decision)
                if ($self->_hook_executor->has_hooks_for('PreToolUse')) {
                    my $result_future = $self->_hook_executor->run_pre_tool_use(
                        $tool_name, $tool_input, $tool_use_id
                    );

                    # Await the Future - hooks may be async
                    my $result = $self->_await_hook_result($result_future);

                    if ($result->{decision} eq 'deny') {
                        $log->info(sprintf("[HOOK] Blocked tool: %s - %s",
                            $tool_name, $result->{reason} // 'denied by hook'));

                        # Send permission denial to CLI
                        $self->respond_to_permission($tool_use_id, {
                            behavior => 'deny',
                            reason   => $result->{reason} // 'Denied by Perl hook',
                        });

                        # Remove from pending since we denied it
                        delete $self->_pending_tool_uses->{$tool_use_id};
                    }
                    elsif ($result->{decision} eq 'allow' && $result->{updated_input}) {
                        $log->debug(sprintf("[HOOK] Modified tool input for: %s", $tool_name));

                        # Send permission with modified input
                        $self->respond_to_permission($tool_use_id, {
                            behavior      => 'allow',
                            updated_input => $result->{updated_input},
                        });

                        # Update pending with modified input
                        $self->_pending_tool_uses->{$tool_use_id}{tool_input} =
                            $result->{updated_input};
                    }
                    # 'continue' or 'allow' without modifications - let it proceed normally
                }
            }
        }
    }

    # Handle system messages with tool results (PostToolUse / PostToolUseFailure)
    if ($msg->isa('Claude::Agent::Message::System')) {
        my $subtype = $msg->subtype // '';

        # Check for tool result in system message
        if ($subtype eq 'tool_result' || $subtype eq 'tool_output') {
            my $tool_use_id = $msg->can('tool_use_id') ? $msg->tool_use_id : undef;

            if ($tool_use_id && exists $self->_pending_tool_uses->{$tool_use_id}) {
                my $pending = delete $self->_pending_tool_uses->{$tool_use_id};
                my $tool_name = $pending->{tool_name};
                my $tool_input = $pending->{tool_input};

                # Determine if success or failure
                my $is_error = $msg->can('is_error') ? $msg->is_error : 0;
                $log->debug(sprintf("Query: Tool result received: name=%s id=%s %s",
                    $tool_name, $tool_use_id, $is_error ? '(error)' : '(success)'));
                my $tool_result = $msg->can('content') ? $msg->content : undef;

                if ($is_error) {
                    if ($self->_hook_executor->has_hooks_for('PostToolUseFailure')) {
                        # Fire and forget - don't block on PostToolUseFailure
                        my $future = $self->_hook_executor->run_post_tool_use_failure(
                            $tool_name, $tool_input, $tool_use_id, $tool_result
                        );
                        $self->_retain_hook_future($future);
                    }
                }
                else {
                    if ($self->_hook_executor->has_hooks_for('PostToolUse')) {



( run in 1.617 second using v1.01-cache-2.11-cpan-39bf76dae61 )