Claude-Agent

 view release on metacpan or  search on metacpan

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

    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;
}

lib/Claude/Agent/MCP/SDKRunner.pm  view on Meta::CPAN

        $timeout = 60;
    } elsif ($timeout > $max_timeout) {
        $log->warning(sprintf("CLAUDE_AGENT_TOOL_TIMEOUT=%d exceeds maximum (%d seconds), capping to %d seconds",
            $timeout, $max_timeout, $max_timeout));
        $timeout = $max_timeout;
    }
    my $start_time = Time::HiRes::time();
    my $backoff = 0.1;
    my $last_buffer_size = length($state->{response_buffer});
    my $stall_count = 0;
    my $max_stall_iterations = 100;  # ~10 seconds at max backoff before declaring stall

    while (!$state->{got_response}) {
        # Check elapsed time before loop_once to ensure accurate timeout enforcement
        my $elapsed = Time::HiRes::time() - $start_time;
        last if $elapsed >= $timeout;

        $state->{loop}->loop_once($backoff);
        $backoff = $backoff * 1.5 if $backoff < 1.0;  # Exponential backoff up to 1 second

        # Detect buffer growth without complete JSON lines (malformed/incomplete data)

lib/Claude/Agent/MCP/SDKRunner.pm  view on Meta::CPAN

            $state->{response_buffer} = '';
            last;
        }
        elsif ($current_buffer_size > $warn_buffer_size) {
            # Log warning at 5MB to alert before hitting hard limit
            $log->debug(sprintf("SDKRunner: Buffer approaching limit (size: %d bytes, limit: %d)",
                $current_buffer_size, $max_buffer_size));
        }
        if ($current_buffer_size > 0 && $current_buffer_size == $last_buffer_size) {
            $stall_count++;
            if ($stall_count >= $max_stall_iterations) {
                $log->debug(sprintf("SDKRunner: Buffer stalled with incomplete data (size: %d)",
                    $current_buffer_size));
                last;
            }
        } elsif ($current_buffer_size != $last_buffer_size) {
            # Buffer changed - reset stall counter but don't reset backoff
            $stall_count = 0;
            $last_buffer_size = $current_buffer_size;
        }



( run in 2.097 seconds using v1.01-cache-2.11-cpan-98e64b0badf )