Claude-Agent

 view release on metacpan or  search on metacpan

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

sub receive {
    my ($self) = @_;

    Claude::Agent::Error->throw(message => 'Not connected') unless $self->_connected;
    Claude::Agent::Error->throw(message => 'Query not initialized') unless $self->_query;

    my $msg = $self->_query->next;

    # Capture session_id from any system message that has one
    if ($msg && $msg->isa('Claude::Agent::Message::System')) {
        my $sid = $msg->get_session_id;
        $self->_session_id($sid) if $sid;
    }

    # Result messages indicate end of current query turn
    # No additional handling needed - caller should check message type

    return $msg;
}

=head2 receive_async

    my $msg = await $client->receive_async;

Async call to receive the next message. Returns a Future.

=cut

sub receive_async {
    my ($self) = @_;

    Claude::Agent::Error->throw(message => 'Not connected') unless $self->_connected;
    Claude::Agent::Error->throw(message => 'Query not initialized') unless $self->_query;

    return $self->_query->next_async;
}

=head2 receive_until_result

    my @messages = $client->receive_until_result;

Receive all messages until a Result message is received.

=cut

sub receive_until_result {
    my ($self) = @_;

    my @messages;
    my $estimated_memory = 0;
    my $max_memory_bytes = $ENV{CLAUDE_AGENT_MAX_MEMORY_MB} ? $ENV{CLAUDE_AGENT_MAX_MEMORY_MB} * 1024 * 1024 : 500 * 1024 * 1024;  # Default 500MB
    # Default to 1000 messages - generous for typical use but prevents runaway loops.
    # For very long-running operations, set CLAUDE_AGENT_MAX_MESSAGES higher (max 5000).
    # Typical queries produce 10-100 messages; 1000 allows for complex multi-tool operations.
    #
    # MEMORY WARNING: Each message object may be 1-100KB depending on content.
    # At max (5000 messages), memory usage could reach 50MB-500MB.
    # For long-running operations, consider processing messages incrementally
    # using receive() in a loop rather than receive_until_result().
    # Set CLAUDE_AGENT_MAX_MEMORY_MB to limit memory usage (default 500MB).
    my $max_iterations = 1000;
    my $max_allowed = 5_000;  # Reduced to prevent memory exhaustion (each message ~1-100KB)
    my $max_msg_env = $ENV{CLAUDE_AGENT_MAX_MESSAGES};
    $max_msg_env =~ s/^\s+|\s+$//g if defined $max_msg_env;  # trim whitespace
    # Validate after trimming - must be positive integer > 0 (rejects 0 and leading zeros)
    if (defined $max_msg_env && $max_msg_env =~ /^[1-9]\d*$/) {
        $max_iterations = $max_msg_env;
        if ($max_iterations > $max_allowed) {
            $log->warning(sprintf("CLAUDE_AGENT_MAX_MESSAGES=%d exceeds maximum (%d), using %d. "
                . "WARNING: High message counts risk memory exhaustion (estimated %dMB-%dMB at max). "
                . "Set CLAUDE_AGENT_MAX_MEMORY_MB to limit memory, or use receive() for incremental processing.",
                $max_iterations, $max_allowed, $max_allowed, $max_allowed / 10, $max_allowed / 1));
            $max_iterations = $max_allowed;
        }
        elsif ($max_iterations > 2500) {
            $log->warning(sprintf("CLAUDE_AGENT_MAX_MESSAGES=%d may cause high memory usage (estimated %dMB-%dMB). "
                . "Consider using receive() with incremental processing or set CLAUDE_AGENT_MAX_MEMORY_MB.",
                $max_iterations, $max_iterations / 10, $max_iterations / 1));
        }
    }
    my $iterations = 0;
    # Create JSON::Lines instance once outside the loop for better performance
    require JSON::Lines;
    my $jsonl = JSON::Lines->new;
    while (my $msg = $self->receive) {
        $iterations++;
        push @messages, $msg;
        # Estimate memory usage (rough heuristic based on message content)
        # Estimate size based on raw data structure - use JSON::Lines for encoding
        my $json_str = eval { $jsonl->encode([$msg->message // {}]) } // '{}';
        $estimated_memory += length($json_str) + 500;  # Add overhead estimate
        last if $msg->isa('Claude::Agent::Message::Result');
        if ($estimated_memory >= $max_memory_bytes) {
            $log->warning(sprintf("receive_until_result: estimated memory usage (%d bytes) exceeds limit (%d bytes), breaking loop. "
                . "Set CLAUDE_AGENT_MAX_MEMORY_MB to increase limit or use incremental processing.",
                $estimated_memory, $max_memory_bytes));
            last;
        }
        if ($iterations >= $max_iterations) {
            $log->warning(sprintf("receive_until_result: processed max messages (%d), breaking loop. "
                . "Set CLAUDE_AGENT_MAX_MESSAGES to increase limit.", $max_iterations));
            last;
        }
    }
    # Check if we exited without a Result (connection dropped)
    if (@messages && !$messages[-1]->isa('Claude::Agent::Message::Result')) {
        $log->debug("receive_until_result: connection closed without Result message");
    }
    return wantarray ? @messages : \@messages;
}

=head2 send

    $client->send($message);

Send a follow-up message in the current session.

=cut

## no critic (ProhibitBuiltinHomonyms)
sub send {
    my ($self, $content) = @_;

    Claude::Agent::Error->throw(message => 'Not connected') unless $self->_connected;

    # Get session_id from previous query or stored value
    my $session_id = $self->_session_id // ($self->_query ? $self->_query->session_id : undef);
    Claude::Agent::Error->throw(message => 'No session_id available for send') unless $session_id;

    # Cleanup previous query if it exists
    if ($self->_query) {
        $self->_query->cleanup();
    }

    $log->debug(sprintf("Client: Sending follow-up, resuming session id=%s", $session_id));

    # Create new options with resume
    my $opts = $self->options;
    my $resume_opts = Claude::Agent::Options->new(
        ($opts->has_allowed_tools ? (allowed_tools => $opts->allowed_tools) : ()),
        ($opts->has_disallowed_tools ? (disallowed_tools => $opts->disallowed_tools) : ()),
        ($opts->has_model ? (model => $opts->model) : ()),
        ($opts->has_permission_mode ? (permission_mode => $opts->permission_mode) : ()),
        ($opts->has_mcp_servers ? (mcp_servers => $opts->mcp_servers) : ()),
        ($opts->has_hooks ? (hooks => $opts->hooks) : ()),
        ($opts->has_agents ? (agents => $opts->agents) : ()),
        ($opts->has_max_turns ? (max_turns => $opts->max_turns) : ()),
        ($opts->has_system_prompt ? (system_prompt => $opts->system_prompt) : ()),
        resume => $session_id,
    );

    # Create new query with resume
    $self->_query(
        Claude::Agent::Query->new(
            prompt  => $content,
            options => $resume_opts,
            ($self->has_loop ? (loop => $self->loop) : ()),
        )
    );

    return $self;



( run in 0.901 second using v1.01-cache-2.11-cpan-98e64b0badf )