diff --git a/packages/agent/julia_implementation/IMPLEMENTATION.md b/packages/agent/julia_implementation/IMPLEMENTATION.md deleted file mode 100644 index ccb21b58..00000000 --- a/packages/agent/julia_implementation/IMPLEMENTATION.md +++ /dev/null @@ -1,119 +0,0 @@ -# Julia Implementation - AgentCore - -This directory contains a Julia reimplementation of the `@earendil-works/pi-agent-core` package. - -## Project Structure - -``` -julia_implementation/ -├── src/ -│ ├── AgentCore.jl # Main module entry point -│ ├── types.jl # Core type definitions -│ ├── stream_fn.jl # Stream function utilities -│ ├── agent_loop.jl # Low-level agent loop -│ ├── agent.jl # High-level Agent struct -│ ├── harness_types.jl # Extended types for AgentHarness -│ ├── messages.jl # Custom message types -│ ├── system_prompt.jl # System prompt formatting -│ ├── skills.jl # Skill loading and formatting -│ ├── prompt_templates.jl # Prompt template handling -│ ├── agent_harness.jl # AgentHarness implementation -│ │ -│ ├── session/ -│ │ ├── session.jl # Session class -│ │ ├── jsonl_storage.jl # JSONL storage -│ │ ├── jsonl_repo.jl # JSONL repository -│ │ ├── memory_storage.jl # In-memory storage -│ │ ├── memory_repo.jl # In-memory repository -│ │ └── repo_utils.jl # Repository utilities -│ │ -│ ├── tools/ -│ │ ├── index.jl # Tool exports -│ │ ├── bash.jl # Bash execution tool -│ │ ├── read.jl # File read tool -│ │ ├── write.jl # File write tool -│ │ ├── edit.jl # File edit tool -│ │ ├── edit_diff.jl # Diff computation -│ │ ├── image.jl # Image utilities -│ │ ├── path_utils.jl # Path resolution -│ │ └── file_mutation_queue.jl # File mutation serialization -│ │ -│ ├── compaction/ -│ │ ├── compaction.jl # Context compaction -│ │ ├── utils.jl # Compaction utilities -│ │ └── branch_summarization.jl # Branch summarization -│ │ -│ ├── utils/ -│ │ ├── truncate.jl # Output truncation -│ │ └── shell_output.jl # Shell output capture -│ │ -│ ├── proxy.jl # Proxy stream function -│ └── utils.jl # Utility functions -│ -├── test/ -├── Project.toml -├── Manifest.toml -└── README.md -``` - -## Key Features - -### Core Architecture - -The implementation follows the same layered architecture as the TypeScript version: - -1. **Low-level (agent_loop.jl)**: Pure agent loop logic that works with `AgentMessage[]` -2. **High-level (agent.jl)**: Stateful wrapper with event streaming and queueing -3. **Harness (agent_harness.jl)**: Session persistence, resource management, hooks -4. **Session (session/)**: Conversation history with compaction and branching -5. **Tools (tools/)**: Built-in execution tools (bash, read, write, edit) - -### Julia-Specific Features - -- **Type system**: Uses Julia's parametric types for type-safe tool definitions -- **Multiple dispatch**: Extensible via multiple dispatch for custom message types -- **Async primitives**: Leverages Julia's `@async` and `@spawn` for concurrent operations -- **Error handling**: Julia exceptions with typed error codes - -## Building - -```julia -using Pkg -Pkg.activate("julia_implementation") -Pkg.instantiate() -``` - -## Usage Example - -```julia -using AgentCore - -# Create an agent -agent = Agent() - -# Subscribe to events -subscribe(agent) do event, signal - if event isa MessageEndEvent - println("Message: $(event.message)") - end -end - -# Run a prompt -prompt(agent, "Hello, world!") -``` - -## Compatibility - -This implementation aims for API compatibility with the TypeScript version while providing idiomatic Julia abstractions. - -## Status - -This is an active implementation. Core functionality is in place, with ongoing work on: - -- Complete tool implementations -- Full session repository functionality -- Test suite - -## License - -MIT diff --git a/packages/agent/julia_implementation/Manifest.toml b/packages/agent/julia_implementation/Manifest.toml deleted file mode 100644 index d67c0900..00000000 --- a/packages/agent/julia_implementation/Manifest.toml +++ /dev/null @@ -1,25 +0,0 @@ -name = "AgentCore" -uuid = "6e2f7b3a-9a0b-4e8e-8f8f-8f8f8f8f8f8f" -authors = ["Mario Zechner "] -version = "0.1.0" - -[deps] -Dates = "ade2ca70-3891-5945-98fb-dc09409a37d3" -JSON3 = "0f8b85d8-8d2f-5481-9e3b-d9a10a9b6c53" -Libdl = "8f399da3-355a-58d1-55dd-a8cd37d21846" -Markdown = "d6f4372e-7a37-5ca6-90db-23e40208355e" -Mmap = "a63ad114-7ff6-5b6b-903e-90ddba579e5d" -Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -Sockets = "6462fe0b-2de3-572b-8e7f-4c2f5e2c2e2b" -Unicode = "4ec0a83e-493e-50e2-b9ac-8f72acf2a872" -UUIDs = "cf7118a7-4649-5bc2-89ac-36d7b14660ca" - -[extras] -Test = "8dfed614-e22c-5e4d-98d3-97fe1b80e45d" - -[targets] -test = ["Test"] - -[compat] -julia = "1.9" diff --git a/packages/agent/julia_implementation/Project.toml b/packages/agent/julia_implementation/Project.toml deleted file mode 100644 index 42d1f303..00000000 --- a/packages/agent/julia_implementation/Project.toml +++ /dev/null @@ -1,22 +0,0 @@ -name = "AgentCore" -uuid = "6e2f7b3a-9a0b-4e8e-8f8f-8f8f8f8f8f8f" -authors = ["Mario Zechner "] -version = "0.1.0" - -[deps] -Dates = "ade2ca70-3891-5945-98fb-dc09409a37d3" -JSON3 = "0f8b85d8-8d2f-5481-9e3b-d9a10a9b6c53" -Libdl = "8f399da3-355a-58d1-55dd-a8cd37d21846" -Markdown = "d6f4372e-7a37-5ca6-90db-23e40208355e" -Mmap = "a63ad114-7ff6-5b6b-903e-90ddba579e5d" -Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -Sockets = "6462fe0b-2de3-572b-8e7f-4c2f5e2c2e2b" -Unicode = "4ec0a83e-493e-50e2-b9ac-8f72acf2a872" -UUIDs = "cf7118a7-4649-5bc2-89ac-36d7b14660ca" - -[extras] -Test = "8dfed614-e22c-5e4d-98d3-97fe1b80e45d" - -[targets] -test = ["Test"] diff --git a/packages/agent/julia_implementation/README.md b/packages/agent/julia_implementation/README.md deleted file mode 100644 index 2908eae8..00000000 --- a/packages/agent/julia_implementation/README.md +++ /dev/null @@ -1,179 +0,0 @@ -# AgentCore.jl - Julia Implementation of Pi Agent Core - -A Julia reimplementation of the `@earendil-works/pi-agent-core` package, providing a stateful agent framework for LLM interactions. - -## Overview - -This package provides: -- Low-level `agentLoop` for stateful LLM interactions with tool execution -- High-level `Agent` struct with state management, event streaming, and queueing -- `AgentHarness` for session persistence, resource management, and extension hooks -- Built-in tools for file operations (read, write, edit) and bash execution -- Session management with JSONL-based storage, compaction, and branch navigation - -## Architecture - -The Julia implementation follows the same layered architecture as the TypeScript version: - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ AgentHarness │ -│ (Session persistence, resource management) │ -└─────────────────────────────────────────────────────────────────────┘ - │ -┌─────────────────────────────▼───────────────────────────────────────┐ -│ Agent │ -│ (State management, event streaming, queueing) │ -└─────────────────────────────────────────────────────────────────────┘ - │ -┌─────────────────────────────▼───────────────────────────────────────┐ -│ AgentLoop │ -│ (Low-level loop, tool execution) │ -└─────────────────────────────────────────────────────────────────────┘ - │ -┌─────────────────────────────▼───────────────────────────────────────┐ -│ Session │ -│ (Conversation history, compaction, branching) │ -└─────────────────────────────────────────────────────────────────────┘ -``` - -## Installation - -```julia -using Pkg -Pkg.add("AgentCore") -``` - -## Quick Start - -```julia -using AgentCore - -# Create an agent with default configuration -agent = Agent() - -# Subscribe to events -subscribe(agent) do event, signal - if event isa MessageEndEvent - println("Received message: $(event.message)") - end -end - -# Run a prompt -prompt(agent, "Hello, how are you?") -``` - -## Core Concepts - -### Agent - -The `Agent` struct provides a high-level interface for interacting with LLMs. It manages: -- Conversation state (messages, tools, system prompt) -- Event streaming and lifecycle management -- Steering and follow-up message queues -- Abort handling - -### AgentLoop - -The `agentLoop` function implements the core agent loop that: -- Transforms `AgentMessage[]` to `Message[]` at the LLM call boundary -- Executes tool calls (parallel or sequential) -- Emits lifecycle events -- Handles steering and follow-up messages - -### AgentHarness - -The `AgentHarness` provides: -- Session persistence with JSONL storage -- Resource management (skills, prompt templates) -- Extension hooks system -- Tool execution with context -- Branch navigation and compaction - -### Sessions - -Sessions track conversation history using a tree-based structure: -- Branch-based history with compaction -- Tree navigation (moveTo, navigateTree) -- Message and metadata persistence - -## Built-in Tools - -### Bash Tool - -Execute shell commands with output capture and truncation. - -```julia -bash_tool = createBashTool() -``` - -### Read Tool - -Read files with support for text and images. - -```julia -read_tool = createReadTool() -``` - -### Write Tool - -Write content to files with automatic directory creation. - -```julia -write_tool = createWriteTool() -``` - -### Edit Tool - -Edit files using exact text replacement. - -```julia -edit_tool = createEditTool() -``` - -## Session Storage - -AgentCore supports two session storage backends: - -1. **JsonlSessionStorage** - File-based storage using JSONL format -2. **InMemorySessionStorage** - In-memory storage for testing - -## Compaction - -The compaction system manages context window usage by: -- Summarizing old conversation history -- Retaining recent messages -- Supporting iterative updates to summaries - -## Event System - -AgentCore uses a rich event system for monitoring and control: - -- `AgentStartEvent` / `AgentEndEvent` - Agent lifecycle -- `TurnStartEvent` / `TurnEndEvent` - Conversation turns -- `MessageStartEvent` / `MessageEndEvent` - Message lifecycle -- `ToolExecutionStartEvent` / `ToolExecutionEndEvent` - Tool execution - -## Examples - -See the `examples/` directory for more detailed examples. - -## Differences from TypeScript - -While maintaining API compatibility where possible, this Julia implementation: -- Uses Julia's type system for better compile-time guarantees -- Leverages Julia's multiple dispatch for extensibility -- Uses Julia's async primitives for concurrent operations -- Provides more idiomatic Julia error handling - -## Contributing - -Contributions are welcome! Please see `CONTRIBUTING.md` for details. - -## License - -MIT - -## Acknowledgments - -This is a reimplementation of the [Pi Agent Core](https://github.com/earendil-works/pi/packages/agent) package in Julia. diff --git a/packages/agent/julia_implementation/src/AgentCore.jl b/packages/agent/julia_implementation/src/AgentCore.jl deleted file mode 100644 index 420c85fd..00000000 --- a/packages/agent/julia_implementation/src/AgentCore.jl +++ /dev/null @@ -1,149 +0,0 @@ -# AgentCore.jl - A Julia implementation of the Pi Agent Core framework -# -# This is a reimplementation of the TypeScript pi-agent-core package in idiomatic Julia. -# -# The AgentCore package provides: -# - Low-level `agentLoop` for stateful LLM interactions with tool execution -# - High-level `Agent` struct with state management, event streaming, and queueing -# - `AgentHarness` for session persistence, resource management, and extension hooks -# - Built-in tools for file operations (read, write, edit) and bash execution -# - Session management with JSONL-based storage, compaction, and branch navigation -# -# For more information about the original TypeScript implementation, see: -# https://github.com/earendil-works/pi/packages/agent - -module AgentCore - -# Core modules -include("types.jl") -include("stream_fn.jl") -include("agent_loop.jl") -include("agent.jl") - -# Harness modules -include("harness_types.jl") -include("messages.jl") -include("system_prompt.jl") -include("skills.jl") -include("prompt_templates.jl") -include("agent_harness.jl") - -# Session modules -include("session/session.jl") -include("session/jsonl_storage.jl") -include("session/jsonl_repo.jl") -include("session/memory_storage.jl") -include("session/memory_repo.jl") -include("session/repo_utils.jl") - -# Tool modules -include("tools/index.jl") -include("tools/bash.jl") -include("tools/read.jl") -include("tools/write.jl") -include("tools/edit.jl") -include("tools/edit_diff.jl") -include("tools/image.jl") -include("tools/path_utils.jl") -include("tools/file_mutation_queue.jl") - -# Compaction modules -include("compaction/compaction.jl") -include("compaction/utils.jl") -include("compaction/branch_summarization.jl") - -# Utility modules -include("utils/truncate.jl") -include("utils/shell_output.jl") -include("proxy.jl") - -# Re-export public API -export - # Core types - AgentMessage, - AgentTool, - AgentContext, - AgentEvent, - ThinkingLevel, - ToolExecutionMode, - QueueMode, - AgentState, - - # Agent - Agent, - AgentOptions, - - # AgentLoop - AgentLoopConfig, - agentLoop, - agentLoopContinue, - runAgentLoop, - runAgentLoopContinue, - - # AgentHarness - AgentHarness, - AgentHarnessOptions, - AgentHarnessEvent, - AgentHarnessResources, - AgentHarnessSystemPrompt, - - # Session - Session, - SessionStorage, - SessionRepo, - JsonlSessionStorage, - JsonlSessionRepo, - InMemorySessionStorage, - InMemorySessionRepo, - - # Tools - createBashTool, - createReadTool, - createWriteTool, - createEditTool, - ExecutionEnv, - - # Compaction - compact, - prepareCompaction, - DEFAULT_COMPACTION_SETTINGS, - generateSummary, - generateBranchSummary, - - # Utils - truncateHead, - truncateTail, - formatSize, - DEFAULT_MAX_LINES, - DEFAULT_MAX_BYTES, - - # Messages - convertToLlm, - bashExecutionToText, - - # System prompt - formatSkillsForSystemPrompt, - - # Skills - loadSkills, - formatSkillInvocation, - - # Prompt templates - loadPromptTemplates, - formatPromptTemplateInvocation, - parseCommandArgs, - substituteArgs, - - # Proxy - streamProxy, - ProxyStreamOptions, - - # Stream - setDefaultStreamFn, - getDefaultStreamFn, - - # Utility functions - uuidv7, - create_timestamp - -end diff --git a/packages/agent/julia_implementation/src/agent.jl b/packages/agent/julia_implementation/src/agent.jl deleted file mode 100644 index 009114ae..00000000 --- a/packages/agent/julia_implementation/src/agent.jl +++ /dev/null @@ -1,416 +0,0 @@ -""" - agent.jl - High-level Agent struct - -This module implements the high-level Agent wrapper around the low-level agent loop, -providing state management, event streaming, and queueing for steering and follow-up messages. -""" - -module Agent - -using ..Types: * -using ..AgentLoop: * -using ..StreamFn: * - -# ============================================================================ -# Default convertToLlm function -# ============================================================================ - -function defaultConvertToLlm(messages::Vector{AgentMessage})::Vector{Message} - return filter( - (m) -> m.role == "user" || m.role == "assistant" || m.role == "toolResult", - messages, - ) -end - -# ============================================================================ -# Empty usage constant -# ============================================================================ - -const EMPTY_USAGE = Usage( - 0, 0, 0, 0, 0, UsageCost(0.0, 0.0, 0.0, 0.0, 0.0) -) - -# ============================================================================ -# Pending message queue -# ============================================================================ - -mutable struct PendingMessageQueue - messages::Vector{AgentMessage} - mode::QueueMode - - function PendingMessageQueue(mode::QueueMode) - new(AgentMessage[], mode) - end -end - -function enqueue!(queue::PendingMessageQueue, message::AgentMessage) - push!(queue.messages, message) -end - -function hasItems(queue::PendingMessageQueue)::Bool - return !isempty(queue.messages) -end - -function drain(queue::PendingMessageQueue)::Vector{AgentMessage} - if queue.mode == QUEUE_ALL - result = copy(queue.messages) - empty!(queue.messages) - return result - else - if isempty(queue.messages) - return AgentMessage[] - end - first = popfirst!(queue.messages) - return [first] - end -end - -function clear!(queue::PendingMessageQueue) - empty!(queue.messages) -end - -# ============================================================================ -# Active run state -# ============================================================================ - -mutable struct ActiveRun - promise::Promise - abort_controller::Base.Atomic{Union{Base.AbstractLock, Nothing}} -end - -# ============================================================================ -# Agent struct -# ============================================================================ - -mutable struct Agent - _state::AgentState - listeners::Set{Tuple{Function, Ref{Bool}}} - steering_queue::PendingMessageQueue - follow_up_queue::PendingMessageQueue - - convert_to_llm::Function - transform_context::Union{Function, Nothing} - stream_function::StreamFn - get_api_key::Union{Function, Nothing} - on_payload::Union{Function, Nothing} - on_response::Union{Function, Nothing} - before_tool_call::Union{Function, Nothing} - after_tool_call::Union{Function, Nothing} - prepare_next_turn::Union{Function, Nothing} - prepare_next_turn_with_context::Union{Function, Nothing} - active_run::Union{ActiveRun, Nothing} - session_id::Union{String, Nothing} - thinking_budgets::Union{Dict{String, Int64}, Nothing} - transport::String - max_retry_delay_ms::Union{Int64, Nothing} - tool_execution::ToolExecutionMode - - function Agent(options::Dict{Symbol, Any}=Dict{Symbol, Any}()) - runtime_options = merge( - Dict{Symbol, Any}( - :stream_fn => getDefaultStreamFn(), - :convertToLlm => defaultConvertToLlm, - :steeringMode => QUEUE_ONE_AT_A_TIME, - :followUpMode => QUEUE_ONE_AT_A_TIME, - :toolExecution => EXECUTION_PARALLEL, - :transport => "auto", - ), - options, - ) - - state = AgentState( - get(runtime_options, :systemPrompt, ""), - get(runtime_options, :model, Model("", "", "unknown", "unknown", "", false, String[], ModelCost(0.0, 0.0, 0.0, 0.0), 0, 0)), - get(runtime_options, :thinkingLevel, THINKING_OFF), - get(runtime_options, :tools, AgentTool[]), - get(runtime_options, :messages, AgentMessage[]), - ) - - new( - state, - Set{Tuple{Function, Ref{Bool}}}(), - PendingMessageQueue(QUEUE_ONE_AT_A_TIME), - PendingMessageQueue(QUEUE_ONE_AT_A_TIME), - get(runtime_options, :convertToLlm, defaultConvertToLlm), - get(runtime_options, :transformContext, nothing), - get(runtime_options, :stream_fn, getDefaultStreamFn()), - get(runtime_options, :getApiKey, nothing), - get(runtime_options, :onPayload, nothing), - get(runtime_options, :onResponse, nothing), - get(runtime_options, :beforeToolCall, nothing), - get(runtime_options, :afterToolCall, nothing), - get(runtime_options, :prepareNextTurn, nothing), - get(runtime_options, :prepareNextTurnWithContext, nothing), - nothing, - get(runtime_options, :sessionId, nothing), - get(runtime_options, :thinkingBudgets, nothing), - get(runtime_options, :transport, "auto"), - get(runtime_options, :maxRetryDelayMs, nothing), - get(runtime_options, :toolExecution, EXECUTION_PARALLEL), - ) - end -end - -# ============================================================================ -# Agent methods -# ============================================================================ - -""" - subscribe(agent, listener) - -Subscribe to agent lifecycle events. - -# Arguments -- `agent`: The agent instance -- `listener`: A function that takes (event::AgentEvent, signal::AbortSignal) - -# Returns -- A function that unsubscribes the listener -""" -function subscribe(agent::Agent, listener::Function)::Function - push!(agent.listeners, (listener, Ref{Bool}(true))) - return () -> begin - filter!(x -> x[1] != listener, agent.listeners) - end -end - -""" - get_state(agent) - -Get the current agent state. -""" -function get_state(agent::Agent)::AgentState - return agent._state -end - -""" - steer(agent, message) - -Queue a message to be injected after the current assistant turn finishes. -""" -function steer(agent::Agent, message::AgentMessage) - enqueue!(agent.steering_queue, message) -end - -""" - followUp(agent, message) - -Queue a message to run only after the agent would otherwise stop. -""" -function followUp(agent::Agent, message::AgentMessage) - enqueue!(agent.follow_up_queue, message) -end - -""" - clearSteeringQueue(agent) - -Remove all queued steering messages. -""" -function clearSteeringQueue(agent::Agent) - clear!(agent.steering_queue) -end - -""" - clearFollowUpQueue(agent) - -Remove all queued follow-up messages. -""" -function clearFollowUpQueue(agent::Agent) - clear!(agent.follow_up_queue) -end - -""" - clearAllQueues(agent) - -Remove all queued steering and follow-up messages. -""" -function clearAllQueues(agent::Agent) - clearSteeringQueue(agent) - clearFollowUpQueue(agent) -end - -""" - hasQueuedMessages(agent) - -Returns true when either queue still contains pending messages. -""" -function hasQueuedMessages(agent::Agent)::Bool - return hasItems(agent.steering_queue) || hasItems(agent.follow_up_queue) -end - -""" - abort(agent) - -Abort the current run, if one is active. -""" -function abort(agent::Agent) - if !isnothing(agent.active_run) - # TODO: Implement abort signal - end -end - -""" - waitForIdle(agent) - -Resolve when the current run and all awaited event listeners have finished. -""" -function waitForIdle(agent::Agent)::Promise - if isnothing(agent.active_run) - return Promise() - end - return agent.active_run.promise -end - -""" - reset(agent) - -Clear transcript state, runtime state, and queued messages. -""" -function reset!(agent::Agent) - agent._state.messages = AgentMessage[] - agent._state.is_streaming = false - agent._state.streaming_message = nothing - agent._state.pending_tool_calls = Set{String}() - agent._state.error_message = nothing - clearFollowUpQueue(agent) - clearSteeringQueue(agent) -end - -""" - prompt(agent, input[, images]) - -Start a new prompt from text, a single message, or a batch of messages. -""" -function prompt(agent::Agent, input::Union{String, AgentMessage, Vector{AgentMessage}}, images::Vector{ImageContent}=ImageContent[])::Nothing - if !isnothing(agent.active_run) - throw(ErrorException( - "Agent is already processing a prompt. Use steer() or followUp() to queue messages, or wait for completion." - )) - end - messages = normalizePromptInput(agent, input, images) - runPromptMessages(agent, messages) -end - -function normalizePromptInput(agent::Agent, input::Vector{AgentMessage}, images::Vector{ImageContent})::Vector{AgentMessage} - return input -end - -function normalizePromptInput(agent::Agent, input::AgentMessage, images::Vector{ImageContent})::Vector{AgentMessage} - return [input] -end - -function normalizePromptInput(agent::Agent, input::String, images::Vector{ImageContent})::Vector{AgentMessage} - content::Vector{MessageContent} = [TextContent(input)] - if !isempty(images) - append!(content, images) - end - return [UserMessage("user", content, Int64(Dates.now(Dates.UTC).datetime))] -end - -function runPromptMessages(agent::Agent, messages::Vector{AgentMessage})::Nothing - # TODO: Implement run with lifecycle - return nothing -end - -""" - continue(agent) - -Continue from the current transcript. The last message must be a user or tool-result message. -""" -function continue!(agent::Agent)::Nothing - if !isnothing(agent.active_run) - throw(ErrorException("Agent is already processing. Wait for completion before continuing.")) - end - - last_message = agent._state.messages[end] - if isnothing(last_message) - throw(ErrorException("No messages to continue from")) - end - - if last_message.role == "assistant" - queued_steering = drain(agent.steering_queue) - if !isempty(queued_steering) - runPromptMessages(agent, queued_steering) - return nothing - end - - queued_follow_ups = drain(agent.follow_up_queue) - if !isempty(queued_follow_ups) - runPromptMessages(agent, queued_follow_ups) - return nothing - end - - throw(ErrorException("Cannot continue from message role: assistant")) - end - - # TODO: Implement run continuation - return nothing -end - -""" - createContextSnapshot(agent) - -Create a snapshot of the current context for use in the agent loop. -""" -function createContextSnapshot(agent::Agent)::AgentContext - return AgentContext( - agent._state.system_prompt, - copy(agent._state.messages), - copy(agent._state.tools), - ) -end - -""" - createLoopConfig(agent, options) - -Create the loop configuration for the agent. -""" -function createLoopConfig(agent::Agent, options::Dict{String, Any}=Dict{String, Any}())::AgentLoopConfig - skip_initial_steering_poll = get(options, "skipInitialSteeringPoll", false) - return AgentLoopConfig( - agent._state.model, - agent._state.thinking_level == THINKING_OFF ? nothing : agent._state.thinking_level, - agent.session_id, - agent.on_payload, - agent.on_response, - agent.transport, - agent.thinking_budgets, - agent.max_retry_delay_ms, - agent.tool_execution, - agent.before_tool_call, - agent.after_tool_call, - isnothing(agent.prepare_next_turn_with_context) && isnothing(agent.prepare_next_turn) ? nothing : function(context) - if !isnothing(agent.prepare_next_turn_with_context) - return agent.prepare_next_turn_with_context(context, getSignal(agent)) - end - return isnothing(agent.prepare_next_turn) ? nothing : agent.prepare_next_turn(getSignal(agent)) - end, - agent.convert_to_llm, - agent.transform_context, - agent.get_api_key, - function() - if skip_initial_steering_poll - skip_initial_steering_poll = false - return AgentMessage[] - end - return drain(agent.steering_queue) - end, - function() - return drain(agent.follow_up_queue) - end, - ) -end - -""" - getSignal(agent) - -Get the active abort signal for the current run, if any. -""" -function getSignal(agent::Agent)::Union{Nothing, Base.Atomic{Bool}} - if isnothing(agent.active_run) - return nothing - end - return agent.active_run.abort_controller -end - -end diff --git a/packages/agent/julia_implementation/src/agent_loop.jl b/packages/agent/julia_implementation/src/agent_loop.jl deleted file mode 100644 index 456cec26..00000000 --- a/packages/agent/julia_implementation/src/agent_loop.jl +++ /dev/null @@ -1,861 +0,0 @@ -""" - agent_loop.jl - Low-level agent loop implementation - -This module implements the core agentLoop functionality that works with AgentMessage -throughout, transforming to Message[] only at the LLM call boundary. -""" - -module AgentLoop - -using ..Types: * -using ..StreamFn: * - -# ============================================================================ -# Event sink type -# ============================================================================ - -const AgentEventSink = Function - -# ============================================================================ -# Main agent loop function -# ============================================================================ - -function agentLoop( - prompts::Vector{AgentMessage}, - context::AgentContext, - config::AgentLoopConfig, - signal::Union{Nothing, AbortSignal}, - stream_fn::StreamFn, -)::EventStream - stream = createAgentStream() - - Threads.@spawn begin - messages = runAgentLoop( - prompts, - context, - config, - (event) -> push!(stream, event), - signal, - stream_fn, - ) - end(stream, messages) - end - - return stream -end - -# ============================================================================ -# Continue agent loop function -# ============================================================================ - -function agentLoopContinue( - context::AgentContext, - config::AgentLoopConfig, - signal::Union{Nothing, AbortSignal}, - stream_fn::StreamFn, -)::EventStream - if isempty(context.messages) - throw(ErrorException("Cannot continue: no messages in context")) - end - - if context.messages[end].role == "assistant" - throw(ErrorException("Cannot continue from message role: assistant")) - end - - stream = createAgentStream() - - Threads.@spawn begin - messages = runAgentLoopContinue( - context, - config, - (event) -> push!(stream, event), - signal, - stream_fn, - ) - end(stream, messages) - end - - return stream -end - -# ============================================================================ -# Run agent loop function -# ============================================================================ - -function runAgentLoop( - prompts::Vector{AgentMessage}, - context::AgentContext, - config::AgentLoopConfig, - emit::AgentEventSink, - signal::Union{Nothing, AbortSignal}, - stream_fn::StreamFn, -)::Vector{AgentMessage} - new_messages::Vector{AgentMessage} = copy(prompts) - current_context::AgentContext = AgentContext( - context.system_prompt, - vcat(context.messages, copy(prompts)), - context.tools, - ) - - emit(AgentStartEvent()) - emit(TurnStartEvent()) - for prompt in prompts - emit(MessageStartEvent(prompt)) - emit(MessageEndEvent(prompt)) - end - - runLoop( - current_context, - new_messages, - config, - signal, - emit, - stream_fn, - ) - return new_messages -end - -# ============================================================================ -# Run agent loop continue function -# ============================================================================ - -function runAgentLoopContinue( - context::AgentContext, - config::AgentLoopConfig, - emit::AgentEventSink, - signal::Union{Nothing, AbortSignal}, - stream_fn::StreamFn, -)::Vector{AgentMessage} - if isempty(context.messages) - throw(ErrorException("Cannot continue: no messages in context")) - end - - if context.messages[end].role == "assistant" - throw(ErrorException("Cannot continue from message role: assistant")) - end - - new_messages::Vector{AgentMessage} = [] - current_context::AgentContext = context - - emit(AgentStartEvent()) - emit(TurnStartEvent()) - - runLoop( - current_context, - new_messages, - config, - signal, - emit, - stream_fn, - ) - return new_messages -end - -# ============================================================================ -# Create agent stream function -# ============================================================================ - -function createAgentStream()::EventStream - return EventStream( - (event::AgentEvent) -> event isa AgentEndEvent, - (event::AgentEvent) -> event isa AgentEndEvent ? event.messages : AgentMessage[], - ) -end - -# ============================================================================ -# Main loop logic shared by agentLoop and agentLoopContinue -# ============================================================================ - -function runLoop( - initial_context::AgentContext, - new_messages::Vector{AgentMessage}, - initial_config::AgentLoopConfig, - signal::Union{Nothing, AbortSignal}, - emit::AgentEventSink, - stream_function::StreamFn, -)::Nothing - current_context::AgentContext = initial_context - config::AgentLoopConfig = initial_config - first_turn::Bool = true - pending_messages::Vector{AgentMessage} = getSteeringMessages(config) do - get_steering_messages(config) - end - - while true - has_more_tool_calls::Bool = true - - while has_more_tool_calls || !isempty(pending_messages) - if !first_turn - emit(TurnStartEvent()) - else - first_turn = false - end - - if !isempty(pending_messages) - for message in pending_messages - emit(MessageStartEvent(message)) - emit(MessageEndEvent(message)) - push!(current_context.messages, message) - push!(new_messages, message) - end - pending_messages = AgentMessage[] - end - - message = streamAssistantResponse( - current_context, - config, - signal, - emit, - stream_function, - ) - push!(new_messages, message) - - if message.stop_reason in ("error", "aborted") - emit(TurnEndEvent(message, ToolResultMessage[])) - emit(AgentEndEvent(new_messages)) - return - end - - tool_calls = filter( - (c) -> c isa ToolCall, - message.content, - ) - - tool_results::Vector{ToolResultMessage} = [] - has_more_tool_calls = false - if !isempty(tool_calls) - executed_tool_batch = - message.stop_reason == "length" - ? failToolCallsFromTruncatedMessage(tool_calls, emit) - : executeToolCalls( - current_context, - message, - config, - signal, - emit, - ) - append!(tool_results, executed_tool_batch.messages) - has_more_tool_calls = !executed_tool_batch.terminate - - for result in tool_results - push!(current_context.messages, result) - push!(new_messages, result) - end - end - - emit(TurnEndEvent(message, tool_results)) - - next_turn_context = PrepareNextTurnContext( - message, - tool_results, - current_context, - new_messages, - ) - next_turn_snapshot = prepare_next_turn(config, next_turn_context) - - if !isnothing(next_turn_snapshot) - current_context = next_turn_snapshot.context - config = AgentLoopConfig( - model = next_turn_snapshot.model, - reasoning = next_turn_snapshot.thinking_level, - convert_to_llm = config.convert_to_llm, - transform_context = config.transform_context, - get_api_key = config.get_api_key, - should_stop_after_turn = config.should_stop_after_turn, - prepare_next_turn = config.prepare_next_turn, - get_steering_messages = config.get_steering_messages, - get_follow_up_messages = config.get_follow_up_messages, - tool_execution = config.tool_execution, - before_tool_call = config.before_tool_call, - after_tool_call = config.after_tool_call, - max_tokens = config.max_tokens, - temperature = config.temperature, - reasoning = config.reasoning, - cache_retention = config.cache_retention, - session_id = config.session_id, - headers = config.headers, - metadata = config.metadata, - transport = config.transport, - signal = signal, - api_key = config.api_key, - on_payload = config.on_payload, - on_response = config.on_response, - max_retry_delay_ms = config.max_retry_delay_ms, - ) - end - - if should_stop_after_turn(config, next_turn_context) - emit(AgentEndEvent(new_messages)) - return - end - - pending_messages = getSteeringMessages(config) do - get_steering_messages(config) - end - end - - follow_up_messages = getFollowUpMessages(config) do - get_follow_up_messages(config) - end - - if !isempty(follow_up_messages) - pending_messages = follow_up_messages - continue - end - - break - end - - emit(AgentEndEvent(new_messages)) -end - -# ============================================================================ -# Helper types -# ============================================================================ - -struct PrepareNextTurnContext - message::AssistantMessage - tool_results::Vector{ToolResultMessage} - context::AgentContext - new_messages::Vector{AgentMessage} -end - -struct AgentLoopTurnUpdate - context::Union{AgentContext, Nothing} - model::Union{Model, Nothing} - thinking_level::Union{ThinkingLevel, Nothing} -end - -# ============================================================================ -# Helper functions for getting messages from queues -# ============================================================================ - -macro getSteeringMessages(config) - :(get_steering_messages($(esc(config)))) -end - -macro getFollowUpMessages(config) - :(get_follow_up_messages($(esc(config)))) -end - -function get_steering_messages(config::AgentLoopConfig)::Vector{AgentMessage} - return isnothing(config.get_steering_messages) ? AgentMessage[] : config.get_steering_messages() -end - -function get_follow_up_messages(config::AgentLoopConfig)::Vector{AgentMessage} - return isnothing(config.get_follow_up_messages) ? AgentMessage[] : config.get_follow_up_messages() -end - -function prepare_next_turn(config::AgentLoopConfig, context::PrepareNextTurnContext)::Union{AgentLoopTurnUpdate, Nothing} - return isnothing(config.prepare_next_turn) ? nothing : config.prepare_next_turn(context) -end - -function should_stop_after_turn(config::AgentLoopConfig, context::PrepareNextTurnContext)::Bool - return isnothing(config.should_stop_after_turn) ? false : config.should_stop_after_turn(context) -end - -# ============================================================================ -# Stream assistant response function -# ============================================================================ - -function streamAssistantResponse( - context::AgentContext, - config::AgentLoopConfig, - signal::Union{Nothing, AbortSignal}, - emit::AgentEventSink, - stream_function::StreamFn, -)::AssistantMessage - messages::Vector{AgentMessage} = context.messages - - if !isnothing(config.transform_context) - messages = config.transform_context(messages, signal) - end - - llm_messages::Vector{Message} = config.convert_to_llm(messages) - - llm_context::Context = Context( - context.system_prompt, - llm_messages, - context.tools, - ) - - resolved_api_key::Union{String, Nothing} = - !isnothing(config.get_api_key) - ? config.get_api_key(config.model.provider) - : nothing - - response = stream_function( - config.model, - llm_context, - merge( - config, - Dict(:apiKey => resolved_api_key, :signal => signal), - ), - ) - - partial_message::Union{AssistantMessage, Nothing} = nothing - added_partial::Bool = false - - for event in response - if event.type == "start" - partial_message = event.partial - push!(context.messages, partial_message) - added_partial = true - emit(MessageStartEvent(copy(partial_message))) - elseif event.type in ("text_start", "text_delta", "text_end", "thinking_start", "thinking_delta", "thinking_end", "toolcall_start", "toolcall_delta", "toolcall_end") - if !isnothing(partial_message) - partial_message = event.partial - context.messages[end] = partial_message - emit(MessageUpdateEvent(copy(partial_message), event)) - end - elseif event.type in ("done", "error") - final_message = response.result() - if added_partial - context.messages[end] = final_message - else - push!(context.messages, final_message) - end - if !added_partial - emit(MessageStartEvent(copy(final_message))) - end - emit(MessageEndEvent(final_message)) - return final_message - end - end - - final_message = response.result() - if added_partial - context.messages[end] = final_message - else - push!(context.messages, final_message) - emit(MessageStartEvent(copy(final_message))) - end - emit(MessageEndEvent(final_message)) - return final_message -end - -# ============================================================================ -# Fail tool calls from truncated message -# ============================================================================ - -struct ExecutedToolCallBatch - messages::Vector{ToolResultMessage} - terminate::Bool -end - -function failToolCallsFromTruncatedMessage( - tool_calls::Vector{ToolCall}, - emit::AgentEventSink, -)::ExecutedToolCallBatch - messages::Vector{ToolResultMessage} = [] - - for tool_call in tool_calls - emit(ToolExecutionStartEvent(tool_call.id, tool_call.name, tool_call.arguments)) - - finalized = FinalizedToolCallOutcome( - tool_call, - createErrorToolResult( - "Tool call \"$(tool_call.name)\" was not executed: the response hit the output token limit, so its arguments may be truncated. Re-issue the tool call with complete arguments.", - ), - true, - ) - - emitToolExecutionEnd(finalized, emit) - tool_result_message = createToolResultMessage(finalized) - emitToolResultMessage(tool_result_message, emit) - push!(messages, tool_result_message) - end - - return ExecutedToolCallBatch(messages, false) -end - -# ============================================================================ -# Execute tool calls -# ============================================================================ - -function executeToolCalls( - current_context::AgentContext, - assistant_message::AssistantMessage, - config::AgentLoopConfig, - signal::Union{Nothing, AbortSignal}, - emit::AgentEventSink, -)::ExecutedToolCallBatch - tool_calls = filter( - (c) -> c isa ToolCall, - assistant_message.content, - ) - - has_sequential_tool_call = any( - (tc) -> begin - tool = findfirst((t) -> t.name == tc.name, current_context.tools) - !isnothing(tool) && tool.execution_mode == EXECUTION_SEQUENTIAL - end, - tool_calls, - ) - - if config.tool_execution == EXECUTION_SEQUENTIAL || has_sequential_tool_call - return executeToolCallsSequential( - current_context, - assistant_message, - tool_calls, - config, - signal, - emit, - ) - end - return executeToolCallsParallel( - current_context, - assistant_message, - tool_calls, - config, - signal, - emit, - ) -end - -# ============================================================================ -# Execute tool calls sequentially -# ============================================================================ - -function executeToolCallsSequential( - current_context::AgentContext, - assistant_message::AssistantMessage, - tool_calls::Vector{ToolCall}, - config::AgentLoopConfig, - signal::Union{Nothing, AbortSignal}, - emit::AgentEventSink, -)::ExecutedToolCallBatch - finalized_calls::Vector{FinalizedToolCallOutcome} = [] - messages::Vector{ToolResultMessage} = [] - - for tool_call in tool_calls - emit(ToolExecutionStartEvent(tool_call.id, tool_call.name, tool_call.arguments)) - - preparation = prepareToolCall(current_context, assistant_message, tool_call, config, signal) - - finalized = if preparation.kind == "immediate" - FinalizedToolCallOutcome(tool_call, preparation.result, preparation.is_error) - else - executed = executePreparedToolCall(preparation, signal, emit) - finalizeExecutedToolCall( - current_context, - assistant_message, - preparation, - executed, - config, - signal, - ) - end - - emitToolExecutionEnd(finalized, emit) - tool_result_message = createToolResultMessage(finalized) - emitToolResultMessage(tool_result_message, emit) - push!(finalized_calls, finalized) - push!(messages, tool_result_message) - - if !isnothing(signal) && signal.aborted - break - end - end - - return ExecutedToolCallBatch(messages, shouldTerminateToolBatch(finalized_calls)) -end - -# ============================================================================ -# Execute tool calls in parallel -# ============================================================================ - -function executeToolCallsParallel( - current_context::AgentContext, - assistant_message::AssistantMessage, - tool_calls::Vector{ToolCall}, - config::AgentLoopConfig, - signal::Union{Nothing, AbortSignal}, - emit::AgentEventSink, -)::ExecutedToolCallBatch - finalized_calls::Vector{Union{FinalizedToolCallOutcome, Function}} = [] - - for tool_call in tool_calls - emit(ToolExecutionStartEvent(tool_call.id, tool_call.name, tool_call.arguments)) - - preparation = prepareToolCall(current_context, assistant_message, tool_call, config, signal) - - if preparation.kind == "immediate" - finalized = FinalizedToolCallOutcome( - tool_call, - preparation.result, - preparation.is_error, - ) - emitToolExecutionEnd(finalized, emit) - push!(finalized_calls, finalized) - if !isnothing(signal) && signal.aborted - break - end - continue - end - - push!(finalized_calls, () -> begin - executed = executePreparedToolCall(preparation, signal, emit) - finalized = finalizeExecutedToolCall( - current_context, - assistant_message, - preparation, - executed, - config, - signal, - ) - emitToolExecutionEnd(finalized, emit) - return finalized - end) - - if !isnothing(signal) && signal.aborted - break - end - end - - ordered_finalized_calls = map( - (entry) -> if entry isa Function - entry() - else - entry - end, - finalized_calls, - ) - - messages::Vector{ToolResultMessage} = [] - for finalized in ordered_finalized_calls - tool_result_message = createToolResultMessage(finalized) - emitToolResultMessage(tool_result_message, emit) - push!(messages, tool_result_message) - end - - return ExecutedToolCallBatch(messages, shouldTerminateToolBatch(ordered_finalized_calls)) -end - -# ============================================================================ -# Prepared tool call types -# ============================================================================ - -struct PreparedToolCall - kind::String - tool_call::ToolCall - tool::AgentTool - args::Any -end - -struct ImmediateToolCallOutcome - kind::String - result::AgentToolResultMutable - is_error::Bool -end - -struct ExecutedToolCallOutcome - result::AgentToolResultMutable - is_error::Bool -end - -struct FinalizedToolCallOutcome - tool_call::ToolCall - result::AgentToolResultMutable - is_error::Bool -end - -# ============================================================================ -# Helper functions -# ============================================================================ - -function shouldTerminateToolBatch(finalized_calls::Vector{FinalizedToolCallOutcome})::Bool - return !isempty(finalized_calls) && all( - (finalized) -> finalized.result.terminate === true, - finalized_calls, - ) -end - -function prepareToolCallArguments(tool::AgentTool, tool_call::ToolCall)::ToolCall - if isnothing(tool.prepare_arguments) - return tool_call - end - prepared_arguments = tool.prepare_arguments(tool_call.arguments) - if prepared_arguments === tool_call.arguments - return tool_call - end - return ToolCall( - tool_call.type, - tool_call.id, - tool_call.name, - prepared_arguments, - tool_call.partial_json, - ) -end - -function prepareToolCall( - current_context::AgentContext, - assistant_message::AssistantMessage, - tool_call::ToolCall, - config::AgentLoopConfig, - signal::Union{Nothing, AbortSignal}, -)::Union{PreparedToolCall, ImmediateToolCallOutcome} - tool = findfirst((t) -> t.name == tool_call.name, current_context.tools) - if isnothing(tool) - return ImmediateToolCallOutcome("immediate", createErrorToolResult("Tool $(tool_call.name) not found"), true) - end - - try - prepared_tool_call = prepareToolCallArguments(tool, tool_call) - validated_args = validateToolArguments(tool, prepared_tool_call) - - if !isnothing(config.before_tool_call) - before_result = config.before_tool_call( - BeforeToolCallContext(assistant_message, tool_call, validated_args, current_context), - signal, - ) - if !isnothing(signal) && signal.aborted - return ImmediateToolCallOutcome("immediate", createErrorToolResult("Operation aborted"), true) - end - if !isnothing(before_result) && before_result.block - reason = isnothing(before_result.reason) ? "Tool execution was blocked" : before_result.reason - return ImmediateToolCallOutcome("immediate", createErrorToolResult(reason), true) - end - end - - if !isnothing(signal) && signal.aborted - return ImmediateToolCallOutcome("immediate", createErrorToolResult("Operation aborted"), true) - end - - return PreparedToolCall("prepared", tool_call, tool, validated_args) - catch error - return ImmediateToolCallOutcome("immediate", createErrorToolResult(string(error)), true) - end -end - -function executePreparedToolCall( - prepared::PreparedToolCall, - signal::Union{Nothing, AbortSignal}, - emit::AgentEventSink, -)::ExecutedToolCallOutcome - update_events::Vector{Future} = [] - accepting_updates::Bool = true - - try - result = prepared.tool.execute( - prepared.tool_call.id, - prepared.args, - signal, - (partial_result) -> begin - if !accepting_updates - return - end - push!( - update_events, - Threads.@spawn begin - emit( - ToolExecutionUpdateEvent( - prepared.tool_call.id, - prepared.tool_call.name, - prepared.tool_call.arguments, - partial_result, - ), - ) - end, - ) - end, - ) - accepting_updates = false - wait.(update_events) - return ExecutedToolCallOutcome(result, false) - catch error - accepting_updates = false - wait.(update_events) - return ExecutedToolCallOutcome(createErrorToolResult(string(error)), true) - finally - accepting_updates = false - end -end - -function finalizeExecutedToolCall( - current_context::AgentContext, - assistant_message::AssistantMessage, - prepared::PreparedToolCall, - executed::ExecutedToolCallOutcome, - config::AgentLoopConfig, - signal::Union{Nothing, AbortSignal}, -)::FinalizedToolCallOutcome - result = executed.result - is_error = executed.is_error - - if !isnothing(config.after_tool_call) - try - after_result = config.after_tool_call( - AfterToolCallContext( - assistant_message, - prepared.tool_call, - prepared.args, - result, - is_error, - current_context, - ), - signal, - ) - if !isnothing(after_result) - result = AgentToolResultMutable( - isnothing(after_result.content) ? result.content : after_result.content, - isnothing(after_result.details) ? result.details : after_result.details, - isnothing(after_result.usage) ? result.usage : after_result.usage, - result.added_tool_names, - isnothing(after_result.terminate) ? result.terminate : after_result.terminate, - ) - is_error = isnothing(after_result.is_error) ? is_error : after_result.is_error - end - catch error - result = createErrorToolResult(string(error)) - is_error = true - end - end - - return FinalizedToolCallOutcome(prepared.tool_call, result, is_error) -end - -function createErrorToolResult(message::String)::AgentToolResultMutable - return AgentToolResultMutable([TextContent(message)], Dict{String, Any}(), nothing, nothing, nothing) -end - -function emitToolExecutionEnd(finalized::FinalizedToolCallOutcome, emit::AgentEventSink)::Nothing - emit(ToolExecutionEndEvent( - finalized.tool_call.id, - finalized.tool_call.name, - finalized.result, - finalized.is_error, - )) - return nothing -end - -function createToolResultMessage(finalized::FinalizedToolCallOutcome)::ToolResultMessage - return ToolResultMessage( - "toolResult", - finalized.tool_call.id, - finalized.tool_call.name, - isnothing(finalized.result.content) ? MessageContent[] : finalized.result.content, - finalized.result.details, - finalized.result.usage, - finalized.result.added_tool_names, - finalized.is_error, - Dates.now(Dates.UTC).datetime, - ) -end - -function emitToolResultMessage(tool_result_message::ToolResultMessage, emit::AgentEventSink)::Nothing - emit(MessageStartEvent(tool_result_message)) - emit(MessageEndEvent(tool_result_message)) - return nothing -end - -# ============================================================================ -# Validation helper -# ============================================================================ - -function validateToolArguments(tool::AgentTool, tool_call::ToolCall)::Any - # Simplified validation - in a full implementation, this would use TypeBox-like validation - return tool_call.arguments -end - -end diff --git a/packages/agent/julia_implementation/src/harness_types.jl b/packages/agent/julia_implementation/src/harness_types.jl deleted file mode 100644 index 0a583cdd..00000000 --- a/packages/agent/julia_implementation/src/harness_types.jl +++ /dev/null @@ -1,1083 +0,0 @@ -""" - harness_types.jl - Extended types for AgentHarness - -This module defines the extended types used by the AgentHarness. -""" - -module HarnessTypes - -using ..Types: * -using ..Session: Session - -# ============================================================================ -# Result type -# ============================================================================ - -abstract type Result{TValue, TError} end - -struct Ok{TValue, TError} <: Result{TValue, TError} - value::TValue -end - -struct Err{TValue, TError} <: Result{TValue, TError} - error::TError -end - -function ok{TValue, TError}(value::TValue)::Ok{TValue, TError} - return Ok{TValue, TError}(value) -end - -function err{TValue, TError}(error::TError)::Err{TValue, TError} - return Err{TValue, TError}(error) -end - -function getOrThrow{TValue, TError}(result::Result{TValue, TError})::TValue - if result isa Ok - return result.value - else - throw(result.error) - end -end - -function getOrUndefined{TValue<:AbstractDict, TError}(result::Result{TValue, TError})::Union{TValue, Nothing} - if result isa Ok - return result.value - else - return nothing - end -end - -function toError(error::Any)::Error - if error isa Error - return error - elseif error isa AbstractString - return ErrorException(error) - else - try - return ErrorException(string(error)) - catch - return ErrorException("Unknown error") - end - end -end - -# ============================================================================ -# Skill types -# ============================================================================ - -mutable struct Skill - name::String - description::String - content::String - filePath::String - disableModelInvocation::Bool -end - -mutable struct PromptTemplate - name::String - description::Union{String, Nothing} - content::String -end - -mutable struct AgentHarnessResources{TSkill<:Skill, TPromptTemplate<:PromptTemplate} - promptTemplates::Union{Vector{TPromptTemplate}, Nothing} - skills::Union{Vector{TSkill}, Nothing} -end - -# ============================================================================ -# Tool types -# ============================================================================ - -mutable struct AgentHarnessTool{TContext, TParameters, TDetails} - name::String - label::String - description::String - parameters::TParameters - execute::Function - prepareArguments::Union{Function, Nothing} - executionMode::Union{ToolExecutionMode, Nothing} -end - -mutable struct AgentHarnessToolContextSource{TContext} - context::Union{TContext, Function} -end - -# ============================================================================ -# Stream options -# ============================================================================ - -mutable struct AgentHarnessStreamOptions - transport::Union{String, Nothing} - timeout_ms::Union{Int64, Nothing} - max_retries::Union{Int64, Nothing} - max_retry_delay_ms::Union{Int64, Nothing} - headers::Union{Dict{String, String}, Nothing} - metadata::Union{Dict{String, Any}, Nothing} - cache_retention::Union{String, Nothing} -end - -mutable struct AgentHarnessStreamOptionsPatch - transport::Union{String, Nothing} - timeout_ms::Union{Int64, Nothing} - max_retries::Union{Int64, Nothing} - max_retry_delay_ms::Union{Int64, Nothing} - cache_retention::Union{String, Nothing} - headers::Union{Dict{String, String}, Nothing} - metadata::Union{Dict{String, Any}, Nothing} -end - -# ============================================================================ -# File system types -# ============================================================================ - -const FileKind = String -const FILE_KIND_FILE = "file" -const FILE_KIND_DIRECTORY = "directory" -const FILE_KIND_SYMLINK = "symlink" - -const FileErrorCode = String -const FILE_ERROR_ABORTED = "aborted" -const FILE_ERROR_NOT_FOUND = "not_found" -const FILE_ERROR_PERMISSION_DENIED = "permission_denied" -const FILE_ERROR_NOT_DIRECTORY = "not_directory" -const FILE_ERROR_IS_DIRECTORY = "is_directory" -const FILE_ERROR_INVALID = "invalid" -const FILE_ERROR_NOT_SUPPORTED = "not_supported" -const FILE_ERROR_UNKNOWN = "unknown" - -mutable struct FileError <: Exception - code::FileErrorCode - message::String - path::Union{String, Nothing} - cause::Union{Exception, Nothing} -end - -# ============================================================================ -# Execution error types -# ============================================================================ - -const ExecutionErrorCode = String -const EXECUTION_ERROR_ABORTED = "aborted" -const EXECUTION_ERROR_TIMEOUT = "timeout" -const EXECUTION_ERROR_SHELL_UNAVAILABLE = "shell_unavailable" -const EXECISION_ERROR_SPAWN_ERROR = "spawn_error" -const EXECUTION_ERROR_CALLBACK_ERROR = "callback_error" -const EXECUTION_ERROR_UNKNOWN = "unknown" - -mutable struct ExecutionError <: Exception - code::ExecutionErrorCode - message::String - cause::Union{Exception, Nothing} -end - -# ============================================================================ -# Compaction error types -# ============================================================================ - -const CompactionErrorCode = String -const COMPACTION_ERROR_ABORTED = "aborted" -const COMPACTION_ERROR_SUMMARIZATION_FAILED = "summarization_failed" -const COMPACTION_ERROR_INVALID_SESSION = "invalid_session" -const COMPACTION_ERROR_UNKNOWN = "unknown" - -mutable struct CompactionError <: Exception - code::CompactionErrorCode - message::String - cause::Union{Exception, Nothing} -end - -# ============================================================================ -# Branch summary error types -# ============================================================================ - -const BranchSummaryErrorCode = String -const BRANCH_SUMMARY_ERROR_ABORTED = "aborted" -const BRANCH_SUMMARY_ERROR_SUMMARIZATION_FAILED = "summarization_failed" -const BRANCH_SUMMARY_ERROR_INVALID_SESSION = "invalid_session" - -mutable struct BranchSummaryError <: Exception - code::BranchSummaryErrorCode - message::String - cause::Union{Exception, Nothing} -end - -# ============================================================================ -# Session error types -# ============================================================================ - -const SessionErrorCode = String -const SESSION_ERROR_NOT_FOUND = "not_found" -const SESSION_ERROR_INVALID_SESSION = "invalid_session" -const SESSION_ERROR_INVALID_ENTRY = "invalid_entry" -const SESSION_ERROR_INVALID_FORK_TARGET = "invalid_fork_target" -const SESSION_ERROR_STORAGE = "storage" -const SESSION_ERROR_UNKNOWN = "unknown" - -mutable struct SessionError <: Exception - code::SessionErrorCode - message::String - cause::Union{Exception, Nothing} -end - -# ============================================================================ -# Agent harness error types -# ============================================================================ - -const AgentHarnessErrorCode = String -const AGENT_HARNESS_ERROR_BUSY = "busy" -const AGENT_HARNESS_ERROR_INVALID_STATE = "invalid_state" -const AGENT_HARNESS_ERROR_INVALID_ARGUMENT = "invalid_argument" -const AGENT_HARNESS_ERROR_SESSION = "session" -const AGENT_HARNESS_ERROR_HOOK = "hook" -const AGENT_HARNESS_ERROR_AUTH = "auth" -const AGENT_HARNESS_ERROR_COMPACTION = "compaction" -const AGENT_HARNESS_ERROR_BRANCH_SUMMARY = "branch_summary" -const AGENT_HARNESS_ERROR_UNKNOWN = "unknown" - -mutable struct AgentHarnessError <: Exception - code::AgentHarnessErrorCode - message::String - cause::Union{Exception, Nothing} -end - -# ============================================================================ -# File system interface -# ============================================================================ - -abstract type FileSystem end - -function absolutePath(fs::FileSystem, path::String, abortSignal::Union{Nothing, Any})::Result{String, FileError} - return err(FileError("not_supported", "absolutePath not implemented", path, nothing)) -end - -function joinPath(fs::FileSystem, parts::Vector{String}, abortSignal::Union{Nothing, Any})::Result{String, FileError} - return err(FileError("not_supported", "joinPath not implemented", nothing, nothing)) -end - -function readTextFile(fs::FileSystem, path::String, abortSignal::Union{Nothing, Any})::Result{String, FileError} - return err(FileError("not_supported", "readTextFile not implemented", path, nothing)) -end - -function readTextLines( - fs::FileSystem, - path::String, - options::Dict{String, Any}, -)::Result{Vector{String}, FileError} - return err(FileError("not_supported", "readTextLines not implemented", path, nothing)) -end - -function readBinaryFile(fs::FileSystem, path::String, abortSignal::Union{Nothing, Any})::Result{Vector{UInt8}, FileError} - return err(FileError("not_supported", "readBinaryFile not implemented", path, nothing)) -end - -function writeFile(fs::FileSystem, path::String, content::Union{String, Vector{UInt8}}, abortSignal::Union{Nothing, Any})::Result{Nothing, FileError} - return err(FileError("not_supported", "writeFile not implemented", path, nothing)) -end - -function appendFile(fs::FileSystem, path::String, content::Union{String, Vector{UInt8}}, abortSignal::Union{Nothing, Any})::Result{Nothing, FileError} - return err(FileError("not_supported", "appendFile not implemented", path, nothing)) -end - -function fileInfo(fs::FileSystem, path::String, abortSignal::Union{Nothing, Any})::Result{FileInfo, FileError} - return err(FileError("not_supported", "fileInfo not implemented", path, nothing)) -end - -function listDir(fs::FileSystem, path::String, abortSignal::Union{Nothing, Any})::Result{Vector{FileInfo}, FileError} - return err(FileError("not_supported", "listDir not implemented", path, nothing)) -end - -function canonicalPath(fs::FileSystem, path::String, abortSignal::Union{Nothing, Any})::Result{String, FileError} - return err(FileError("not_supported", "canonicalPath not implemented", path, nothing)) -end - -function exists(fs::FileSystem, path::String, abortSignal::Union{Nothing, Any})::Result{Bool, FileError} - return err(FileError("not_supported", "exists not implemented", path, nothing)) -end - -function createDir( - fs::FileSystem, - path::String, - options::Dict{String, Any}, -)::Result{Nothing, FileError} - return err(FileError("not_supported", "createDir not implemented", path, nothing)) -end - -function remove( - fs::FileSystem, - path::String, - options::Dict{String, Any}, -)::Result{Nothing, FileError} - return err(FileError("not_supported", "remove not implemented", path, nothing)) -end - -function createTempDir(fs::FileSystem, prefix::String="tmp-", abortSignal::Union{Nothing, Any})::Result{String, FileError} - return err(FileError("not_supported", "createTempDir not implemented", nothing, nothing)) -end - -function createTempFile(fs::FileSystem, options::Dict{String, Any})::Result{String, FileError} - return err(FileError("not_supported", "createTempFile not implemented", nothing, nothing)) -end - -function cleanup(fs::FileSystem)::Nothing - return nothing -end - -# ============================================================================ -# Shell interface -# ============================================================================ - -mutable struct ShellExecOptions - cwd::Union{String, Nothing} - env::Union{Dict{String, String}, Nothing} - inheritEnv::Bool - timeout::Union{Int64, Nothing} - abortSignal::Union{Any, Nothing} - onStdout::Union{Function, Nothing} - onStderr::Union{Function, Nothing} -end - -abstract type Shell end - -function exec(shell::Shell, command::String, options::Dict{String, Any})::Result{Dict{String, Any}, ExecutionError} - return err(ExecutionError("not_supported", "exec not implemented", nothing)) -end - -function cleanup(shell::Shell)::Nothing - return nothing -end - -# ============================================================================ -# Execution environment -# ============================================================================ - -abstract type ExecutionEnv <: FileSystem, Shell end - -# ============================================================================ -# Session tree entry types -# ============================================================================ - -abstract type SessionTreeEntry end - -struct MessageEntry <: SessionTreeEntry - type::String - id::String - parent_id::Union{String, Nothing} - timestamp::String - message::AgentMessage -end - -struct ThinkingLevelChangeEntry <: SessionTreeEntry - type::String - id::String - parent_id::Union{String, Nothing} - timestamp::String - thinking_level::String -end - -struct ModelChangeEntry <: SessionTreeEntry - type::String - id::String - parent_id::Union{String, Nothing} - timestamp::String - provider::String - model_id::String -end - -struct ActiveToolsChangeEntry <: SessionTreeEntry - type::String - id::String - parent_id::Union{String, Nothing} - timestamp::String - active_tool_names::Vector{String} -end - -struct CompactionEntry <: SessionTreeEntry - type::String - id::String - parent_id::Union{String, Nothing} - timestamp::String - summary::String - first_kept_entry_id::Union{String, Nothing} - tokens_before::Int64 - retained_tail::Union{Vector{AgentMessage}, Nothing} - details::Union{Any, Nothing} - usage::Union{Usage, Nothing} - from_hook::Bool -end - -struct BranchSummaryEntry <: SessionTreeEntry - type::String - id::String - parent_id::Union{String, Nothing} - timestamp::String - from_id::String - summary::String - details::Union{Any, Nothing} - usage::Union{Usage, Nothing} - from_hook::Bool -end - -struct CustomEntry <: SessionTreeEntry - type::String - id::String - parent_id::Union{String, Nothing} - timestamp::String - custom_type::String - data::Union{Any, Nothing} -end - -struct CustomMessageEntry <: SessionTreeEntry - type::String - id::String - parent_id::Union{String, Nothing} - timestamp::String - custom_type::String - content::String - details::Union{Any, Nothing} - display::Bool -end - -struct LabelEntry <: SessionTreeEntry - type::String - id::String - parent_id::Union{String, Nothing} - timestamp::String - target_id::String - label::Union{String, Nothing} -end - -struct SessionInfoEntry <: SessionTreeEntry - type::String - id::String - parent_id::Union{String, Nothing} - timestamp::String - name::Union{String, Nothing} -end - -struct LeafEntry <: SessionTreeEntry - type::String - id::String - parent_id::Union{String, Nothing} - timestamp::String - target_id::Union{String, Nothing} -end - -# ============================================================================ -# Session context -# ============================================================================ - -struct SessionContext - messages::Vector{AgentMessage} - thinking_level::String - model::Union{Dict{String, String}, Nothing} - active_tool_names::Union{Vector{String}, Nothing} -end - -# ============================================================================ -# Session stats -# ============================================================================ - -struct SessionStats - message_count::Int64 - cached_tokens::Int64 - uncached_tokens::Int64 - total_tokens::Int64 - cost_total::Float64 -end - -# ============================================================================ -# Session metadata -# ============================================================================ - -mutable struct SessionMetadata - id::String - created_at::String -end - -mutable struct JsonlSessionMetadata <: SessionMetadata - id::String - created_at::String - cwd::String - path::String - parent_session_path::Union{String, Nothing} - metadata::Union{Dict{String, Any}, Nothing} -end - -# ============================================================================ -# Session storage interface -# ============================================================================ - -abstract type SessionStorage{T<:SessionMetadata} end - -function getMetadata(storage::SessionStorage)::Promise{T} - return Promise() -end - -function getLeafId(storage::SessionStorage)::Promise{Union{String, Nothing}} - return Promise() -end - -function setLeafId(storage::SessionStorage, leaf_id::String)::Promise{Nothing} - return Promise() -end - -function createEntryId(storage::SessionStorage)::Promise{String} - return Promise() -end - -function appendEntry(storage::SessionStorage, entry::SessionTreeEntry)::Promise{Nothing} - return Promise() -end - -function getEntry(storage::SessionStorage, id::String)::Promise{Union{SessionTreeEntry, Nothing}} - return Promise() -end - -function findEntries(storage::SessionStorage, type::String)::Promise{Vector{SessionTreeEntry}} - return Promise() -end - -function getLabel(storage::SessionStorage, id::String)::Promise{Union{String, Nothing}} - return Promise() -end - -function getSessionName(storage::SessionStorage)::Promise{Union{String, Nothing}} - return Promise() -end - -function getSessionStats(storage::SessionStorage)::Promise{SessionStats} - return Promise() -end - -function getPathToRootOrCompaction(storage::SessionStorage, leaf_id::String)::Promise{Vector{SessionTreeEntry}} - return Promise() -end - -function getEntries(storage::SessionStorage, options::Dict{String, Any})::Promise{Vector{SessionTreeEntry}} - return Promise() -end - -# ============================================================================ -# Session repo interface -# ============================================================================ - -abstract type SessionRepo< - TMetadata<:SessionMetadata, - TCreateOptions, - TListOptions -> end - -function create(repo::SessionRepo, options::TCreateOptions)::Promise{Session} - return Promise() -end - -function open(repo::SessionRepo, metadata::TMetadata)::Promise{Session} - return Promise() -end - -function list(repo::SessionRepo, options::TListOptions)::Promise{Vector{TMetadata}} - return Promise() -end - -function delete(repo::SessionRepo, metadata::TMetadata)::Promise{Nothing} - return Promise() -end - -function fork(repo::SessionRepo, source::TMetadata, options::Dict{String, Any})::Promise{Session} - return Promise() -end - -# ============================================================================ -# Pending session write -# ============================================================================ - -mutable struct PendingSessionWrite - type::String - message::Union{AgentMessage, Nothing} - provider::Union{String, Nothing} - model_id::Union{String, Nothing} - thinking_level::Union{String, Nothing} - active_tool_names::Union{Vector{String}, Nothing} - custom_type::Union{String, Nothing} - data::Union{Any, Nothing} - content::Union{String, Nothing} - display::Union{Bool, Nothing} - target_id::Union{String, Nothing} - label::Union{String, Nothing} - name::Union{String, Nothing} -end - -# ============================================================================ -# Queue update event -# ============================================================================ - -mutable struct QueueUpdateEvent - type::String - steer::Vector{AgentMessage} - followUp::Vector{AgentMessage} - nextTurn::Vector{AgentMessage} -end - -# ============================================================================ -# Save point event -# ============================================================================ - -mutable struct SavePointEvent - type::String - had_pending_mutations::Bool -end - -# ============================================================================ -# Abort event -# ============================================================================ - -mutable struct AbortEvent - type::String - cleared_steer::Vector{AgentMessage} - cleared_follow_up::Vector{AgentMessage} -end - -# ============================================================================ -# Settled event -# ============================================================================ - -mutable struct SettledEvent - type::String - next_turn_count::Int64 -end - -# ============================================================================ -# Before agent start event -# ============================================================================ - -mutable struct BeforeAgentStartEvent{TSkill<:Skill, TPromptTemplate<:PromptTemplate} - type::String - prompt::String - images::Union{Vector{ImageContent}, Nothing} - system_prompt::String - resources::AgentHarnessResources{TSkill, TPromptTemplate} -end - -# ============================================================================ -# Context event -# ============================================================================ - -mutable struct ContextEvent - type::String - messages::Vector{AgentMessage} -end - -# ============================================================================ -# Before provider request event -# ============================================================================ - -mutable struct BeforeProviderRequestEvent - type::String - model::Model - session_id::String - stream_options::AgentHarnessStreamOptions -end - -# ============================================================================ -# Before provider payload event -# ============================================================================ - -mutable struct BeforeProviderPayloadEvent - type::String - model::Model - payload::Any -end - -# ============================================================================ -# After provider response event -# ============================================================================ - -mutable struct AfterProviderResponseEvent - type::String - status::Int64 - headers::Dict{String, String} -end - -# ============================================================================ -# Tool call event -# ============================================================================ - -mutable struct ToolCallEvent - type::String - tool_call_id::String - tool_name::String - input::Dict{String, Any} -end - -# ============================================================================ -# Tool result event -# ============================================================================ - -mutable struct ToolResultEvent - type::String - tool_call_id::String - tool_name::String - input::Dict{String, Any} - content::Vector{MessageContent} - details::Any - is_error::Bool - usage::Union{Usage, Nothing} -end - -# ============================================================================ -# Session before compact event -# ============================================================================ - -mutable struct SessionBeforeCompactEvent - type::String - preparation::Any - branch_entries::Vector{SessionTreeEntry} - custom_instructions::Union{String, Nothing} - signal::Any -end - -# ============================================================================ -# Session compact event -# ============================================================================ - -mutable struct SessionCompactEvent - type::String - compaction_entry::CompactionEntry - from_hook::Bool -end - -# ============================================================================ -# Session before tree event -# ============================================================================ - -mutable struct SessionBeforeTreeEvent - type::String - preparation::Any - signal::Any -end - -# ============================================================================ -# Session tree event -# ============================================================================ - -mutable struct SessionTreeEvent - type::String - new_leaf_id::Union{String, Nothing} - old_leaf_id::Union{String, Nothing} - summary_entry::Union{BranchSummaryEntry, Nothing} - from_hook::Union{Bool, Nothing} -end - -# ============================================================================ -# Retry scheduled event -# ============================================================================ - -mutable struct RetryScheduledEvent - type::String - operation::String - attempt::Int64 - max_attempts::Int64 - delay_ms::Int64 - error_message::String -end - -# ============================================================================ -# Retry attempt start event -# ============================================================================ - -mutable struct RetryAttemptStartEvent - type::String - operation::String -end - -# ============================================================================ -# Retry finished event -# ============================================================================ - -mutable struct RetryFinishedEvent - type::String - operation::String -end - -# ============================================================================ -# Model update event -# ============================================================================ - -mutable struct ModelUpdateEvent - type::String - model::Model - previous_model::Union{Model, Nothing} - source::String -end - -# ============================================================================ -# Thinking level update event -# ============================================================================ - -mutable struct ThinkingLevelUpdateEvent - type::String - level::ThinkingLevel - previous_level::ThinkingLevel -end - -# ============================================================================ -# Tools update event -# ============================================================================ - -mutable struct ToolsUpdateEvent - type::String - tool_names::Vector{String} - previous_tool_names::Vector{String} - active_tool_names::Vector{String} - previous_active_tool_names::Vector{String} - source::String -end - -# ============================================================================ -# Resources update event -# ============================================================================ - -mutable struct ResourcesUpdateEvent{TSkill<:Skill, TPromptTemplate<:PromptTemplate} - type::String - resources::AgentHarnessResources{TSkill, TPromptTemplate} - previous_resources::AgentHarnessResources{TSkill, TPromptTemplate} -end - -# ============================================================================ -# Agent harness own events -# ============================================================================ - -abstract type AgentHarnessOwnEvent{TSkill<:Skill, TPromptTemplate<:PromptTemplate} end - -# ============================================================================ -# Agent harness event -# ============================================================================ - -abstract type AgentHarnessEvent{TSkill<:Skill, TPromptTemplate<:PromptTemplate} <: AgentEvent, AgentHarnessOwnEvent{TSkill, TPromptTemplate} end - -# ============================================================================ -# Before agent start result -# ============================================================================ - -mutable struct BeforeAgentStartResult - messages::Union{Vector{AgentMessage}, Nothing} - system_prompt::Union{String, Nothing} -end - -# ============================================================================ -# Context result -# ============================================================================ - -mutable struct ContextResult - messages::Vector{AgentMessage} -end - -# ============================================================================ -# Before provider request result -# ============================================================================ - -mutable struct BeforeProviderRequestResult - stream_options::Union{AgentHarnessStreamOptionsPatch, Nothing} -end - -# ============================================================================ -# Before provider payload result -# ============================================================================ - -mutable struct BeforeProviderPayloadResult - payload::Any -end - -# ============================================================================ -# Tool call result -# ============================================================================ - -mutable struct ToolCallResult - block::Union{Bool, Nothing} - reason::Union{String, Nothing} -end - -# ============================================================================ -# Tool result patch -# ============================================================================ - -mutable struct ToolResultPatch - content::Union{Vector{MessageContent}, Nothing} - details::Union{Any, Nothing} - is_error::Union{Bool, Nothing} - usage::Union{Usage, Nothing} - terminate::Union{Bool, Nothing} -end - -# ============================================================================ -# Session before compact result -# ============================================================================ - -mutable struct SessionBeforeCompactResult - cancel::Union{Bool, Nothing} - compaction::Union{CompactResult, Nothing} -end - -# ============================================================================ -# Session before tree result -# ============================================================================ - -mutable struct SessionBeforeTreeResult - cancel::Union{Bool, Nothing} - summary::Union{Dict{String, Any}, Nothing} - custom_instructions::Union{String, Nothing} - replace_instructions::Union{Bool, Nothing} - label::Union{String, Nothing} -end - -# ============================================================================ -# Agent harness event result map -# ============================================================================ - -# ============================================================================ -# Agent harness prompt options -# ============================================================================ - -mutable struct AgentHarnessPromptOptions - images::Union{Vector{ImageContent}, Nothing} -end - -# ============================================================================ -# Abort result -# ============================================================================ - -mutable struct AbortResult - cleared_steer::Vector{AgentMessage} - cleared_follow_up::Vector{AgentMessage} -end - -# ============================================================================ -# Compact result -# ============================================================================ - -mutable struct CompactResult - summary::String - first_kept_entry_id::Union{String, Nothing} - tokens_before::Int64 - usage::Union{Usage, Nothing} - retained_tail::Union{Vector{AgentMessage}, Nothing} - details::Union{Any, Nothing} -end - -# ============================================================================ -# Navigate tree result -# ============================================================================ - -mutable struct NavigateTreeResult - cancelled::Bool - editor_text::Union{String, Nothing} - summary_entry::Union{BranchSummaryEntry, Nothing} -end - -# ============================================================================ -# Compaction settings -# ============================================================================ - -mutable struct CompactionSettings - enabled::Bool - reserve_tokens::Int64 - keep_recent_tokens::Int64 -end - -const DEFAULT_COMPACTION_SETTINGS = CompactionSettings(true, 16384, 20000) - -# ============================================================================ -# Compaction preparation -# ============================================================================ - -mutable struct CompactionPreparation - first_kept_entry_id::String - messages_to_summarize::Vector{AgentMessage} - turn_prefix_messages::Vector{AgentMessage} - retained_tail::Vector{AgentMessage} - is_split_turn::Bool - tokens_before::Int64 - previous_summary::Union{String, Nothing} - file_ops::Any - settings::CompactionSettings -end - -# ============================================================================ -# File operations -# ============================================================================ - -mutable struct FileOperations - read::Set{String} - written::Set{String} - edited::Set{String} -end - -# ============================================================================ -# Tree preparation -# ============================================================================ - -mutable struct TreePreparation - target_id::String - old_leaf_id::Union{String, Nothing} - common_ancestor_id::Union{String, Nothing} - entries_to_summarize::Vector{SessionTreeEntry} - user_wants_summary::Bool - custom_instructions::Union{String, Nothing} - replace_instructions::Union{Bool, Nothing} - label::Union{String, Nothing} -end - -# ============================================================================ -# Generate branch summary options -# ============================================================================ - -mutable struct GenerateBranchSummaryOptions - model::Model - api_key::String - headers::Union{Dict{String, String}, Nothing} - signal::Any - custom_instructions::Union{String, Nothing} - replace_instructions::Union{Bool, Nothing} - reserve_tokens::Int64 -end - -# ============================================================================ -# Branch summary result -# ============================================================================ - -mutable struct BranchSummaryResult - summary::String - usage::Union{Usage, Nothing} - read_files::Vector{String} - modified_files::Vector{String} -end - -# ============================================================================ -# Agent harness system prompt -# ============================================================================ - -mutable struct AgentHarnessSystemPrompt{TC<:Any, TSkill<:Skill, TPromptTemplate<:PromptTemplate, TTool<:AgentHarnessTool} - value::Union{String, Function} -end - -# ============================================================================ -# Agent harness options -# ============================================================================ - -mutable struct AgentHarnessOptions{TC<:Any, TSkill<:Skill, TPromptTemplate<:PromptTemplate, TTool<:AgentHarnessTool} - session::Session - models::Any - tools::Union{Vector{TTool}, Nothing} - resources::Union{AgentHarnessResources{TSkill, TPromptTemplate}, Nothing} - system_prompt::Union{AgentHarnessSystemPrompt{TC, TSkill, TPromptTemplate, TTool}, Nothing} - stream_options::Union{AgentHarnessStreamOptions, Nothing} - retry::Union{Any, Nothing} - model::Model - thinking_level::Union{ThinkingLevel, Nothing} - active_tool_names::Union{Vector{String}, Nothing} - steering_mode::Union{QueueMode, Nothing} - follow_up_mode::Union{QueueMode, Nothing} - tool_context::Union{AgentHarnessToolContextSource{TC}, Nothing} -end - -end diff --git a/packages/agent/julia_implementation/src/messages.jl b/packages/agent/julia_implementation/src/messages.jl deleted file mode 100644 index bf29b07f..00000000 --- a/packages/agent/julia_implementation/src/messages.jl +++ /dev/null @@ -1,183 +0,0 @@ -""" - messages.jl - Custom message types and LLM conversion - -This module provides custom message types and the convertToLlm function. -""" - -module Messages - -using ..Types: * - -const COMPACTION_SUMMARY_PREFIX = """The conversation history before this point was compacted into the following summary: - - -""" - -const COMPACTION_SUMMARY_SUFFIX = """ -""" - -const BRANCH_SUMMARY_PREFIX = """The following is a summary of a branch that this conversation came back from: - - -""" - -const BRANCH_SUMMARY_SUFFIX = """""" - -# ============================================================================ -# Custom message types -# ============================================================================ - -mutable struct BashExecutionMessage - role::String - command::String - output::String - exit_code::Union{Int64, Nothing} - cancelled::Bool - truncated::Bool - full_output_path::Union{String, Nothing} - timestamp::Timestamp - exclude_from_context::Bool -end - -mutable struct CustomMessage{T} - role::String - custom_type::String - content::Union{String, Vector{MessageContent}} - display::Bool - details::Union{T, Nothing} - timestamp::Timestamp -end - -mutable struct BranchSummaryMessage - role::String - summary::String - from_id::String - timestamp::Timestamp -end - -mutable struct CompactionSummaryMessage - role::String - summary::String - tokens_before::Int64 - timestamp::Timestamp -end - -# ============================================================================ -# Bash execution to text conversion -# ============================================================================ - -function bashExecutionToText(msg::BashExecutionMessage)::String - text = "Ran `$(msg.command)`\n" - if !isempty(msg.output) - text *= "```\n$(msg.output)\n```" - else - text *= "(no output)" - end - if msg.cancelled - text *= "\n\n(command cancelled)" - elseif !isnothing(msg.exit_code) && msg.exit_code != 0 - text *= "\n\nCommand exited with code $(msg.exit_code)" - end - if msg.truncated && !isnothing(msg.full_output_path) - text *= "\n\n[Output truncated. Full output: $(msg.full_output_path)]" - end - return text -end - -# ============================================================================ -# Message creation functions -# ============================================================================ - -function createBranchSummaryMessage(summary::String, from_id::String, timestamp::String)::BranchSummaryMessage - return BranchSummaryMessage( - "branchSummary", - summary, - from_id, - Int64(Dates.now(Dates.UTC).datetime), - ) -end - -function createCompactionSummaryMessage(summary::String, tokens_before::Int64, timestamp::String)::CompactionSummaryMessage - return CompactionSummaryMessage( - "compactionSummary", - summary, - tokens_before, - Int64(Dates.now(Dates.UTC).datetime), - ) -end - -function createCustomMessage(custom_type::String, content::Union{String, Vector{MessageContent}}, display::Bool, details::Union{Any, Nothing}, timestamp::String)::CustomMessage - return CustomMessage( - "custom", - custom_type, - content, - display, - details, - Int64(Dates.now(Dates.UTC).datetime), - ) -end - -# ============================================================================ -# Convert to LLM messages -# ============================================================================ - -function convertToLlm(messages::Vector{AgentMessage})::Vector{Message} - result::Vector{Message} = Message[] - - for m in messages - converted = convertToLlmMessage(m) - if !isnothing(converted) - push!(result, converted) - end - end - - return result -end - -function convertToLlmMessage(m::BashExecutionMessage)::Union{UserMessage, Nothing} - if m.exclude_from_context - return nothing - end - return UserMessage( - "user", - [TextContent(bashExecutionToText(m))], - m.timestamp, - ) -end - -function convertToLlmMessage(m::CustomMessage)::Union{UserMessage, Nothing} - content = if m.content isa String - [TextContent(m.content)] - else - m.content - end - return UserMessage("user", content, m.timestamp) -end - -function convertToLlmMessage(m::BranchSummaryMessage)::UserMessage - text = BRANCH_SUMMARY_PREFIX * m.summary * BRANCH_SUMMARY_SUFFIX - return UserMessage("user", [TextContent(text)], m.timestamp) -end - -function convertToLlmMessage(m::CompactionSummaryMessage)::UserMessage - text = COMPACTION_SUMMARY_PREFIX * m.summary * COMPACTION_SUMMARY_SUFFIX - return UserMessage("user", [TextContent(text)], m.timestamp) -end - -function convertToLlmMessage(m::UserMessage)::UserMessage - return m -end - -function convertToLlmMessage(m::AssistantMessage)::AssistantMessage - return m -end - -function convertToLlmMessage(m::ToolResultMessage)::ToolResultMessage - return m -end - -function convertToLlmMessage(m::AgentMessage)::Union{Message, Nothing} - return nothing -end - -end diff --git a/packages/agent/julia_implementation/src/prompt_templates.jl b/packages/agent/julia_implementation/src/prompt_templates.jl deleted file mode 100644 index 1defbdd9..00000000 --- a/packages/agent/julia_implementation/src/prompt_templates.jl +++ /dev/null @@ -1,335 +0,0 @@ -""" - prompt_templates.jl - Prompt template loading and formatting - -This module provides utilities for loading prompt templates and formatting invocations. -""" - -module PromptTemplates - -using ..Types: * -using ..HarnessTypes: ExecutionEnv, toError, Result, ok, err - -# ============================================================================ -# Prompt template diagnostic types -# ============================================================================ - -const PromptTemplateDiagnosticCode = String -const PROMPT_TEMPLATE_DIAGNOSTIC_FILE_INFO_FAILED = "file_info_failed" -const PROMPT_TEMPLATE_DIAGNOSTIC_LIST_FAILED = "list_failed" -const PROMPT_TEMPLATE_DIAGNOSTIC_READ_FAILED = "read_failed" -const PROMPT_TEMPLATE_DIAGNOSTIC_PARSE_FAILED = "parse_failed" - -mutable struct PromptTemplateDiagnostic - type::String - code::PromptTemplateDiagnosticCode - message::String - path::String -end - -# ============================================================================ -# Prompt template frontmatter -# ============================================================================ - -mutable struct PromptTemplateFrontmatter - description::Union{String, Nothing} - argument_hint::Union{String, Nothing} - extra::Dict{String, Any} -end - -# ============================================================================ -# Load prompt templates from paths -# ============================================================================ - -function loadPromptTemplates( - env::ExecutionEnv, - paths::Union{String, Vector{String}}, -)::Tuple{Vector{PromptTemplate}, Vector{PromptTemplateDiagnostic}} - prompt_templates::Vector{PromptTemplate} = PromptTemplate[] - diagnostics::Vector{PromptTemplateDiagnostic} = PromptTemplateDiagnostic[] - - path_list = if paths isa String - [paths] - else - paths - end - - for path in path_list - info_result = fileInfo(env, path, nothing) - if !info_result.ok - if info_result.error.code != "not_found" - push!(diagnostics, PromptTemplateDiagnostic( - "warning", - "file_info_failed", - info_result.error.message, - path, - )) - end - continue - end - - info = info_result.value - kind = getFileKind(env, info, diagnostics) - - if kind == "directory" - result = loadTemplatesFromDir(env, info.path) - append!(prompt_templates, result.prompt_templates) - append!(diagnostics, result.diagnostics) - elseif kind == "file" && endswith(info.name, ".md") - result = loadTemplateFromFile(env, info.path) - if !isnothing(result.prompt_template) - push!(prompt_templates, result.prompt_template) - end - append!(diagnostics, result.diagnostics) - end - end - - return prompt_templates, diagnostics -end - -function getFileKind(env::ExecutionEnv, info::FileInfo, diagnostics::Vector{PromptTemplateDiagnostic})::Union{String, Nothing} - if info.kind == "file" || info.kind == "directory" - return info.kind - end - - canonical_path = canonicalPath(env, info.path, nothing) - if !canonical_path.ok - if canonical_path.error.code != "not_found" - push!(diagnostics, PromptTemplateDiagnostic( - "warning", - "file_info_failed", - canonical_path.error.message, - info.path, - )) - end - return nothing - end - - target = fileInfo(env, canonical_path.value, nothing) - if !target.ok - if target.error.code != "not_found" - push!(diagnostics, PromptTemplateDiagnostic( - "warning", - "file_info_failed", - target.error.message, - info.path, - )) - end - return nothing - end - - if target.value.kind == "file" || target.value.kind == "directory" - return target.value.kind - end - - return nothing -end - -# ============================================================================ -# Load templates from directory -# ============================================================================ - -function loadTemplatesFromDir( - env::ExecutionEnv, - dir::String, -)::Tuple{Vector{PromptTemplate}, Vector{PromptTemplateDiagnostic}} - prompt_templates::Vector{PromptTemplate} = PromptTemplate[] - diagnostics::Vector{PromptTemplateDiagnostic} = PromptTemplateDiagnostic[] - - entries_result = listDir(env, dir, nothing) - if !entries_result.ok - push!(diagnostics, PromptTemplateDiagnostic( - "warning", - "list_failed", - entries_result.error.message, - dir, - )) - return prompt_templates, diagnostics - end - - entries = entries_result.value - - for entry in sort(entries, by=e -> e.name) - kind = getFileKind(env, entry, diagnostics) - if kind != "file" || !endswith(entry.name, ".md") - continue - end - - result = loadTemplateFromFile(env, entry.path) - if !isnothing(result.prompt_template) - push!(prompt_templates, result.prompt_template) - end - append!(diagnostics, result.diagnostics) - end - - return prompt_templates, diagnostics -end - -# ============================================================================ -# Load template from file -# ============================================================================ - -function loadTemplateFromFile( - env::ExecutionEnv, - file_path::String, -)::Tuple{Union{PromptTemplate, Nothing}, Vector{PromptTemplateDiagnostic}} - diagnostics::Vector{PromptTemplateDiagnostic} = PromptTemplateDiagnostic[] - - raw_content = readTextFile(env, file_path, nothing) - if !raw_content.ok - push!(diagnostics, PromptTemplateDiagnostic( - "warning", - "read_failed", - raw_content.error.message, - file_path, - )) - return nothing, diagnostics - end - - # TODO: Parse frontmatter - # parsed = parseFrontmatter(rawContent.value); - # if !parsed.ok { - # diagnostics.push({ - # type: "warning", - # code: "parse_failed", - # message: parsed.error.message, - # path: filePath, - # }); - # return { promptTemplate: null, diagnostics }; - # } - - # const { frontmatter, body } = parsed.value; - # const firstLine = body.split("\n").find((line) => line.trim()); - # let description = typeof frontmatter.description === "string" ? frontmatter.description : ""; - # if (!description && firstLine) { - # description = firstLine.slice(0, 60); - # if (firstLine.length > 60) description += "..."; - # } - - # return { - # promptTemplate: { - # name: basenameEnvPath(filePath).replace(/\.md$/i, ""), - # description, - # content: body, - # }, - # diagnostics, - # }; - - return nothing, diagnostics -end - -# ============================================================================ -# Parse command arguments -# ============================================================================ - -function parseCommandArgs(args_string::String)::Vector{String} - args::Vector{String} = String[] - current::String = "" - in_quote::Union{String, Nothing} = nothing - - for i in 1:length(args_string) - char = args_string[i] - if !isnothing(in_quote) - if char == in_quote - in_quote = nothing - else - current *= char - end - elseif char == '"' || char == '\'' - in_quote = char - elseif char == ' ' || char == '\t' - if !isempty(current) - push!(args, current) - current = "" - end - else - current *= char - end - end - - if !isempty(current) - push!(args, current) - end - - return args -end - -# ============================================================================ -# Substitute arguments -# ============================================================================ - -function substituteArgs(content::String, args::Vector{String})::String - result = content - - # Substitute $1, $2, etc. - result = replace(result, r"\$(\d+)" => s -> begin - idx = parse(Int, s[1]) - if idx > 0 && idx <= length(args) - return args[idx] - end - return "" - end) - - # Substitute ${@:N} and ${@:N:L} - result = replace(result, r"\$\{@:(\d+)(?::(\d+))?\}" => s -> begin - m = match(r"\$\{@:(\d+)(?::(\d+))?\}", s) - if !isnothing(m) - start = parse(Int, m.captures[1]) - 1 - if start < 0 - start = 0 - end - if !isnothing(m.captures[2]) - length = parse(Int, m.captures[2]) - return join(args[start+1:start+length], " ") - end - return join(args[start+1:end], " ") - end - return s - end) - - # Substitute $ARGUMENTS and $@ - all_args = join(args, " ") - result = replace(result, "$ARGUMENTS" => all_args) - result = replace(result, "$@" => all_args) - - return result -end - -# ============================================================================ -# Format prompt template invocation -# ============================================================================ - -function formatPromptTemplateInvocation(template::PromptTemplate, args::Vector{String}=String[])::String - return substituteArgs(template.content, args) -end - -# ============================================================================ -# Helper functions -# ============================================================================ - -function basenameEnvPath(path::String)::String - normalized = rtrim(path, '/') - slash_index = findlast('/', normalized) - if isnothing(slash_index) - return normalized - end - return normalized[slash_index+1:end] -end - -function findlast(pattern::Char, s::String)::Union{Int64, Nothing} - for i in length(s):-1:1 - if s[i] == pattern - return i - end - end - return nothing -end - -function rtrim(s::String, chars::String)::String - idx = length(s) - while idx >= 1 && s[idx] in chars - idx -= 1 - end - return s[1:idx] -end - -end diff --git a/packages/agent/julia_implementation/src/session/jsonl_repo.jl b/packages/agent/julia_implementation/src/session/jsonl_repo.jl deleted file mode 100644 index 1cc8a0d6..00000000 --- a/packages/agent/julia_implementation/src/session/jsonl_repo.jl +++ /dev/null @@ -1,148 +0,0 @@ -""" - session/jsonl_repo.jl - JSONL session repository - -This module provides a JSONL-based session repository implementation. -""" - -module JsonlRepo - -using ..Types: * -using ..SessionStorage: SessionStorage, SessionMetadata -using ..JsonlStorage: JsonlSessionStorage, headerToSessionMetadata -using ..MemoryRepo: createSessionId, createTimestamp, getEntriesToFork, toSession -using ..HarnessTypes: SessionRepo, SessionForkOptions - -# ============================================================================ -# JSONL session repository -# ============================================================================ - -mutable struct JsonlSessionRepo <: SessionRepo{ - JsonlSessionMetadata, - JsonlSessionCreateOptions, - JsonlSessionListOptions -} - fs::Any - sessions_root_input::String - sessions_root::Union{String, Nothing} - - function JsonlSessionRepo(; sessions_root::String, fs::Any) - new(fs, sessions_root, nothing) - end -end - -# ============================================================================ -# Session repo methods -# ============================================================================ - -function create(repo::JsonlSessionRepo, options::JsonlSessionCreateOptions)::Session - id = if haskey(options, :id) && !isnothing(options[:id]) - options[:id] - else - createSessionId() - end - created_at = createTimestamp() - - session_dir = getSessionDir(repo, options.cwd) - - file_path = createSessionFilePath(repo, options.cwd, id, created_at) - - storage = JsonlSessionStorage( - file_path, - SessionHeader( - "session", - 3, - id, - created_at, - options.cwd, - get(options, :parentSessionPath, nothing), - get(options, :metadata, nothing), - ), - SessionTreeEntry[], - nothing, - ) - - return toSession(storage) -end - -function open(repo::JsonlSessionRepo, metadata::JsonlSessionMetadata)::Session - # TODO: Open existing file - return toSession(JsonlSessionStorage( - metadata.path, - SessionHeader( - "session", - 3, - metadata.id, - metadata.created_at, - metadata.cwd, - metadata.parent_session_path, - metadata.metadata, - ), - SessionTreeEntry[], - nothing, - )) -end - -function list(repo::JsonlSessionRepo, options::JsonlSessionListOptions=JsonlSessionListOptions())::Vector{JsonlSessionMetadata} - # TODO: List sessions - return JsonlSessionMetadata[] -end - -function delete(repo::JsonlSessionRepo, metadata::JsonlSessionMetadata)::Nothing - # TODO: Delete session file - return nothing -end - -function fork(repo::JsonlSessionRepo, source::JsonlSessionMetadata, options::Dict{String, Any})::Session - # TODO: Fork session - return create(repo, JsonlSessionCreateOptions( - cwd=get(options, "cwd", ""), - id=get(options, "id", createSessionId()), - )) -end - -# ============================================================================ -# Helper functions -# ============================================================================ - -function getSessionsRoot(repo::JsonlSessionRepo)::String - if isnothing(repo.sessions_root) - repo.sessions_root = getFileSystemResultOrThrow( - absolutePath(repo.fs, repo.sessions_root_input), - "Failed to resolve sessions root $(repo.sessions_root_input)", - ) - end - return repo.sessions_root -end - -function getSessionDir(repo::JsonlSessionRepo, cwd::String)::String - return getFileSystemResultOrThrow( - joinPath(repo.fs, [getSessionsRoot(repo), encodeCwd(cwd)]), - "Failed to resolve session directory for $(cwd)", - ) -end - -function encodeCwd(cwd::String)::String - result = replace(cwd, r"^[/\\]" => "") - result = replace(result, r"[/\\:]" => "-") - return "--$(result)--" -end - -function createSessionFilePath(repo::JsonlSessionRepo, cwd::String, session_id::String, timestamp::String)::String - return getFileSystemResultOrThrow( - joinPath(repo.fs, [ - getSessionDir(repo, cwd), - "$(replace(timestamp, r"[:.]" => "-"))_$(session_id).jsonl", - ]), - "Failed to resolve session file path for $(session_id)", - ) -end - -function getFileSystemResultOrThrow(result::Result, message::String) - if !result.ok - code = result.error.code == "not_found" ? "not_found" : "storage" - throw(SessionError(code, "$(message): $(result.error.message)", result.error)) - end - return result.value -end - -end diff --git a/packages/agent/julia_implementation/src/session/jsonl_storage.jl b/packages/agent/julia_implementation/src/session/jsonl_storage.jl deleted file mode 100644 index 8c9b88a3..00000000 --- a/packages/agent/julia_implementation/src/session/jsonl_storage.jl +++ /dev/null @@ -1,290 +0,0 @@ -""" - session/jsonl_storage.jl - JSONL session storage - -This module provides JSONL-based session storage implementation. -""" - -module JsonlStorage - -using ..Types: * -using ..SessionStorage: SessionStorage, SessionMetadata - -# ============================================================================ -# Session header -# ============================================================================ - -mutable struct SessionHeader - type::String - version::Int64 - id::String - timestamp::String - cwd::String - parent_session::Union{String, Nothing} - metadata::Union{Dict{String, Any}, Nothing} -end - -# ============================================================================ -# JSONL session storage -# ============================================================================ - -mutable struct JsonlSessionStorage{T<:SessionMetadata} <: SessionStorage{T} - file_path::String - metadata::T - entries::Vector{SessionTreeEntry} - by_id::Dict{String, SessionTreeEntry} - labels_by_id::Dict{String, String} - current_leaf_id::Union{String, Nothing} - - function JsonlSessionStorage{T}( - file_path::String, - header::SessionHeader, - entries::Vector{SessionTreeEntry}, - leaf_id::Union{String, Nothing}, - ) where T - by_id = Dict{String, SessionTreeEntry}((e.id, e) for e in entries) - labels_by_id = Dict{String, String}() - - for entry in entries - if entry isa LabelEntry && !isnothing(entry.label) - labels_by_id[entry.target_id] = entry.label - end - end - - new( - file_path, - header, - entries, - by_id, - labels_by_id, - leaf_id, - ) - end -end - -# ============================================================================ -# Session storage methods -# ============================================================================ - -function getMetadata(storage::JsonlSessionStorage)::T - return storage.metadata -end - -function getLeafId(storage::JsonlSessionStorage)::Union{String, Nothing} - if !isnothing(storage.current_leaf_id) && !haskey(storage.by_id, storage.current_leaf_id) - throw(SessionError("invalid_session", "Entry $(storage.current_leaf_id) not found")) - end - return storage.current_leaf_id -end - -function setLeafId(storage::JsonlSessionStorage, leaf_id::Union{String, Nothing})::Nothing - if !isnothing(leaf_id) && !haskey(storage.by_id, leaf_id) - throw(SessionError("not_found", "Entry $(leaf_id) not found")) - end - - entry = LeafEntry( - "leaf", - generateEntryId(storage.by_id), - storage.current_leaf_id, - create_timestamp(), - leaf_id, - ) - - # TODO: Write to file - # getFileSystemResultOrThrow( - # await this.fs.appendFile(this.filePath, `${JSON.stringify(entry)}\n`), - # `Failed to append session leaf ${entry.id}`, - # ); - - push!(storage.entries, entry) - storage.by_id[entry.id] = entry - storage.current_leaf_id = leaf_id - return nothing -end - -function createEntryId(storage::JsonlSessionStorage)::String - return generateEntryId(storage.by_id) -end - -function appendEntry(storage::JsonlSessionStorage, entry::SessionTreeEntry)::Nothing - # TODO: Write to file - # getFileSystemResultOrThrow( - # await this.fs.appendFile(this.filePath, `${JSON.stringify(entry)}\n`), - # `Failed to append session entry ${entry.id}`, - # ); - - push!(storage.entries, entry) - storage.by_id[entry.id] = entry - - if entry isa LabelEntry - updateLabelCache(storage.labels_by_id, entry) - end - - storage.current_leaf_id = leafIdAfterEntry(entry) - return nothing -end - -function getEntry(storage::JsonlSessionStorage, id::String)::Union{SessionTreeEntry, Nothing} - return get(storage.by_id, id, nothing) -end - -function findEntries(storage::JsonlSessionStorage, type::String)::Vector{SessionTreeEntry} - return filter(entry -> entry.type == type, storage.entries) -end - -function getLabel(storage::JsonlSessionStorage, id::String)::Union{String, Nothing} - return get(storage.labels_by_id, id, nothing) -end - -function getSessionName(storage::JsonlSessionStorage)::Union{String, Nothing} - entries = findEntries(storage, "session_info") - if isempty(entries) - return nothing - end - return strip(entries[end].name) -end - -function getSessionStats(storage::JsonlSessionStorage)::SessionStats - message_count = 0 - cached_tokens = 0 - uncached_tokens = 0 - total_tokens = 0 - cost_total = 0.0 - - for entry in storage.entries - if entry isa MessageEntry - message_count += 1 - end - - usage = if entry isa MessageEntry && entry.message.role == "assistant" - entry.message.usage - elseif entry isa CompactionEntry || entry isa BranchSummaryEntry - entry.usage - else - nothing - end - - if !isnothing(usage) && - usage.input isa Int64 && - usage.output isa Int64 && - usage.cache_read isa Int64 && - usage.cache_write isa Int64 && - usage.cost.total isa Float64 - - cached_tokens += usage.cache_read - uncached_tokens += usage.input + usage.cache_write - total_tokens += usage.input + usage.output + usage.cache_read + usage.cache_write - cost_total += usage.cost.total - end - end - - return SessionStats( - message_count, - cached_tokens, - uncached_tokens, - total_tokens, - cost_total, - ) -end - -function getPathToRootOrCompaction(storage::JsonlSessionStorage, leaf_id::Union{String, Nothing})::Vector{SessionTreeEntry} - if isnothing(leaf_id) - return SessionTreeEntry[] - end - - path::Vector{SessionTreeEntry} = SessionTreeEntry[] - stop_at_entry_id::Union{String, Nothing} = nothing - current = get(storage.by_id, leaf_id, nothing) - - if isnothing(current) - throw(SessionError("not_found", "Entry $(leaf_id) not found")) - end - - while !isnothing(current) - unshift!(path, current) - - if !isnothing(stop_at_entry_id) && current.id == stop_at_entry_id - break - end - - if current isa CompactionEntry - if !isnothing(current.retained_tail) - break - end - stop_at_entry_id = current.first_kept_entry_id - end - - if isnothing(current.parent_id) - break - end - - parent = get(storage.by_id, current.parent_id, nothing) - if isnothing(parent) - throw(SessionError("invalid_session", "Entry $(current.parent_id) not found")) - end - - current = parent - end - - return path -end - -function getEntries(storage::JsonlSessionStorage, options::Dict{String, Any})::Vector{SessionTreeEntry} - start = get(options, "afterEntrySeq", 0) - end_idx = if haskey(options, "limit") - start + options["limit"] - else - nothing - end - - if isnothing(end_idx) - return copy(storage.entries[start+1:end]) - end - - return copy(storage.entries[start+1:end_idx]) -end - -# ============================================================================ -# Helper functions -# ============================================================================ - -function updateLabelCache(labels_by_id::Dict{String, String}, entry::SessionTreeEntry)::Nothing - if entry isa LabelEntry - label = strip(get(entry, :label, nothing)) - if !isnothing(label) && !isempty(label) - labels_by_id[entry.target_id] = label - else - delete!(labels_by_id, entry.target_id) - end - end - return nothing -end - -function generateEntryId(by_id::Dict{String, SessionTreeEntry})::String - for i in 1:100 - id = uuidv7()[end-7:end] - if !haskey(by_id, id) - return id - end - end - return uuidv7() -end - -function leafIdAfterEntry(entry::SessionTreeEntry)::Union{String, Nothing} - if entry isa LeafEntry - return entry.target_id - end - return entry.id -end - -function headerToSessionMetadata(header::SessionHeader, path::String)::JsonlSessionMetadata - return JsonlSessionMetadata( - header.id, - header.timestamp, - header.cwd, - path, - header.parent_session, - header.metadata, - ) -end - -end diff --git a/packages/agent/julia_implementation/src/session/memory_repo.jl b/packages/agent/julia_implementation/src/session/memory_repo.jl deleted file mode 100644 index d9012485..00000000 --- a/packages/agent/julia_implementation/src/session/memory_repo.jl +++ /dev/null @@ -1,133 +0,0 @@ -""" - session/memory_repo.jl - In-memory session repository - -This module provides an in-memory session repository implementation for testing. -""" - -module MemoryRepo - -using ..Types: * -using ..SessionStorage: SessionStorage, SessionMetadata -using ..MemoryStorage: InMemorySessionStorage - -# ============================================================================ -# In-memory session repository -# ============================================================================ - -mutable struct InMemorySessionRepo <: SessionRepo{SessionMetadata, Dict{String, Any}, Nothing} - sessions::Dict{String, Session} - - function InMemorySessionRepo() - new(Dict{String, Session}()) - end -end - -# ============================================================================ -# Session repo methods -# ============================================================================ - -function create(repo::InMemorySessionRepo, options::Dict{String, Any}=Dict{String, Any}())::Session - metadata = SessionMetadata( - if haskey(options, :id) && !isnothing(options[:id]) - options[:id] - else - createSessionId() - end, - createTimestamp(), - ) - - storage = InMemorySessionStorage{SessionMetadata}(metadata=metadata) - session = toSession(storage) - - repo.sessions[metadata.id] = session - - return session -end - -function open(repo::InMemorySessionRepo, metadata::SessionMetadata)::Session - session = get(repo.sessions, metadata.id, nothing) - if isnothing(session) - throw(SessionError("not_found", "Session not found: $(metadata.id)")) - end - return session -end - -function list(repo::InMemorySessionRepo)::Vector{SessionMetadata} - return [getMetadata(session) for session in values(repo.sessions)] -end - -function delete(repo::InMemorySessionRepo, metadata::SessionMetadata)::Nothing - delete!(repo.sessions, metadata.id) - return nothing -end - -function fork(repo::InMemorySessionRepo, source::SessionMetadata, options::Dict{String, Any})::Session - source_session = open(repo, source) - forked_entries = getEntriesToFork(getStorage(source_session), options) - - metadata = SessionMetadata( - if haskey(options, :id) && !isnothing(options[:id]) - options[:id] - else - createSessionId() - end, - createTimestamp(), - ) - - storage = InMemorySessionStorage{SessionMetadata}( - entries=forked_entries, - metadata=metadata, - ) - - session = toSession(storage) - repo.sessions[metadata.id] = session - - return session -end - -# ============================================================================ -# Helper functions -# ============================================================================ - -function createSessionId()::String - return uuidv7() -end - -function createTimestamp()::String - return create_timestamp() -end - -function toSession(storage::SessionStorage)::Session - return Session(storage) -end - -function getEntriesToFork(storage::SessionStorage, options::Dict{String, Any})::Vector{SessionTreeEntry} - if !haskey(options, :entryId) || isnothing(options[:entryId]) - return getEntries(storage, Dict{String, Any}()) - end - - target = getEntry(storage, options[:entryId]) - if isnothing(target) - throw(SessionError("invalid_fork_target", "Entry $(options[:entryId]) not found")) - end - - effective_leaf_id::Union{String, Nothing} - position = get(options, "position", "before") - - if position == "at" - effective_leaf_id = target.id - else - if target isa MessageEntry && target.message.role != "user" - throw(SessionError("invalid_fork_target", "Entry $(options[:entryId]) is not a user message")) - end - effective_leaf_id = target.parent_id - end - - return getPathToRootOrCompaction(storage, effective_leaf_id) -end - -function getStorage(session::Session)::SessionStorage - return session.storage -end - -end diff --git a/packages/agent/julia_implementation/src/session/memory_storage.jl b/packages/agent/julia_implementation/src/session/memory_storage.jl deleted file mode 100644 index 235efc1d..00000000 --- a/packages/agent/julia_implementation/src/session/memory_storage.jl +++ /dev/null @@ -1,227 +0,0 @@ -""" - session/memory_storage.jl - In-memory session storage - -This module provides an in-memory session storage implementation for testing and temporary use. -""" - -module MemoryStorage - -using ..Types: * -using ..SessionStorage: SessionStorage, SessionMetadata -using ..JsonlStorage: updateLabelCache, generateEntryId, leafIdAfterEntry - -# ============================================================================ -# In-memory session storage -# ============================================================================ - -mutable struct InMemorySessionStorage{T<:SessionMetadata} <: SessionStorage{T} - metadata::T - entries::Vector{SessionTreeEntry} - by_id::Dict{String, SessionTreeEntry} - labels_by_id::Dict{String, String} - leaf_id::Union{String, Nothing} - - function InMemorySessionStorage{T}(; - entries::Vector{SessionTreeEntry}=SessionTreeEntry[], - metadata::Union{T, Nothing]=nothing, - ) where T - by_id = Dict{String, SessionTreeEntry}((e.id, e) for e in entries) - labels_by_id = Dict{String, String}() - - leaf_id = nothing - for entry in entries - if entry isa LabelEntry - updateLabelCache(labels_by_id, entry) - end - leaf_id = leafIdAfterEntry(entry) - end - - if !isnothing(leaf_id) && !haskey(by_id, leaf_id) - throw(SessionError("invalid_session", "Entry $(leaf_id) not found")) - end - - new( - if isnothing(metadata) - T(uuidv7(), create_timestamp()) - else - metadata - end, - copy(entries), - by_id, - labels_by_id, - leaf_id, - ) - end -end - -# ============================================================================ -# Session storage methods -# ============================================================================ - -function getMetadata(storage::InMemorySessionStorage)::T - return storage.metadata -end - -function getLeafId(storage::InMemorySessionStorage)::Union{String, Nothing} - if !isnothing(storage.leaf_id) && !haskey(storage.by_id, storage.leaf_id) - throw(SessionError("invalid_session", "Entry $(storage.leaf_id) not found")) - end - return storage.leaf_id -end - -function setLeafId(storage::InMemorySessionStorage, leaf_id::Union{String, Nothing})::Nothing - if !isnothing(leaf_id) && !haskey(storage.by_id, leaf_id) - throw(SessionError("not_found", "Entry $(leaf_id) not found")) - end - - entry = LeafEntry( - "leaf", - generateEntryId(storage.by_id), - storage.leaf_id, - create_timestamp(), - leaf_id, - ) - - push!(storage.entries, entry) - storage.by_id[entry.id] = entry - storage.leaf_id = leaf_id - return nothing -end - -function createEntryId(storage::InMemorySessionStorage)::String - return generateEntryId(storage.by_id) -end - -function appendEntry(storage::InMemorySessionStorage, entry::SessionTreeEntry)::Nothing - push!(storage.entries, entry) - storage.by_id[entry.id] = entry - - if entry isa LabelEntry - updateLabelCache(storage.labels_by_id, entry) - end - - storage.leaf_id = leafIdAfterEntry(entry) - return nothing -end - -function getEntry(storage::InMemorySessionStorage, id::String)::Union{SessionTreeEntry, Nothing} - return get(storage.by_id, id, nothing) -end - -function findEntries(storage::InMemorySessionStorage, type::String)::Vector{SessionTreeEntry} - return filter(entry -> entry.type == type, storage.entries) -end - -function getLabel(storage::InMemorySessionStorage, id::String)::Union{String, Nothing} - return get(storage.labels_by_id, id, nothing) -end - -function getSessionName(storage::InMemorySessionStorage)::Union{String, Nothing} - entries = findEntries(storage, "session_info") - if isempty(entries) - return nothing - end - return strip(entries[end].name) -end - -function getSessionStats(storage::InMemorySessionStorage)::SessionStats - message_count = 0 - cached_tokens = 0 - uncached_tokens = 0 - total_tokens = 0 - cost_total = 0.0 - - for entry in storage.entries - if entry isa MessageEntry - message_count += 1 - end - - usage = if entry isa MessageEntry && entry.message.role == "assistant" - entry.message.usage - elseif entry isa CompactionEntry || entry isa BranchSummaryEntry - entry.usage - else - nothing - end - - if !isnothing(usage) && - usage.input isa Int64 && - usage.output isa Int64 && - usage.cache_read isa Int64 && - usage.cache_write isa Int64 && - usage.cost.total isa Float64 - - cached_tokens += usage.cache_read - uncached_tokens += usage.input + usage.cache_write - total_tokens += usage.input + usage.output + usage.cache_read + usage.cache_write - cost_total += usage.cost.total - end - end - - return SessionStats( - message_count, - cached_tokens, - uncached_tokens, - total_tokens, - cost_total, - ) -end - -function getPathToRootOrCompaction(storage::InMemorySessionStorage, leaf_id::Union{String, Nothing})::Vector{SessionTreeEntry} - if isnothing(leaf_id) - return SessionTreeEntry[] - end - - path::Vector{SessionTreeEntry} = SessionTreeEntry[] - stop_at_entry_id::Union{String, Nothing} = nothing - current = get(storage.by_id, leaf_id, nothing) - - if isnothing(current) - throw(SessionError("not_found", "Entry $(leaf_id) not found")) - end - - while !isnothing(current) - unshift!(path, current) - - if !isnothing(stop_at_entry_id) && current.id == stop_at_entry_id - break - end - - if current isa CompactionEntry - if !isnothing(current.retained_tail) - break - end - stop_at_entry_id = current.first_kept_entry_id - end - - if isnothing(current.parent_id) - break - end - - parent = get(storage.by_id, current.parent_id, nothing) - if isnothing(parent) - throw(SessionError("invalid_session", "Entry $(current.parent_id) not found")) - end - - current = parent - end - - return path -end - -function getEntries(storage::InMemorySessionStorage, options::Dict{String, Any})::Vector{SessionTreeEntry} - start = get(options, "afterEntrySeq", 0) - end_idx = if haskey(options, "limit") - start + options["limit"] - else - nothing - end - - if isnothing(end_idx) - return copy(storage.entries[start+1:end]) - end - - return copy(storage.entries[start+1:end_idx]) -end - -end diff --git a/packages/agent/julia_implementation/src/session/repo_utils.jl b/packages/agent/julia_implementation/src/session/repo_utils.jl deleted file mode 100644 index fb79916a..00000000 --- a/packages/agent/julia_implementation/src/session/repo_utils.jl +++ /dev/null @@ -1,65 +0,0 @@ -""" - session/repo_utils.jl - Session repository utilities - -This module provides shared utilities for session repository implementations. -""" - -module RepoUtils - -using ..Types: * -using ..SessionStorage: SessionStorage, SessionMetadata -using ..Session: Session - -# ============================================================================ -# Helper functions -# ============================================================================ - -function createSessionId()::String - return uuidv7() -end - -function createTimestamp()::String - return create_timestamp() -end - -function toSession{T<:SessionMetadata}(storage::SessionStorage{T})::Session{T} - return Session(storage) -end - -function getFileSystemResultOrThrow{TValue}(result::Result{TValue, FileError}, message::String)::TValue - if !result.ok - code = result.error.code == "not_found" ? "not_found" : "storage" - throw(SessionError(code, "$(message): $(result.error.message)", result.error)) - end - return result.value -end - -function getEntriesToFork( - storage::SessionStorage, - options::Dict{String, Any}, -)::Vector{SessionTreeEntry} - if !haskey(options, :entryId) || isnothing(options[:entryId]) - return getEntries(storage, Dict{String, Any}()) - end - - target = getEntry(storage, options[:entryId]) - if isnothing(target) - throw(SessionError("invalid_fork_target", "Entry $(options[:entryId]) not found")) - end - - effective_leaf_id::Union{String, Nothing} - position = get(options, "position", "before") - - if position == "at" - effective_leaf_id = target.id - else - if target isa MessageEntry && target.message.role != "user" - throw(SessionError("invalid_fork_target", "Entry $(options[:entryId]) is not a user message")) - end - effective_leaf_id = target.parent_id - end - - return getPathToRootOrCompaction(storage, effective_leaf_id) -end - -end diff --git a/packages/agent/julia_implementation/src/session/session.jl b/packages/agent/julia_implementation/src/session/session.jl deleted file mode 100644 index 72efce94..00000000 --- a/packages/agent/julia_implementation/src/session/session.jl +++ /dev/null @@ -1,422 +0,0 @@ -""" - session/session.jl - Session management - -This module provides the Session class for managing conversation history with branch support. -""" - -module Session - -using ..Types: * -using ..SessionStorage: SessionStorage -using ..Messages: * -using ..HarnessTypes: * - -# ============================================================================ -# Session context build options -# ============================================================================ - -mutable struct SessionContextBuildOptions - entry_transforms::Union{Vector{Function}, Nothing} - entry_projectors::Union{Dict{String, Function}, Nothing} -end - -# ============================================================================ -# Default context entry transform -# ============================================================================ - -function defaultContextEntryTransform(path_entries::Vector{SessionTreeEntry})::Vector{SessionTreeEntry} - compaction = nothing - for entry in path_entries - if entry isa CompactionEntry - compaction = entry - break - end - end - - if isnothing(compaction) - return copy(path_entries) - end - - entries::Vector{SessionTreeEntry} = [compaction] - compaction_idx = findfirst( - (entry) -> entry isa CompactionEntry && entry.id == compaction.id, - path_entries, - ) - - if !isnothing(compaction.retained_tail) - for i in compaction_idx+1:length(path_entries) - push!(entries, path_entries[i]) - end - return entries - end - - if !isnothing(compaction.first_kept_entry_id) - found_first_kept = false - for i in 1:compaction_idx-1 - entry = path_entries[i] - if entry.id == compaction.first_kept_entry_id - found_first_kept = true - end - if found_first_kept - push!(entries, entry) - end - end - end - - for i in compaction_idx+1:length(path_entries) - push!(entries, path_entries[i]) - end - - return entries -end - -# ============================================================================ -# Build context entries -# ============================================================================ - -function buildContextEntries( - path_entries::Vector{SessionTreeEntry}, - options::SessionContextBuildOptions=SessionContextBuildOptions(nothing, nothing), -)::Vector{SessionTreeEntry} - entries = defaultContextEntryTransform(path_entries) - - if !isnothing(options.entry_transforms) - for transform in options.entry_transforms - entries = transform(entries) - end - end - - return entries -end - -# ============================================================================ -# Session entry to context messages -# ============================================================================ - -function sessionEntryToContextMessages( - entry::SessionTreeEntry, - index::Int64, - entries::Vector{SessionTreeEntry}, - options::SessionContextBuildOptions=SessionContextBuildOptions(nothing, nothing), -)::Vector{AgentMessage} - if entry isa MessageEntry - return [entry.message] - end - - if entry isa CustomMessageEntry - return [createCustomMessage( - entry.custom_type, - entry.content, - entry.display, - entry.details, - entry.timestamp, - )] - end - - if entry isa CompactionEntry - messages = [createCompactionSummaryMessage( - entry.summary, - entry.tokens_before, - entry.timestamp, - )] - if !isnothing(entry.retained_tail) - append!(messages, entry.retained_tail) - end - return messages - end - - if entry isa BranchSummaryEntry - return [createBranchSummaryMessage( - entry.summary, - entry.from_id, - entry.timestamp, - )] - end - - if entry isa CustomEntry - if !isnothing(options.entry_projectors) && haskey(options.entry_projectors, entry.custom_type) - projector = options.entry_projectors[entry.custom_type] - return projector(entry, index, entries) - end - return AgentMessage[] - end - - return AgentMessage[] -end - -# ============================================================================ -# Build session context -# ============================================================================ - -function buildSessionContext( - path_entries::Vector{SessionTreeEntry}, - options::SessionContextBuildOptions=SessionContextBuildOptions(nothing, nothing), -)::SessionContext - state = deriveSessionContextState(path_entries) - context_entries = buildContextEntries(path_entries, options) - messages = SessionTreeEntry[] - for (i, entry) in enumerate(context_entries) - append!(messages, sessionEntryToContextMessages(entry, i, context_entries, options)) - end - return SessionContext(messages, state.thinking_level, state.model, state.active_tool_names) -end - -function deriveSessionContextState(path_entries::Vector{SessionTreeEntry})::Dict{String, Any} - thinking_level = "off" - model = nothing - active_tool_names = nothing - - for entry in path_entries - if entry isa ThinkingLevelChangeEntry - thinking_level = entry.thinking_level - elseif entry isa ModelChangeEntry - model = Dict{String, String}("provider" => entry.provider, "modelId" => entry.model_id) - elseif entry isa MessageEntry && entry.message.role == "assistant" - model = Dict{String, String}("provider" => entry.message.provider, "modelId" => entry.message.model) - elseif entry isa ActiveToolsChangeEntry - active_tool_names = copy(entry.active_tool_names) - end - end - - return Dict{String, Any}( - "thinking_level" => thinking_level, - "model" => model, - "active_tool_names" => active_tool_names, - ) -end - -# ============================================================================ -# Session class -# ============================================================================ - -mutable struct Session{T<:SessionMetadata} - storage::SessionStorage{T} - context_build_options::SessionContextBuildOptions - - function Session( - storage::SessionStorage, - context_build_options::SessionContextBuildOptions=SessionContextBuildOptions(nothing, nothing), - ) - new{typeof(storage.metadata)}(storage, context_build_options) - end -end - -# ============================================================================ -# Session methods -# ============================================================================ - -function getMetadata(session::Session)::T - return getMetadata(session.storage) -end - -function getStorage(session::Session)::SessionStorage - return session.storage -end - -function getLeafId(session::Session)::Union{String, Nothing} - return getLeafId(session.storage) -end - -function getEntry(session::Session, id::String)::Union{SessionTreeEntry, Nothing} - return getEntry(session.storage, id) -end - -function getEntries(session::Session, options::Dict{String, Any}=Dict{String, Any}())::Vector{SessionTreeEntry} - return getEntries(session.storage, options) -end - -function getBranch(session::Session, from_id::Union{String, Nothing}=nothing)::Vector{SessionTreeEntry} - leaf_id = if isnothing(from_id) - getLeafId(session.storage) - else - from_id - end - return getPathToRootOrCompaction(session.storage, leaf_id) -end - -function buildContextEntries(session::Session, options::SessionContextBuildOptions=SessionContextBuildOptions())::Vector{SessionTreeEntry} - return buildContextEntries(getBranch(session), mergeContextBuildOptions(session, options)) -end - -function buildContext(session::Session, options::SessionContextBuildOptions=SessionContextBuildOptions())::SessionContext - return buildSessionContext(getBranch(session), mergeContextBuildOptions(session, options)) -end - -function mergeContextBuildOptions(session::Session, options::SessionContextBuildOptions)::SessionContextBuildOptions - return SessionContextBuildOptions( - vcat( - isnothing(session.context_build_options.entry_transforms) ? [] : session.context_build_options.entry_transforms, - isnothing(options.entry_transforms) ? [] : options.entry_transforms, - ), - merge( - isnothing(session.context_build_options.entry_projectors) ? Dict{String, Any}() : session.context_build_options.entry_projectors, - isnothing(options.entry_projectors) ? Dict{String, Any}() : options.entry_projectors, - promote=true, - ), - ) -end - -function getLabel(session::Session, id::String)::Union{String, Nothing} - return getLabel(session.storage, id) -end - -function getSessionStats(session::Session)::SessionStats - return getSessionStats(session.storage) -end - -function getSessionName(session::Session)::Union{String, Nothing} - return getSessionName(session.storage) -end - -function appendMessage(session::Session, message::AgentMessage)::String - return appendTypedEntry(session, MessageEntry( - "message", - createEntryId(session.storage), - getLeafId(session.storage), - create_timestamp(), - message, - )) -end - -function appendThinkingLevelChange(session::Session, thinking_level::String)::String - return appendTypedEntry(session, ThinkingLevelChangeEntry( - "thinking_level_change", - createEntryId(session.storage), - getLeafId(session.storage), - create_timestamp(), - thinking_level, - )) -end - -function appendModelChange(session::Session, provider::String, model_id::String)::String - return appendTypedEntry(session, ModelChangeEntry( - "model_change", - createEntryId(session.storage), - getLeafId(session.storage), - create_timestamp(), - provider, - model_id, - )) -end - -function appendActiveToolsChange(session::Session, active_tool_names::Vector{String})::String - return appendTypedEntry(session, ActiveToolsChangeEntry( - "active_tools_change", - createEntryId(session.storage), - getLeafId(session.storage), - create_timestamp(), - active_tool_names, - )) -end - -function appendCompaction( - session::Session, - summary::String, - first_kept_entry_id::Union{String, Nothing}, - tokens_before::Int64, - details::Union{Any, Nothing}=nothing, - from_hook::Bool=false, - usage::Union{Usage, Nothing}=nothing, - retained_tail::Union{Vector{AgentMessage}, Nothing}=nothing, -)::String - return appendTypedEntry(session, CompactionEntry( - "compaction", - createEntryId(session.storage), - getLeafId(session.storage), - create_timestamp(), - summary, - first_kept_entry_id, - tokens_before, - retained_tail, - details, - usage, - from_hook, - )) -end - -function appendCustomEntry(session::Session, custom_type::String, data::Union{Any, Nothing}=nothing)::String - return appendTypedEntry(session, CustomEntry( - "custom", - createEntryId(session.storage), - getLeafId(session.storage), - create_timestamp(), - custom_type, - data, - )) -end - -function appendCustomMessageEntry( - session::Session, - custom_type::String, - content::String, - display::Bool, - details::Union{Any, Nothing}=nothing, -)::String - return appendTypedEntry(session, CustomMessageEntry( - "custom_message", - createEntryId(session.storage), - getLeafId(session.storage), - create_timestamp(), - custom_type, - content, - details, - display, - )) -end - -function appendLabel(session::Session, target_id::String, label::Union{String, Nothing})::String - if isnothing(getEntry(session, target_id)) - throw(SessionError("not_found", "Entry $(target_id) not found")) - end - return appendTypedEntry(session, LabelEntry( - "label", - createEntryId(session.storage), - getLeafId(session.storage), - create_timestamp(), - target_id, - label, - )) -end - -function appendSessionName(session::Session, name::String)::String - sanitizedName = replace(name, r"[\r\n]+" => " ") - return appendTypedEntry(session, SessionInfoEntry( - "session_info", - createEntryId(session.storage), - getLeafId(session.storage), - create_timestamp(), - sanitizedName, - )) -end - -function moveTo( - session::Session, - entry_id::Union{String, Nothing}, - summary::Union{Dict{String, Any}, Nothing}=nothing, -)::Union{String, Nothing - if !isnothing(entry_id) && isnothing(getEntry(session, entry_id)) - throw(SessionError("not_found", "Entry $(entry_id) not found")) - end - setLeafId(session.storage, entry_id) - if isnothing(summary) - return nothing - end - return appendTypedEntry(session, BranchSummaryEntry( - "branch_summary", - createEntryId(session.storage), - entry_id, - create_timestamp(), - entry_id, - summary["summary"], - get(summary, "details", nothing), - get(summary, "usage", nothing), - get(summary, "from_hook", false), - )) -end - -function appendTypedEntry(session::Session, entry::SessionTreeEntry)::String - appendEntry(session.storage, entry) - return entry.id -end - -end diff --git a/packages/agent/julia_implementation/src/skills.jl b/packages/agent/julia_implementation/src/skills.jl deleted file mode 100644 index ae83a7f9..00000000 --- a/packages/agent/julia_implementation/src/skills.jl +++ /dev/null @@ -1,375 +0,0 @@ -""" - skills.jl - Skill loading and formatting - -This module provides utilities for loading skills from SKILL.md files and formatting skill invocations. -""" - -module Skills - -using ..Types: * -using ..HarnessTypes: Skill, ExecutionEnv, FileSystem, toError, FileError, Result, ok, err - -const MAX_NAME_LENGTH = 64 -const MAX_DESCRIPTION_LENGTH = 1024 -const IGNORE_FILE_NAMES = [".gitignore", ".ignore", ".fdignore"] - -# ============================================================================ -# Skill diagnostic types -# ============================================================================ - -const SkillDiagnosticCode = String -const SKILL_DIAGNOSTIC_FILE_INFO_FAILED = "file_info_failed" -const SKILL_DIAGNOSTIC_LIST_FAILED = "list_failed" -const SKILL_DIAGNOSTIC_READ_FAILED = "read_failed" -const SKILL_DIAGNOSTIC_PARSE_FAILED = "parse_failed" -const SKILL_DIAGNOSTIC_INVALID_METADATA = "invalid_metadata" - -mutable struct SkillDiagnostic - type::String - code::SkillDiagnosticCode - message::String - path::String -end - -# ============================================================================ -# Skill frontmatter -# ============================================================================ - -mutable struct SkillFrontmatter - name::Union{String, Nothing} - description::Union{String, Nothing} - disable_model_invocation::Union{Bool, Nothing} - extra::Dict{String, Any} -end - -# ============================================================================ -# Format skill invocation -# ============================================================================ - -function formatSkillInvocation(skill::Skill, additional_instructions::Union{String, Nothing})::String - skill_block = "\nReferences are relative to $(dirnameEnvPath(skill.filePath)).\n\n$(skill.content)\n" - if isnothing(additional_instructions) - return skill_block - end - return "$(skill_block)\n\n$(additional_instructions)" -end - -# ============================================================================ -# Load skills from directories -# ============================================================================ - -function loadSkills(env::ExecutionEnv, dirs::Union{String, Vector{String}})::Tuple{Vector{Skill}, Vector{SkillDiagnostic}} - skills::Vector{Skill} = Skill[] - diagnostics::Vector{SkillDiagnostic} = SkillDiagnostic[] - - dir_list = if dirs isa String - [dirs] - else - dirs - end - - for dir in dir_list - root_info_result = fileInfo(env, dir, nothing) - if !root_info_result.ok - if root_info_result.error.code != "not_found" - push!(diagnostics, SkillDiagnostic( - "warning", - "file_info_failed", - root_info_result.error.message, - dir, - )) - end - continue - end - - root_info = root_info_result.value - if !isDirectory(env, root_info, diagnostics) - continue - end - - result = loadSkillsFromDirInternal(env, root_info.path, true, Dict{String, Any}(), root_info.path) - append!(skills, result.skills) - append!(diagnostics, result.diagnostics) - end - - return skills, diagnostics -end - -function isDirectory(env::ExecutionEnv, info::FileInfo, diagnostics::Vector{SkillDiagnostic})::Bool - return info.kind == "directory" -end - -function loadSkillsFromDirInternal( - env::ExecutionEnv, - dir::String, - include_root_files::Bool, - ignore_matcher::Dict{String, Any}, - root_dir::String, -)::Tuple{Vector{Skill}, Vector{SkillDiagnostic}} - skills::Vector{Skill} = Skill[] - diagnostics::Vector{SkillDiagnostic} = SkillDiagnostic[] - - dir_info_result = fileInfo(env, dir, nothing) - if !dir_info_result.ok - if dir_info_result.error.code != "not_found" - push!(diagnostics, SkillDiagnostic( - "warning", - "file_info_failed", - dir_info_result.error.message, - dir, - )) - end - return skills, diagnostics - end - - dir_info = dir_info_result.value - if !isDirectory(env, dir_info, diagnostics) - return skills, diagnostics - end - - # TODO: Implement ignore rules - # await addIgnoreRules(env, ignoreMatcher, dir, rootDir, diagnostics); - - entries_result = listDir(env, dir, nothing) - if !entries_result.ok - push!(diagnostics, SkillDiagnostic( - "warning", - "list_failed", - entries_result.error.message, - dir, - )) - return skills, diagnostics - end - - entries = entries_result.value - - # Look for SKILL.md - for entry in entries - if entry.name != "SKILL.md" - continue - end - - full_path = entry.path - if !isFile(env, entry, diagnostics) - continue - end - - result = loadSkillFromFile(env, full_path) - if !isnothing(result.skill) - push!(skills, result.skill) - end - append!(diagnostics, result.diagnostics) - return skills, diagnostics - end - - # Process other files - for entry in sort(entries, by=e -> e.name) - if startswith(entry.name, ".") || entry.name == "node_modules" - continue - end - - full_path = entry.path - kind = getFileKind(env, entry, diagnostics) - if isnothing(kind) - continue - end - - rel_path = relativeEnvPath(root_dir, full_path) - ignore_path = kind == "directory" ? "$(rel_path)/" : rel_path - - if !isnothing(ignore_matcher) && haskey(ignore_matcher, ignore_path) - continue - end - - if kind == "directory" - result = loadSkillsFromDirInternal(env, full_path, false, ignore_matcher, root_dir) - append!(skills, result.skills) - append!(diagnostics, result.diagnostics) - continue - end - - if kind != "file" || !include_root_files || !endswith(entry.name, ".md") - continue - end - - result = loadSkillFromFile(env, full_path) - if !isnothing(result.skill) - push!(skills, result.skill) - end - append!(diagnostics, result.diagnostics) - end - - return skills, diagnostics -end - -function isFile(env::ExecutionEnv, info::FileInfo, diagnostics::Vector{SkillDiagnostic})::Bool - return info.kind == "file" -end - -function getFileKind(env::ExecutionEnv, info::FileInfo, diagnostics::Vector{SkillDiagnostic})::Union{String, Nothing} - if info.kind == "file" || info.kind == "directory" - return info.kind - end - - canonical_path = canonicalPath(env, info.path, nothing) - if !canonical_path.ok - if canonical_path.error.code != "not_found" - push!(diagnostics, SkillDiagnostic( - "warning", - "file_info_failed", - canonical_path.error.message, - info.path, - )) - end - return nothing - end - - target = fileInfo(env, canonical_path.value, nothing) - if !target.ok - if target.error.code != "not_found" - push!(diagnostics, SkillDiagnostic( - "warning", - "file_info_failed", - target.error.message, - info.path, - )) - end - return nothing - end - - if target.value.kind == "file" || target.value.kind == "directory" - return target.value.kind - end - - return nothing -end - -# ============================================================================ -# Load skill from file -# ============================================================================ - -function loadSkillFromFile(env::ExecutionEnv, file_path::String)::Tuple{Union{Skill, Nothing}, Vector{SkillDiagnostic}} - diagnostics::Vector{SkillDiagnostic} = SkillDiagnostic[] - - raw_content = readTextFile(env, file_path, nothing) - if !raw_content.ok - push!(diagnostics, SkillDiagnostic( - "warning", - "read_failed", - raw_content.error.message, - file_path, - )) - return nothing, diagnostics - end - - # TODO: Parse frontmatter - # parsed = parseFrontmatter(rawContent.value); - # if !parsed.ok { - # diagnostics.push({ type: "warning", code: "parse_failed", message: parsed.error.message, path: filePath }); - # return { skill: null, diagnostics }; - # } - - # const { frontmatter, body } = parsed.value; - # const skillDir = dirnameEnvPath(filePath); - # const parentDirName = basenameEnvPath(skillDir); - # const description = typeof frontmatter.description === "string" ? frontmatter.description : undefined; - - # for (const error of validateDescription(description)) { - # diagnostics.push({ type: "warning", code: "invalid_metadata", message: error, path: filePath }); - # } - - # const frontmatterName = typeof frontmatter.name === "string" ? frontmatter.name : undefined; - # const name = frontmatterName || parentDirName; - # for (const error of validateName(name, parentDirName)) { - # diagnostics.push({ type: "warning", code: "invalid_metadata", message: error, path: filePath }); - # } - - # if (!description || description.trim() === "") { - # return { skill: null, diagnostics }; - # } - - # return { - # skill: { - # name, - # description, - # content: body, - # filePath, - # disableModelInvocation: frontmatter["disable-model-invocation"] === true, - # }, - # diagnostics, - # }; - - return nothing, diagnostics -end - -# ============================================================================ -# Path utility functions -# ============================================================================ - -function joinEnvPath(base::String, child::String)::String - return "$(rtrim(base, '/'))/$(ltrim(child, '/'))" -end - -function dirnameEnvPath(path::String)::String - normalized = rtrim(path, '/') - slash_index = findlast('/', normalized) - if isnothing(slash_index) || slash_index <= 1 - return "/" - end - return normalized[1:slash_index-1] -end - -function basenameEnvPath(path::String)::String - normalized = rtrim(path, '/') - slash_index = findlast('/', normalized) - if isnothing(slash_index) - return normalized - end - return normalized[slash_index+1:end] -end - -function relativeEnvPath(root::String, path::String)::String - normalized_root = rtrim(root, '/') - normalized_path = rtrim(path, '/') - - if normalized_path == normalized_root - return "" - end - - if startswith(normalized_path, "$(normalized_root)/") - return normalized_path[length(normalized_root)+2:end] - end - - return lstrip(normalized_path, '/') -end - -# ============================================================================ -# Helper functions -# ============================================================================ - -function lstrip(s::String, chars::String)::String - idx = 1 - while idx <= length(s) && s[idx] in chars - idx += 1 - end - return s[idx:end] -end - -function rtrim(s::String, chars::String)::String - idx = length(s) - while idx >= 1 && s[idx] in chars - idx -= 1 - end - return s[1:idx] -end - -function findlast(pattern::Char, s::String)::Union{Int64, Nothing} - for i in length(s):-1:1 - if s[i] == pattern - return i - end - end - return nothing -end - -end diff --git a/packages/agent/julia_implementation/src/stream_fn.jl b/packages/agent/julia_implementation/src/stream_fn.jl deleted file mode 100644 index c78481fe..00000000 --- a/packages/agent/julia_implementation/src/stream_fn.jl +++ /dev/null @@ -1,45 +0,0 @@ -""" - stream_fn.jl - Stream function utilities - -This module provides the default stream function configuration for AgentCore. -""" - -module StreamFn - -using ..Types: StreamFn - -let default_stream_fn::Union{StreamFn, Nothing} = nothing - -""" - setDefaultStreamFn(stream_fn) - -Configure the fallback used by Agent and low-level loops when callers omit stream_fn. - -# Arguments -- `stream_fn`: The stream function to set as default -""" -function setDefaultStreamFn(stream_fn::Union{StreamFn, Nothing}) - global default_stream_fn = stream_fn -end - -""" - getDefaultStreamFn() - -Get the configured default stream function, or throw an error if none is configured. - -# Returns -- The configured stream function - -# Throws -- ErrorException if no default stream function is configured -""" -function getDefaultStreamFn()::StreamFn - if isnothing(default_stream_fn) - throw(ErrorException( - "No default stream function configured. Pass stream_fn explicitly or call setDefaultStreamFn()." - )) - end - return default_stream_fn -end - -end diff --git a/packages/agent/julia_implementation/src/system_prompt.jl b/packages/agent/julia_implementation/src/system_prompt.jl deleted file mode 100644 index ba1a4e85..00000000 --- a/packages/agent/julia_implementation/src/system_prompt.jl +++ /dev/null @@ -1,56 +0,0 @@ -""" - system_prompt.jl - System prompt formatting - -This module provides utilities for formatting skills in the system prompt. -""" - -module SystemPrompt - -using ..Types: Skill - -""" - formatSkillsForSystemPrompt(skills) - -Format skills for inclusion in the system prompt using XML-formatted blocks. -""" -function formatSkillsForSystemPrompt(skills::Vector{Skill})::String - visible_skills = filter(s -> !s.disableModelInvocation, skills) - if isempty(visible_skills) - return "" - end - - lines = String[ - "The following skills provide specialized instructions for specific tasks.", - "Read the full skill file when the task matches its description.", - "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.", - "", - "", - ] - - for skill in visible_skills - push!(lines, " ") - push!(lines, " $(escapeXml(skill.name))") - push!(lines, " $(escapeXml(skill.description))") - push!(lines, " $(escapeXml(skill.filePath))") - push!(lines, " ") - end - - push!(lines, "") - return join(lines, "\n") -end - -""" - escapeXml(value) - -Escape special characters in a string for XML. -""" -function escapeXml(value::String)::String - result = replace(value, "&" => "&") - result = replace(result, "<" => "<") - result = replace(result, ">" => ">") - result = replace(result, "\"" => """) - result = replace(result, "'" => "'") - return result -end - -end diff --git a/packages/agent/julia_implementation/src/tools/bash.jl b/packages/agent/julia_implementation/src/tools/bash.jl deleted file mode 100644 index ab4e3b64..00000000 --- a/packages/agent/julia_implementation/src/tools/bash.jl +++ /dev/null @@ -1,49 +0,0 @@ -""" - tools/bash.jl - Bash execution tool - -This module provides the bash execution tool for AgentCore. -""" - -module Bash - -using ..Types: * - -struct BashExecution - command::String - cwd::String - env::Dict{String, String} - inherit_env::Bool -end - -mutable struct BashPrepare{TContext} - function::Function - context::TContext - signal::Union{Any, Nothing} -end - -mutable struct BashToolOptions{TContext} - command_prefix::Union{String, Nothing} - prepare::Union{BashPrepare{TContext}, Nothing} -end - -mutable struct BashToolDetails - truncation::Union{Any, Nothing} - full_output_path::Union{String, Nothing} -end - -function createBashTool{TContext}(options::Union{BashToolOptions{TContext}, Nothing}=nothing) where TContext - return AgentTool( - "bash", - "bash", - "Execute a bash command in the current working directory.", - Dict{String, Any}(), - (tool_call_id, params, signal, on_update, context) -> begin - # TODO: Implement bash execution - return AgentToolResult([TextContent("Command executed successfully")], nothing, nothing, nothing, nothing) - end, - nothing, - nothing, - ) -end - -end diff --git a/packages/agent/julia_implementation/src/tools/edit.jl b/packages/agent/julia_implementation/src/tools/edit.jl deleted file mode 100644 index 8b004052..00000000 --- a/packages/agent/julia_implementation/src/tools/edit.jl +++ /dev/null @@ -1,32 +0,0 @@ -""" - tools/edit.jl - File edit tool - -This module provides the file edit tool for AgentCore. -""" - -module Edit - -using ..Types: * - -mutable struct EditToolDetails - diff::String - patch::String - first_changed_line::Union{Int64, Nothing} -end - -function createEditTool{TContext}() where TContext - return AgentTool( - "edit", - "edit", - "Edit a single file using exact text replacement.", - Dict{String, Any}(), - (tool_call_id, params, signal, on_update, context) -> begin - # TODO: Implement edit execution - return AgentToolResult([TextContent("File edited successfully")], nothing, nothing, nothing, nothing) - end, - nothing, - nothing, - ) -end - -end diff --git a/packages/agent/julia_implementation/src/tools/edit_diff.jl b/packages/agent/julia_implementation/src/tools/edit_diff.jl deleted file mode 100644 index 1dd4c7de..00000000 --- a/packages/agent/julia_implementation/src/tools/edit_diff.jl +++ /dev/null @@ -1,67 +0,0 @@ -""" - tools/edit_diff.jl - Edit diff utilities - -This module provides shared diff computation utilities for the edit tool. -""" - -module EditDiff - -using ..Types: * - -function detectLineEnding(content::String)::String - crlf_idx = findfirst("\r\n", content) - lf_idx = findfirst("\n", content) - if isnothing(lf_idx) - return "\n" - end - if isnothing(crlf_idx) - return "\n" - end - return crlf_idx < lf_idx ? "\r\n" : "\n" -end - -function normalizeToLF(text::String)::String - return replace(text, "\r\n" => "\n", "\r" => "\n") -end - -function restoreLineEndings(text::String, ending::String)::String - if ending == "\r\n" - return replace(text, "\n" => "\r\n") - end - return text -end - -function normalizeForFuzzyMatch(text::String)::String - # TODO: Implement fuzzy matching normalization - return text -end - -function splitLinesWithEndings(content::String)::Vector{String} - # TODO: Implement line splitting with endings - return split(content, "\n") -end - -function applyEditsToNormalizedContent( - normalized_content::String, - edits::Vector{Any}, - path::String, -)::Tuple{String, String} - # TODO: Implement edit application - return normalized_content, normalized_content -end - -function generateUnifiedPatch(path::String, old_content::String, new_content::String, context_lines::Int64=4)::String - # TODO: Implement unified patch generation - return "" -end - -function generateDiffString( - old_content::String, - new_content::String, - context_lines::Int64=4, -)::Tuple{String, Union{Int64, Nothing}} - # TODO: Implement diff string generation - return "", nothing -end - -end diff --git a/packages/agent/julia_implementation/src/tools/file_mutation_queue.jl b/packages/agent/julia_implementation/src/tools/file_mutation_queue.jl deleted file mode 100644 index 2f106ce9..00000000 --- a/packages/agent/julia_implementation/src/tools/file_mutation_queue.jl +++ /dev/null @@ -1,59 +0,0 @@ -""" - tools/file_mutation_queue.jl - File mutation queue - -This module provides file mutation serialization for safe concurrent file writes. -""" - -module FileMutationQueue - -using ..Types: * -using ..HarnessTypes: ExecutionEnv, getOrThrow, FileError, Result - -# ============================================================================ -# Mutation queue state -# ============================================================================ - -mutable struct MutationQueueState - queues::Dict{String, Any} - registration::Any -end - -# Global state -const states = Dict{ExecutionEnv, MutationQueueState}() - -function getState(env::ExecutionEnv)::MutationQueueState - if !haskey(states, env) - states[env] = MutationQueueState(Dict{String, Any}(), nothing) - end - return states[env] -end - -# ============================================================================ -# File mutation queue helpers -# ============================================================================ - -async function getMutationQueueKey(env::ExecutionEnv, path::String)::String - absolute_path = getOrThrow(getOrThrow(absolutePath(env, path), "Failed to get absolute path")) - canonical_path = canonicalPath(env, absolute_path, nothing) - if canonical_path.ok - return canonical_path.value - end - if canonical_path.error.code in ("not_found", "not_supported") - return absolute_path - end - throw(canonical_path.error) -end - -# ============================================================================ -# Main function - serialize file mutations -# ============================================================================ - -function withFileMutationQueue{T}(env::ExecutionEnv, path::String, fn::Function)::T - state = getState(env) - - # TODO: Implement proper async queueing - # This is a simplified version - return fn() -end - -end diff --git a/packages/agent/julia_implementation/src/tools/image.jl b/packages/agent/julia_implementation/src/tools/image.jl deleted file mode 100644 index db99146e..00000000 --- a/packages/agent/julia_implementation/src/tools/image.jl +++ /dev/null @@ -1,66 +0,0 @@ -""" - tools/image.jl - Image utilities - -This module provides image detection and encoding utilities. -""" - -module Image - -using ..Types: * - -function detectSupportedImageMimeType(buffer::Vector{UInt8})::Union{String, Nothing} - if length(buffer) >= 3 && buffer[1:3] == [0xff, 0xd8, 0xff] - if buffer[4] == 0xf7 - return nothing - end - return "image/jpeg" - end - - if length(buffer) >= 8 && buffer[1:8] == [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] - return "image/png" - end - - if length(buffer) >= 3 && buffer[1:3] == [0x47, 0x49, 0x46] - return "image/gif" - end - - if length(buffer) >= 12 && buffer[1:4] == [0x52, 0x49, 0x46, 0x46] && buffer[9:12] == [0x57, 0x45, 0x42, 0x50] - return "image/webp" - end - - if length(buffer) >= 2 && buffer[1:2] == [0x42, 0x4d] - return "image/bmp" - end - - return nothing -end - -function encodeBase64(bytes::Vector{UInt8})::String - alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" - output = "" - - for i in 1:3:length(bytes) - first_byte = i <= length(bytes) ? bytes[i] : 0 - second_byte = i+1 <= length(bytes) ? bytes[i+1] : 0 - third_byte = i+2 <= length(bytes) ? bytes[i+2] : 0 - - output *= alphabet[first_byte >> 2 + 1] - output *= alphabet[(((first_byte & 0x03) << 4) | ((second_byte >> 4) & 0x0f)) + 1] - - if i+1 <= length(bytes) - output *= alphabet[(((second_byte & 0x0f) << 2) | ((third_byte >> 6) & 0x03)) + 1] - else - output *= "=" - end - - if i+2 <= length(bytes) - output *= alphabet[third_byte & 0x3f + 1] - else - output *= "=" - end - end - - return output -end - -end diff --git a/packages/agent/julia_implementation/src/tools/index.jl b/packages/agent/julia_implementation/src/tools/index.jl deleted file mode 100644 index d20be3aa..00000000 --- a/packages/agent/julia_implementation/src/tools/index.jl +++ /dev/null @@ -1,35 +0,0 @@ -""" - tools/index.jl - Tool exports - -This module exports all tools. -""" - -module ToolsIndex - -using ..Tools.Bash: createBashTool -using ..Tools.Read: createReadTool -using ..Tools.Write: createWriteTool -using ..Tools.Edit: createEditTool -using ..Tools.Edit: EditToolDetails, EditToolInput -using ..Tools.Read: ReadToolDetails, ReadToolInput, ReadToolOptions, ReadImageProcessor, ReadImageProcessorResult - -export - createBashTool, - createReadTool, - createWriteTool, - createEditTool, - BashExecution, - BashPrepare, - BashToolDetails, - BashToolInput, - BashToolOptions, - EditToolDetails, - EditToolInput, - ReadToolDetails, - ReadToolInput, - ReadToolOptions, - ReadImageProcessor, - ReadImageProcessorResult, - WriteToolInput - -end diff --git a/packages/agent/julia_implementation/src/tools/path_utils.jl b/packages/agent/julia_implementation/src/tools/path_utils.jl deleted file mode 100644 index dadacf49..00000000 --- a/packages/agent/julia_implementation/src/tools/path_utils.jl +++ /dev/null @@ -1,44 +0,0 @@ -""" - tools/path_utils.jl - Path resolution utilities - -This module provides path resolution utilities for tools. -""" - -module PathUtils - -using ..Types: * -using ..HarnessTypes: ExecutionEnv, getOrThrow, FileError, Result - -function normalizeToolPath(path::String)::String - normalized = replace(path, r"[\u00A0\u2000-\u200A\u202F\u205F\u3000]" => " ") - if startswith(normalized, "@") - return normalized[2:end] - end - return normalized -end - -function resolveToolPath(env::ExecutionEnv, path::String, signal::Union{Any, Nothing}=nothing)::String - return getOrThrow(getOrThrow(absolutePath(env, normalizeToolPath(path), signal), "Failed to resolve path")) -end - -function resolveReadToolPath(env::ExecutionEnv, path::String, signal::Union{Any, Nothing}=nothing)::String - resolved = getOrThrow(getOrThrow(absolutePath(env, normalizeToolPath(path), signal), "Failed to resolve path")) - - variants = String[ - resolved, - replace(resolved, r" (AM|PM)\."i => " $1."), - normalized = replace(resolved, NFC => NFD), - replace(resolved, "'" => "\u2019"), - replace(replace(resolved, NFC => NFD), "'" => "\u2019"), - ] - - for variant in variants - if getOrThrow(getOrThrow(exists(env, variant, signal), "Failed to check existence"), "Not found") - return variant - end - end - - return resolved -end - -end diff --git a/packages/agent/julia_implementation/src/tools/read.jl b/packages/agent/julia_implementation/src/tools/read.jl deleted file mode 100644 index b3584377..00000000 --- a/packages/agent/julia_implementation/src/tools/read.jl +++ /dev/null @@ -1,35 +0,0 @@ -""" - tools/read.jl - File read tool - -This module provides the file read tool for AgentCore. -""" - -module Read - -using ..Types: * - -mutable struct ReadToolDetails - truncation::Union{Any, Nothing} -end - -mutable struct ReadToolOptions - auto_resize_images::Bool - image_processor::Union{Any, Nothing} -end - -function createReadTool{TContext}(options::Union{ReadToolOptions, Nothing}=nothing) where TContext - return AgentTool( - "read", - "read", - "Read the contents of a file.", - Dict{String, Any}(), - (tool_call_id, params, signal, on_update, context) -> begin - # TODO: Implement read execution - return AgentToolResult([TextContent("File read successfully")], nothing, nothing, nothing, nothing) - end, - nothing, - nothing, - ) -end - -end diff --git a/packages/agent/julia_implementation/src/tools/write.jl b/packages/agent/julia_implementation/src/tools/write.jl deleted file mode 100644 index 5d4acc3d..00000000 --- a/packages/agent/julia_implementation/src/tools/write.jl +++ /dev/null @@ -1,26 +0,0 @@ -""" - tools/write.jl - File write tool - -This module provides the file write tool for AgentCore. -""" - -module Write - -using ..Types: * - -function createWriteTool{TContext}() where TContext - return AgentTool( - "write", - "write", - "Write content to a file.", - Dict{String, Any}(), - (tool_call_id, params, signal, on_update, context) -> begin - # TODO: Implement write execution - return AgentToolResult([TextContent("File written successfully")], nothing, nothing, nothing, nothing) - end, - nothing, - nothing, - ) -end - -end diff --git a/packages/agent/julia_implementation/src/types.jl b/packages/agent/julia_implementation/src/types.jl deleted file mode 100644 index fd39e412..00000000 --- a/packages/agent/julia_implementation/src/types.jl +++ /dev/null @@ -1,588 +0,0 @@ -""" - types.jl - Core types for AgentCore - -This module defines the fundamental types used throughout the AgentCore package. -""" - -module Types - -using Dates -using UUIDs -using JSON3 -using Unicode - -# ============================================================================ -# Basic type aliases -# ============================================================================ - -const Timestamp = Int64 - -# ============================================================================ -# Thinking level enum -# ============================================================================ - -@enum ThinkingLevel begin - THINKING_OFF = "off" - THINKING_MINIMAL = "minimal" - THINKING_LOW = "low" - THINKING_MEDIUM = "medium" - THINKING_HIGH = "high" - THINKING_XHIGH = "xhigh" - THINKING_MAX = "max" -end - -# ============================================================================ -# Tool execution modes -# ============================================================================ - -@enum ToolExecutionMode begin - EXECUTION_SEQUENTIAL = "sequential" - EXECUTION_PARALLEL = "parallel" -end - -# ============================================================================ -# Queue drain modes -# ============================================================================ - -@enum QueueMode begin - QUEUE_ALL = "all" - QUEUE_ONE_AT_A_TIME = "one-at-a-time" -end - -# ============================================================================ -# Message content types -# ============================================================================ - -abstract type MessageContent end - -struct TextContent <: MessageContent - text::String -end - -struct ImageContent <: MessageContent - data::String - mime_type::String -end - -# ============================================================================ -# Message types -# ============================================================================ - -abstract type Message end - -struct UserMessage <: Message - role::String - content::Vector{MessageContent} - timestamp::Timestamp -end - -struct AssistantMessage <: Message - role::String - content::Vector{MessageContent} - api::String - provider::String - model::String - usage::Usage - stop_reason::String - error_message::Union{String, Nothing} - timestamp::Timestamp -end - -struct ToolResultMessage <: Message - role::String - tool_call_id::String - tool_name::String - content::Vector{MessageContent} - details::Any - usage::Union{Usage, Nothing} - added_tool_names::Union{Vector{String}, Nothing} - is_error::Bool - timestamp::Timestamp -end - -# ============================================================================ -# Usage statistics -# ============================================================================ - -struct UsageCost - input::Float64 - output::Float64 - cache_read::Float64 - cache_write::Float64 - total::Float64 -end - -struct Usage - input::Int64 - output::Int64 - cache_read::Int64 - cache_write::Int64 - total_tokens::Int64 - cost::UsageCost -end - -# ============================================================================ -# Model types -# ============================================================================ - -struct ModelCost - input::Float64 - output::Float64 - cache_read::Float64 - cache_write::Float64 -end - -struct Model{Api} - id::String - name::String - api::Api - provider::String - base_url::String - reasoning::Bool - input::Vector{String} - cost::ModelCost - context_window::Int64 - max_tokens::Int64 -end - -# ============================================================================ -# Agent message union type -# ============================================================================ - -abstract type AgentMessage end - -# Custom message types can extend this via multiple dispatch -struct CustomMessage <: AgentMessage - message::AgentMessage - custom_type::String -end - -# ============================================================================ -# Tool types -# ============================================================================ - -struct AgentToolResult{T} - content::Vector{MessageContent} - details::T - usage::Union{Usage, Nothing} - added_tool_names::Union{Vector{String}, Nothing} - terminate::Union{Bool, Nothing} -end - -struct AgentTool{TParameters, TDetails} - name::String - label::String - description::String - parameters::TParameters - execute::Function - prepare_arguments::Union{Function, Nothing} - execution_mode::Union{ToolExecutionMode, Nothing} -end - -# ============================================================================ -# Agent context -# ============================================================================ - -struct AgentContext - system_prompt::String - messages::Vector{AgentMessage} - tools::Union{Vector{AgentTool}, Nothing} -end - -# ============================================================================ -# Event types -# ============================================================================ - -abstract type AgentEvent end - -struct AgentStartEvent <: AgentEvent end -struct AgentEndEvent <: AgentEvent - messages::Vector{AgentMessage} -end -struct TurnStartEvent <: AgentEvent end -struct TurnEndEvent <: AgentEvent - message::AgentMessage - tool_results::Vector{ToolResultMessage} -end -struct MessageStartEvent <: AgentEvent - message::AgentMessage -end -struct MessageUpdateEvent <: AgentEvent - message::AgentMessage - assistant_message_event::Any -end -struct MessageEndEvent <: AgentEvent - message::AgentMessage -end -struct ToolExecutionStartEvent <: AgentEvent - tool_call_id::String - tool_name::String - args::Any -end -struct ToolExecutionUpdateEvent <: AgentEvent - tool_call_id::String - tool_name::String - args::Any - partial_result::Any -end -struct ToolExecutionEndEvent <: AgentEvent - tool_call_id::String - tool_name::String - result::Any - is_error::Bool -end - -# ============================================================================ -# Assistant message event types -# ============================================================================ - -abstract type AssistantMessageEvent end - -struct StartEvent <: AssistantMessageEvent - partial::AssistantMessage -end -struct TextStartEvent <: AssistantMessageEvent - content_index::Int64 - partial::AssistantMessage -end -struct TextDeltaEvent <: AssistantMessageEvent - content_index::Int64 - delta::String - partial::AssistantMessage -end -struct TextEndEvent <: AssistantMessageEvent - content_index::Int64 - content::String - partial::AssistantMessage -end -struct DoneEvent <: AssistantMessageEvent - reason::String - usage::Usage - message::AssistantMessage -end -struct ErrorEvent <: AssistantMessageEvent - reason::String - error_message::Union{String, Nothing} - usage::Usage - error::AssistantMessage -end - -# ============================================================================ -# Agent state -# ============================================================================ - -mutable struct AgentState - system_prompt::String - model::Model - thinking_level::ThinkingLevel - tools::Vector{AgentTool} - messages::Vector{AgentMessage} - is_streaming::Bool - streaming_message::Union{AgentMessage, Nothing} - pending_tool_calls::Set{String} - error_message::Union{String, Nothing} - - function AgentState( - system_prompt::String="", - model::Model=Model("", "", "unknown", "unknown", "", false, String[], ModelCost(0.0, 0.0, 0.0, 0.0), 0, 0), - thinking_level::ThinkingLevel=THINKING_OFF, - tools::Vector{AgentTool}=AgentTool[], - messages::Vector{AgentMessage}=AgentMessage[], - ) - new( - system_prompt, - model, - thinking_level, - copy(tools), - copy(messages), - false, - nothing, - Set{String}(), - nothing, - ) - end -end - -# ============================================================================ -# Tool call types -# ============================================================================ - -struct ToolCall - type::String - id::String - name::String - arguments::Dict{String, Any} - partial_json::Union{String, Nothing} -end - -# ============================================================================ -# Context transform types -# ============================================================================ - -struct PrepareNextTurnContext - message::AssistantMessage - tool_results::Vector{ToolResultMessage} - context::AgentContext - new_messages::Vector{AgentMessage} -end - -struct AgentLoopTurnUpdate - context::Union{AgentContext, Nothing} - model::Union{Model, Nothing} - thinking_level::Union{ThinkingLevel, Nothing} -end - -# ============================================================================ -# Before/After tool call types -# ============================================================================ - -struct BeforeToolCallContext - assistant_message::AssistantMessage - tool_call::ToolCall - args::Any - context::AgentContext -end - -struct BeforeToolCallResult - block::Union{Bool, Nothing} - reason::Union{String, Nothing} -end - -struct AfterToolCallContext - assistant_message::AssistantMessage - tool_call::ToolCall - args::Any - result::AgentToolResult - is_error::Bool - context::AgentContext -end - -struct AfterToolCallResult - content::Union{Vector{MessageContent}, Nothing} - details::Union{Any, Nothing} - is_error::Union{Bool, Nothing} - usage::Union{Usage, Nothing} - terminate::Union{Bool, Nothing} -end - -# ============================================================================ -# Stream function signature -# ============================================================================ - -const StreamFn = Function - -# ============================================================================ -# File types -# ============================================================================ - -struct FileKind - value::String -end -const FILE_KIND_FILE = FileKind("file") -const FILE_KIND_DIRECTORY = FileKind("directory") -const FILE_KIND_SYMLINK = FileKind("symlink") - -struct FileInfo - name::String - path::String - kind::FileKind - size::Int64 - mtime_ms::Int64 -end - -struct FileError <: Exception - code::String - message::String - path::Union{String, Nothing} - cause::Union{Exception, Nothing} -end - -struct ExecutionError <: Exception - code::String - message::String - cause::Union{Exception, Nothing} -end - -struct CompactionError <: Exception - code::String - message::String - cause::Union{Exception, Nothing} -end - -struct BranchSummaryError <: Exception - code::String - message::String - cause::Union{Exception, Nothing} -end - -struct SessionError <: Exception - code::String - message::String - cause::Union{Exception, Nothing} -end - -struct AgentHarnessError <: Exception - code::String - message::String - cause::Union{Exception, Nothing} -end - -# ============================================================================ -# Session tree entry types -# ============================================================================ - -abstract type SessionTreeEntry end - -struct SessionTreeEntryBase - type::String - id::String - parent_id::Union{String, Nothing} - timestamp::String -end - -struct MessageEntry <: SessionTreeEntry - base::SessionTreeEntryBase - message::AgentMessage -end - -struct ThinkingLevelChangeEntry <: SessionTreeEntry - base::SessionTreeEntryBase - thinking_level::String -end - -struct ModelChangeEntry <: SessionTreeEntry - base::SessionTreeEntryBase - provider::String - model_id::String -end - -struct ActiveToolsChangeEntry <: SessionTreeEntry - base::SessionTreeEntryBase - active_tool_names::Vector{String} -end - -struct CompactionEntry{T} <: SessionTreeEntry - base::SessionTreeEntryBase - summary::String - first_kept_entry_id::Union{String, Nothing} - tokens_before::Int64 - retained_tail::Union{Vector{AgentMessage}, Nothing} - details::Union{T, Nothing} - usage::Union{Usage, Nothing} - from_hook::Bool -end - -struct BranchSummaryEntry{T} <: SessionTreeEntry - base::SessionTreeEntryBase - from_id::String - summary::String - details::Union{T, Nothing} - usage::Union{Usage, Nothing} - from_hook::Bool -end - -struct CustomEntry{T} <: SessionTreeEntry - base::SessionTreeEntryBase - custom_type::String - data::Union{T, Nothing} -end - -struct CustomMessageEntry{T} <: SessionTreeEntry - base::SessionTreeEntryBase - custom_type::String - content::String - details::Union{T, Nothing} - display::Bool -end - -struct LabelEntry <: SessionTreeEntry - base::SessionTreeEntryBase - target_id::String - label::Union{String, Nothing} -end - -struct SessionInfoEntry <: SessionTreeEntry - base::SessionTreeEntryBase - name::Union{String, Nothing} -end - -struct LeafEntry <: SessionTreeEntry - base::SessionTreeEntryBase - target_id::Union{String, Nothing} -end - -# ============================================================================ -# Session context -# ============================================================================ - -struct SessionContext - messages::Vector{AgentMessage} - thinking_level::String - model::Union{Dict{String, String}, Nothing} - active_tool_names::Union{Vector{String}, Nothing} -end - -# ============================================================================ -# Session stats -# ============================================================================ - -struct SessionStats - message_count::Int64 - cached_tokens::Int64 - uncached_tokens::Int64 - total_tokens::Int64 - cost_total::Float64 -end - -# ============================================================================ -# Session metadata -# ============================================================================ - -abstract type SessionMetadata end - -struct JsonlSessionMetadata <: SessionMetadata - id::String - created_at::String - cwd::String - path::String - parent_session_path::Union{String, Nothing} - metadata::Union{Dict{String, Any}, Nothing} -end - -# ============================================================================ -# Session storage interface -# ============================================================================ - -abstract type SessionStorage{T<:SessionMetadata} end - -# ============================================================================ -# Session repo interface -# ============================================================================ - -abstract type SessionRepo< - TMetadata<:SessionMetadata, - TCreateOptions, - TListOptions -> end - -# ============================================================================ -# Helper functions -# ============================================================================ - -function create_timestamp()::String - return string(Dates.now(Dates.UTC)) -end - -function uuidv7()::String - return string(UUIDs.uuid7()) -end - -function uuidstring()::String - return string(UUIDs.uuid4()) -end - -function tempname()::String - return tempname() -end - -end diff --git a/packages/agent/learning/01-ARCHITECTURE-OVERVIEW.md b/packages/agent/learning/01-ARCHITECTURE-OVERVIEW.md new file mode 100644 index 00000000..943dca92 --- /dev/null +++ b/packages/agent/learning/01-ARCHITECTURE-OVERVIEW.md @@ -0,0 +1,433 @@ +# Pi Agent Architecture - Top-Down Overview + +## Executive Summary + +The Pi Agent is a **stateful, event-driven agent framework** built in TypeScript. It provides: + +1. **Core Agent** - Low-level agent loop with message/tool streaming +2. **Agent Harness** - High-level session management with persistence, branching, and compaction + +Both layers follow the **same core pattern**: stream LLM response → execute tools → emit events → repeat. + +--- + +## Architecture Layers + +``` +┌─────────────────────────────────────────────────────────────────────────────────────┐ +│ APPLICATION LAYER │ +│ • Creates Agent/AgentHarness instances │ +│ • Subscribes to events for UI updates │ +│ • Provides tools and model configuration │ +└─────────────────────────────────────────────────────────────────────────────────────┘ + │ + ┌─────────────────────────────┼─────────────────────────────┐ + │ │ │ + ▼ ▼ ▼ +┌────────────────────────┐ ┌────────────────────────────────┐ ┌─────────────────┐ +│ Agent (Core) │ │ Agent Harness (High-Level) │ │ Agent-Loop │ +│ │ │ │ │ │ +│ • State management │ │ • Session persistence │ │ • Turn │ +│ • Event streaming │ │ • Branching/compaction │ │ • Tool exec │ +│ • Steering/follow-up │ │ • Skills/templates │ │ • Message │ +│ queues │ │ • Tool context binding │ │ streaming │ +│ • Hook system │ │ • State snapshots │ │ │ +└────────────────────────┘ └────────────────────────────────┘ └─────────────────┘ + │ │ + ▼ ▼ + ┌──────────────────────────┐ ┌─────────────────┐ + │ LLM Provider API │ │ Session Repo │ + │ (via @earendil-works) │ │ (JSONL/ │ + └──────────────────────────┘ │ Memory) │ + └─────────────────┘ +``` + +--- + +## Core Concepts + +### 1. AgentMessage + +```typescript +type AgentMessage = Message | CustomAgentMessages[keyof CustomAgentMessages] +``` + +The unified message type that combines: +- **LLM messages**: `user`, `assistant`, `toolResult` (from pi-ai) +- **Custom messages**: Application-specific types (via declaration merging) + +### 2. AgentEvent + +```typescript +type AgentEvent = + | { type: "agent_start" } + | { type: "agent_end"; messages: AgentMessage[] } + | { type: "turn_start" } + | { type: "turn_end"; message: AgentMessage; toolResults: ToolResultMessage[] } + | { type: "message_start"; message: AgentMessage } + | { type: "message_update"; message: AgentMessage; assistantMessageEvent: AssistantMessageEvent } + | { type: "message_end"; message: AgentMessage } + | { type: "tool_execution_start"; toolCallId: string; toolName: string; args: any } + | { type: "tool_execution_update"; toolCallId: string; toolName: string; args: any; partialResult: any } + | { type: "tool_execution_end"; toolCallId: string; toolName: string; result: any; isError: boolean } +``` + +**Event Flow per Turn:** +``` +turn_start + message_start (user prompt) + message_end + message_start (assistant streaming) + message_update (multiple - as chunks arrive) + message_end + tool_execution_start (if tool calls present) + tool_execution_update (if tool streams partial results) + tool_execution_end + turn_end +``` + +### 3. AgentTool + +```typescript +interface AgentTool { + name: string; + label: string; + description: string; + parameters: TSchema; + execute( + toolCallId: string, + params: Static, + signal?: AbortSignal, + onUpdate?: AgentToolUpdateCallback + ): Promise>; +} +``` + +--- + +## Process Flow Diagrams + +### Prompt Flow (High-Level) + +``` +User Input + │ + ▼ +Agent.prompt("Hello") + │ + ├─► normalizePromptInput() → AgentMessage[] + │ + ├─► runWithLifecycle() + │ ├─► Set isStreaming=true + │ └─► Create abort controller + │ + ▼ +runAgentLoop() + │ + ├─► Emit: agent_start + ├─► Emit: turn_start + ├─► Emit: message_start/end (prompts) + │ + ▼ +runLoop() - Main Loop + │ + ├─► Check steering queue (drain if any) + ├─► Check follow-up queue (skip if not first turn) + │ + ▼ +streamAssistantResponse() + │ + ├─► transformContext() [optional] + ├─► convertToLlm() → Message[] + ├─► Build Context {systemPrompt, messages, tools} + ├─► Resolve API key + ├─► Call streamFn(model, context, options) + │ + ▼ +Assistant Message Stream + │ + ├─► message_start (assistant) + ├─► message_update (text chunks) + ├─► message_update (toolCall blocks) + ├─► message_end + │ + ▼ +executeToolCalls() + │ + ├─► Check if sequential/parallel execution + ├─► For each tool call: + │ ├─► prepareToolCall() + │ │ ├─► Find tool by name + │ │ ├─► Validate arguments + │ │ └─► beforeToolCall() hook + │ │ + │ ├─► executePreparedToolCall() + │ │ └─► tool.execute() with onUpdate callback + │ │ + │ └─► finalizeExecutedToolCall() + │ └─► afterToolCall() hook + │ + ├─► Emit: tool_execution_start/update/end + └─► Emit: message_start/end (toolResult) + │ + ▼ +turn_end + │ + ├─► Check prepareNextTurn hook + ├─► Check shouldStopAfterTurn hook + ├─► Drain steering queue + └─► Drain follow-up queue + │ + ├─► If steering/follow-up exists → repeat loop + └─► If no more messages → agent_end +``` + +### Tool Execution Flow (Detailed) + +``` +Tool Call from LLM + │ + ▼ +prepareToolCall() + │ + ├─► Find tool in currentContext.tools + │ └─► If not found → immediate error + │ + ├─► prepareToolCallArguments() [optional] + │ + ├─► validateToolArguments() + │ └─► If invalid → immediate error + │ + └─► beforeToolCall() hook + ├─► Return {block: true} → error + └─► Continue + │ + ▼ +executePreparedToolCall() + │ + ├─► Call tool.execute() with onUpdate callback + │ └─► tool calls onUpdate(partialResult) during execution + │ + ├─► onUpdate() → emit tool_execution_update + └─► Return {result, isError} + │ + ▼ +finalizeExecutedToolCall() + │ + └─► afterToolCall() hook + ├─► Override content/details/usage/terminate + └─► Return {toolCall, result, isError} + │ + ▼ +emitToolExecutionEnd() + │ + └─► Emit: tool_execution_end + │ + ▼ +createToolResultMessage() + │ + └─► Create ToolResultMessage with: + ├─► toolCallId + ├─► toolName + ├─► content + ├─► details + ├─► usage + └─► isError + │ + ▼ +emitToolResultMessage() + │ + ├─► Emit: message_start + └─► Emit: message_end +``` + +### Session Persistence Flow + +``` +AgentHarness.handleAgentEvent() + │ + ├─► message_end → session.appendMessage() + │ └─► Storage: write entry to JSONL file + │ + ├─► turn_end → flushPendingSessionWrites() + │ ├─► Write all pending entries + │ ├─► Emit: save_point + │ └─► session.getStorage().setLeafId() + │ + └─► agent_end → flushPendingSessionWrites() + ├─► Write leaf entry pointing to last message + └─► Emit: settled + │ + ▼ +Session Tree Structure: + root + ├─► message (user prompt #1) + ├─► message (assistant #1) + ├─► tool_result (result #1) + ├─► turn_end + ├─► message (user prompt #2) + ├─► message (assistant #2) + ├─► compaction (summary of history) + ├─► message (assistant continues) + └─► leaf → points to current head +``` + +--- + +## Hook System + +### Agent-Level Hooks (agent-loop.ts) + +```typescript +interface AgentLoopConfig { + // Message transformation + convertToLlm: (messages: AgentMessage[]) => Message[] + transformContext?: (messages: AgentMessage[]) => AgentMessage[] + + // Lifecycle hooks + beforeToolCall?: (context: BeforeToolCallContext) => BeforeToolCallResult + afterToolCall?: (context: AfterToolCallContext) => AfterToolCallResult + shouldStopAfterTurn?: (context: ShouldStopAfterTurnContext) => boolean + prepareNextTurn?: (context: PrepareNextTurnContext) => AgentLoopTurnUpdate + + // Queue draining + getSteeringMessages?: () => AgentMessage[] + getFollowUpMessages?: () => AgentMessage[] +} +``` + +### Harness-Level Hooks (agent-harness.ts) + +```typescript +// Hook types in AgentHarnessEventResultMap: +type HookName = + | "before_agent_start" + | "context" + | "tool_call" + | "tool_result" + | "session_before_compact" + | "session_before_tree" + | "before_provider_request" + | "before_provider_payload" +``` + +**Hook Execution Order per Turn:** +``` +1. before_agent_start (harness) +2. context (harness) → transformContext +3. streamAssistantResponse + ├─► Before provider request (harness) + ├─► convertToLlm (agent) + └─► LLM call +4. For each tool call: + ├─► tool_call (harness) → beforeToolCall + ├─► Execute tool + └─► tool_result (harness) → afterToolCall +5. turn_end +6. shouldStopAfterTurn (agent) +7. prepareNextTurn (agent) +8. Drain steering/follow-up queues +``` + +--- + +## Data Flow Summary + +``` +┌────────────────────────────────────────────────────────────────────────────────┐ +│ AGENT LIFECYCLE - DATA FLOW │ +├────────────────────────────────────────────────────────────────────────────────┤ +│ 1. INPUT │ +│ • prompt("Hello") → normalizePromptInput() │ +│ → AgentMessage[] │ +│ 2. INITIATE │ +│ • createMutableAgentState() │ +│ • runWithLifecycle() │ +│ 3. LOOP CONTROL │ +│ • runLoop() │ +│ ├─► Steering queue? → drain and inject │ +│ └─► Follow-up queue? (after first turn) │ +│ 4. LLM STREAMING │ +│ • transformContext() [optional] │ +│ • convertToLlm() │ +│ • streamFn() │ +│ → AssistantMessage stream (text + toolCalls) │ +│ 5. TOOL EXECUTION │ +│ • executeToolCalls() │ +│ ├─► prepareToolCall() │ +│ │ ├─► beforeToolCall() hook │ +│ │ └─► Validate args │ +│ ├─► executePreparedToolCall() │ +│ │ └─► tool.execute() │ +│ └─► finalizeExecutedToolCall() │ +│ └─► afterToolCall() hook │ +│ 6. UPDATE STATE │ +│ • Push assistant message to state.messages │ +│ • Push toolResult messages to state.messages │ +│ 7. TERMINATION CHECK │ +│ • shouldStopAfterTurn? → exit │ +│ • prepareNextTurn? → update context/model │ +│ • Drain steering/follow-up → continue │ +│ 8. FINISH │ +│ • emit agent_end │ +│ • finishRun() → reset isStreaming │ +└────────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Key Design Patterns + +### 1. Event-Driven Architecture + +- All external communication via `AgentEvent` stream +- Hooks can be async and are awaited in order +- Abort signal propagated through all operations + +### 2. State Isolation + +- `AgentState` is read-only externally +- `AgentHarness` snapshots state per turn +- Context transforms return new arrays (immutability) + +### 3. Layered Abstraction + +``` +Low-level (agent-loop.ts) + • Pure async iteration + • No session management + • No tool context binding + +High-level (agent-harness.ts) + • Session persistence + • Branching/compaction + • Hook system for customization +``` + +### 4. Extensibility Points + +- **Custom messages**: Extend `CustomAgentMessages` interface +- **Custom hooks**: Add handlers via `subscribe()`/`on()` +- **Tool context**: Pass `toolContext` to harness constructor +- **Storage**: Implement `SessionStorage` interface + +--- + +## Learning Path + +1. **Start with types.ts** - Understand `AgentMessage`, `AgentEvent`, `AgentTool` +2. **Read agent-loop.ts** - See how messages flow through the loop +3. **Study agent.ts** - See how Agent wraps the loop with state management +4. **Read agent-harness.ts** - See how session management hooks into the loop +5. **Explore session/* files** - Understand persistence and branching +6. **Study tools/* files** - See concrete tool implementations + +--- + +## Next Steps + +See individual markdown files in this folder for: +- `AGENT-LOOP-DETAILED.md` - Deep dive into the agent loop +- `HOOK-SYSTEM.md` - Complete hook documentation +- `SESSION-ARCHITECTURE.md` - Session persistence details +- `TOOL-EXECUTION.md` - Tool execution mechanics diff --git a/packages/agent/learning/02-AGENT-LOOP-DETAILED.md b/packages/agent/learning/02-AGENT-LOOP-DETAILED.md new file mode 100644 index 00000000..31edc6b7 --- /dev/null +++ b/packages/agent/learning/02-AGENT-LOOP-DETAILED.md @@ -0,0 +1,697 @@ +# Agent Loop Deep Dive + +## Overview + +The `agent-loop.ts` file contains the **core async iteration logic** that drives the agent. It's intentionally low-level and stateless - it takes a snapshot of context and drives it to completion. + +--- + +## Core Functions + +### 1. `runAgentLoop()` + +**Purpose**: Start a new agent run with initial prompt messages. + +```typescript +async function runAgentLoop( + prompts: AgentMessage[], + context: AgentContext, + config: AgentLoopConfig, + emit: AgentEventSink, + signal: AbortSignal | undefined, + streamFn: StreamFn, +): Promise +``` + +**Flow**: +``` +1. Create newMessages = [...prompts] +2. Append prompts to context.messages +3. Emit: agent_start +4. Emit: turn_start +5. For each prompt: + - Emit: message_start + - Emit: message_end +6. Call: runLoop() - main iteration logic +7. Return: newMessages +``` + +### 2. `runAgentLoopContinue()` + +**Purpose**: Continue from existing context (no new prompts). + +```typescript +async function runAgentLoopContinue( + context: AgentContext, + config: AgentLoopConfig, + emit: AgentEventSink, + signal: AbortSignal | undefined, + streamFn: StreamFn, +): Promise +``` + +**Constraints**: +- Last message must convert to `user` or `toolResult` +- Throws if context is empty or last message is `assistant` + +**Flow**: +``` +1. Validate context (non-empty, last message is not assistant) +2. Create newMessages = [] (empty - we continue) +3. Emit: agent_start +4. Emit: turn_start +5. Call: runLoop() +6. Return: newMessages +``` + +### 3. `runLoop()` - The Heart of the Agent + +**Purpose**: Main iteration loop that drives conversation. + +```typescript +async function runLoop( + initialContext: AgentContext, + newMessages: AgentMessage[], + initialConfig: AgentLoopConfig, + signal: AbortSignal | undefined, + emit: AgentEventSink, + streamFunction: StreamFn, +): Promise +``` + +**Structure**: + +```typescript +async function runLoop(...) { + let currentContext = initialContext; + let config = initialConfig; + let firstTurn = true; + let pendingMessages: AgentMessage[] = []; + + // OUTER LOOP: Handles follow-up messages + while (true) { + let hasMoreToolCalls = true; + + // INNER LOOP: Handles tool calls and steering + while (hasMoreToolCalls || pendingMessages.length > 0) { + if (!firstTurn) { + await emit({ type: "turn_start" }); + } else { + firstTurn = false; + } + + // 1. Process pending messages (steering/follow-up) + if (pendingMessages.length > 0) { + for (const message of pendingMessages) { + await emit({ type: "message_start", message }); + await emit({ type: "message_end", message }); + currentContext.messages.push(message); + newMessages.push(message); + } + pendingMessages = []; + } + + // 2. Stream assistant response + const message = await streamAssistantResponse(...); + newMessages.push(message); + + // 3. Check for errors + if (message.stopReason === "error" || message.stopReason === "aborted") { + await emit({ type: "turn_end", message, toolResults: [] }); + await emit({ type: "agent_end", messages: newMessages }); + return; + } + + // 4. Execute tool calls + const toolCalls = message.content.filter(c => c.type === "toolCall"); + const toolResults: ToolResultMessage[] = []; + hasMoreToolCalls = false; + + if (toolCalls.length > 0) { + const executedBatch = await executeToolCalls(...); + toolResults.push(...executedBatch.messages); + hasMoreToolCalls = !executedBatch.terminate; + + for (const result of toolResults) { + currentContext.messages.push(result); + newMessages.push(result); + } + } + + // 5. Emit turn_end + await emit({ type: "turn_end", message, toolResults }); + + // 6. Prepare next turn + const nextTurnContext = { message, toolResults, context, newMessages }; + const nextTurnSnapshot = await config.prepareNextTurn?.(nextTurnContext); + + if (nextTurnSnapshot) { + currentContext = nextTurnSnapshot.context ?? currentContext; + config = { ...config, model: nextTurnSnapshot.model }; + } + + // 7. Check termination + if (await config.shouldStopAfterTurn?.(...)) { + await emit({ type: "agent_end", messages: newMessages }); + return; + } + + // 8. Drain steering queue + pendingMessages = (await config.getSteeringMessages?.()) || []; + } + + // Outer loop: Check for follow-up messages + const followUpMessages = (await config.getFollowUpMessages?.()) || []; + if (followUpMessages.length > 0) { + pendingMessages = followUpMessages; + continue; // Back to inner loop + } + + // No more messages - exit + break; + } + + await emit({ type: "agent_end", messages: newMessages }); +} +``` + +--- + +## Message Streaming + +### `streamAssistantResponse()` + +**Purpose**: Stream assistant response from LLM provider. + +```typescript +async function streamAssistantResponse( + context: AgentContext, + config: AgentLoopConfig, + signal: AbortSignal | undefined, + emit: AgentEventSink, + streamFunction: StreamFn, +): Promise +``` + +**Flow**: + +``` +1. Apply transformContext() if configured + ├─► messages = await config.transformContext(messages) + └─► Returns new AgentMessage[] + +2. Convert to LLM format + ├─► llmMessages = await config.convertToLlm(messages) + └─► Returns Message[] (filters custom messages) + +3. Build LLM Context + Context = { + systemPrompt: context.systemPrompt, + messages: llmMessages, + tools: context.tools + } + +4. Resolve API key + ├─► Get key from getApiKey() hook + └─► Fallback to config.apiKey + +5. Call streamFn() + ├─► StreamFn(model, context, options) + └─► Returns AssistantMessageEventStream + +6. Process stream events + for await (const event of response) { + switch (event.type) { + case "start": + // Initialize partial message + partialMessage = event.partial + context.messages.push(partialMessage) + emit({ type: "message_start", message }) + + case "text_start" | "text_delta" | "text_end": + case "thinking_start" | "thinking_delta" | "thinking_end": + case "toolcall_start" | "toolcall_delta" | "toolcall_end": + // Update partial message + partialMessage = event.partial + emit({ type: "message_update", ... }) + + case "done" | "error": + const finalMessage = await response.result() + emit({ type: "message_end", message }) + return finalMessage + } + } +``` + +--- + +## Tool Execution + +### Sequential vs Parallel + +**Sequential Mode**: +- Each tool call prepared, executed, finalized before next +- Emit `tool_execution_end` immediately after each +- Tool results in source order + +**Parallel Mode**: +- All tool calls prepared sequentially +- Allowed tools execute concurrently +- Emit `tool_execution_end` in completion order +- Tool results in source order + +### `executeToolCalls()` + +```typescript +async function executeToolCalls(...): Promise { + const toolCalls = assistantMessage.content.filter(c => c.type === "toolCall"); + + // Check if any tool requires sequential execution + const hasSequentialToolCall = toolCalls.some(tc => { + const tool = currentContext.tools?.find(t => t.name === tc.name); + return tool?.executionMode === "sequential"; + }); + + if (config.toolExecution === "sequential" || hasSequentialToolCall) { + return executeToolCallsSequential(...); + } + + return executeToolCallsParallel(...); +} +``` + +### `executeToolCallsSequential()` + +```typescript +async function executeToolCallsSequential(...): Promise { + const finalizedCalls: FinalizedToolCallOutcome[] = []; + const messages: ToolResultMessage[] = []; + + for (const toolCall of toolCalls) { + // 1. Prepare + const preparation = await prepareToolCall(...); + + let finalized: FinalizedToolCallOutcome; + if (preparation.kind === "immediate") { + // Validation/permission hook blocked execution + finalized = { toolCall, result: preparation.result, isError: preparation.isError }; + } else { + // Execute + const executed = await executePreparedToolCall(preparation, signal, emit); + finalized = await finalizeExecutedToolCall(...); + } + + // 2. Emit + await emitToolExecutionEnd(finalized, emit); + const toolResultMessage = createToolResultMessage(finalized); + await emitToolResultMessage(toolResultMessage, emit); + + finalizedCalls.push(finalized); + messages.push(toolResultMessage); + + if (signal?.aborted) break; + } + + return { + messages, + terminate: shouldTerminateToolBatch(finalizedCalls) + }; +} +``` + +### `executeToolCallsParallel()` + +```typescript +async function executeToolCallsParallel(...): Promise { + const finalizedCalls: FinalizedToolCallEntry[] = []; + + // Phase 1: Prepare all tool calls + for (const toolCall of toolCalls) { + const preparation = await prepareToolCall(...); + + if (preparation.kind === "immediate") { + // Blocked or error - execute immediately + const finalized = { + toolCall, + result: preparation.result, + isError: preparation.isError + }; + await emitToolExecutionEnd(finalized, emit); + finalizedCalls.push(finalized); + } else { + // Schedule for concurrent execution + finalizedCalls.push(async () => { + const executed = await executePreparedToolCall(preparation, signal, emit); + const finalized = await finalizeExecutedToolCall(...); + await emitToolExecutionEnd(finalized, emit); + return finalized; + }); + } + + if (signal?.aborted) break; + } + + // Phase 2: Execute concurrent tools and collect results + const orderedFinalizedCalls = await Promise.all( + finalizedCalls.map(entry => typeof entry === "function" ? entry() : Promise.resolve(entry)) + ); + + // Phase 3: Emit tool result messages in source order + const messages: ToolResultMessage[] = []; + for (const finalized of orderedFinalizedCalls) { + const toolResultMessage = createToolResultMessage(finalized); + await emitToolResultMessage(toolResultMessage, emit); + messages.push(toolResultMessage); + } + + return { + messages, + terminate: shouldTerminateToolBatch(orderedFinalizedCalls) + }; +} +``` + +--- + +## Tool Preparation Flow + +### `prepareToolCall()` + +```typescript +async function prepareToolCall(...): Promise { + // 1. Find tool + const tool = currentContext.tools?.find(t => t.name === toolCall.name); + if (!tool) { + return { + kind: "immediate", + result: createErrorToolResult(`Tool ${toolCall.name} not found`), + isError: true + }; + } + + try { + // 2. Prepare arguments (optional shim) + const preparedToolCall = prepareToolCallArguments(tool, toolCall); + + // 3. Validate arguments + const validatedArgs = validateToolArguments(tool, preparedToolCall); + + // 4. beforeToolCall hook + if (config.beforeToolCall) { + const beforeResult = await config.beforeToolCall( + { assistantMessage, toolCall, args: validatedArgs, context: currentContext }, + signal + ); + + if (signal?.aborted) { + return immediateError("Operation aborted"); + } + + if (beforeResult?.block) { + return { + kind: "immediate", + result: createErrorToolResult(beforeResult.reason || "Tool execution was blocked"), + isError: true + }; + } + } + + if (signal?.aborted) { + return immediateError("Operation aborted"); + } + + // 5. Return prepared call for execution + return { + kind: "prepared", + toolCall, + tool, + args: validatedArgs + }; + } catch (error) { + return { + kind: "immediate", + result: createErrorToolResult(error.message), + isError: true + }; + } +} +``` + +--- + +## Tool Execution Flow + +### `executePreparedToolCall()` + +```typescript +async function executePreparedToolCall( + prepared: PreparedToolCall, + signal: AbortSignal | undefined, + emit: AgentEventSink, +): Promise { + const updateEvents: Promise[] = []; + let acceptingUpdates = true; + + try { + // Call tool.execute() with onUpdate callback + const result = await prepared.tool.execute( + prepared.toolCall.id, + prepared.args, + signal, + (partialResult) => { + if (!acceptingUpdates) return; + + // Buffer update events to emit in order + updateEvents.push( + Promise.resolve( + emit({ + type: "tool_execution_update", + toolCallId: prepared.toolCall.id, + toolName: prepared.toolCall.name, + args: prepared.toolCall.arguments, + partialResult + }) + ) + ); + } + ); + + acceptingUpdates = false; + await Promise.all(updateEvents); // Wait for all updates to flush + return { result, isError: false }; + } catch (error) { + acceptingUpdates = false; + await Promise.all(updateEvents); + return { + result: createErrorToolResult(error.message), + isError: true + }; + } finally { + acceptingUpdates = false; + } +} +``` + +--- + +## Tool Finalization Flow + +### `finalizeExecutedToolCall()` + +```typescript +async function finalizeExecutedToolCall( + currentContext: AgentContext, + assistantMessage: AssistantMessage, + prepared: PreparedToolCall, + executed: ExecutedToolCallOutcome, + config: AgentLoopConfig, + signal: AbortSignal | undefined, +): Promise { + let result = executed.result; + let isError = executed.isError; + + // afterToolCall hook - can override result + if (config.afterToolCall) { + try { + const afterResult = await config.afterToolCall( + { + assistantMessage, + toolCall: prepared.toolCall, + args: prepared.args, + result, + isError, + context: currentContext + }, + signal + ); + + if (afterResult) { + // Field-by-field override (no deep merge) + result = { + ...result, + content: afterResult.content ?? result.content, + details: afterResult.details ?? result.details, + usage: afterResult.usage ?? result.usage, + terminate: afterResult.terminate ?? result.terminate, + }; + isError = afterResult.isError ?? isError; + } + } catch (error) { + result = createErrorToolResult(error.message); + isError = true; + } + } + + return { + toolCall: prepared.toolCall, + result, + isError + }; +} +``` + +--- + +## Termination Logic + +### `shouldTerminateToolBatch()` + +```typescript +function shouldTerminateToolBatch(finalizedCalls: FinalizedToolCallOutcome[]): boolean { + return finalizedCalls.length > 0 && + finalizedCalls.every(f => f.result.terminate === true); +} +``` + +**Key Points**: +- Only terminates if **ALL** tool calls set `terminate: true` +- Allows partial tool execution while signaling early termination + +### `shouldStopAfterTurn()` + +Called after `turn_end`, before checking steering/follow-up queues: + +```typescript +if (await config.shouldStopAfterTurn?.({ + message, + toolResults, + context: currentContext, + newMessages +})) { + await emit({ type: "agent_end", messages: newMessages }); + return; +} +``` + +**Common use cases**: +- Stop before context gets too large +- Stop after completing a specific goal +- Stop on error + +--- + +## Queue Management + +### Steering Queue + +**Purpose**: Interrupt agent while it's working. + +**When drained**: After each turn ends, before next LLM call. + +**Mode**: `"all"` or `"one-at-a-time"` + +```typescript +// Example: Steer agent mid-execution +agent.steer("Wait, let me check something else first"); +agent.steer("Also, use a different approach"); +``` + +### Follow-up Queue + +**Purpose**: Queue messages for after agent would naturally stop. + +**When drained**: When agent has no more tool calls and no steering messages. + +**Mode**: `"all"` or `"one-at-a-time"` + +```typescript +// Example: Follow up after agent finishes +agent.followUp("Now summarize what you did"); +agent.followUp("What's next?"); +``` + +--- + +## Error Handling + +### Truncated Tool Calls + +```typescript +async function failToolCallsFromTruncatedMessage( + toolCalls: AgentToolCall[], + emit: AgentEventSink +): Promise { + // All tool calls from truncated assistant message fail + // Reason: tool call arguments may be incomplete + + for (const toolCall of toolCalls) { + await emit({ type: "tool_execution_start", ... }); + await emit({ + type: "tool_execution_end", + toolCallId: toolCall.id, + toolName: toolCall.name, + result: createErrorToolResult( + `Tool call was not executed: response hit output token limit, arguments may be truncated.` + ), + isError: true + }); + } + + return { messages: [], terminate: false }; +} +``` + +--- + +## Abort Handling + +All async operations respect the abort signal: + +```typescript +// In prepareToolCall +if (signal?.aborted) { + return immediateError("Operation aborted"); +} + +// In executePreparedToolCall +const result = await tool.execute(id, args, signal, onUpdate); +// Tool can check signal.aborted and cancel long-running operations + +// In streamAssistantResponse +for await (const event of response) { + if (signal?.aborted) { + throw new Error("Aborted"); + } + // Process event +} +``` + +--- + +## Summary + +The agent loop is a **two-level iterator**: + +1. **Outer loop**: Handles follow-up messages after agent would stop +2. **Inner loop**: Handles tool calls and steering messages + +Each iteration: +- Streams assistant response (LLM) +- Executes tool calls (sequential or parallel) +- Emits events for UI updates +- Updates context with new messages + +The loop terminates when: +- `shouldStopAfterTurn()` returns true +- Error or abort occurs +- No more steering/follow-up messages diff --git a/packages/agent/learning/03-HOOK-SYSTEM.md b/packages/agent/learning/03-HOOK-SYSTEM.md new file mode 100644 index 00000000..229734e8 --- /dev/null +++ b/packages/agent/learning/03-HOOK-SYSTEM.md @@ -0,0 +1,792 @@ +# Hook System Reference + +## Overview + +The hook system provides **extensibility points** at both the Agent and AgentHarness layers. Hooks are asynchronous, can be cancelled via abort signal, and run in subscription order. + +--- + +## Hook Categories + +### 1. Message Transformation Hooks + +#### `convertToLlm` + +**Location**: `AgentLoopConfig.convertToLlm` + +**Purpose**: Convert `AgentMessage[]` to `Message[]` before LLM call. + +**When called**: Just before each LLM request. + +**Key contract**: +- Must not throw or reject +- Must handle all `AgentMessage` variants +- Filter out UI-only messages (notifications, artifacts, etc.) + +**Example**: +```typescript +convertToLlm: (messages) => messages.filter(m => + m.role === "user" || + m.role === "assistant" || + m.role === "toolResult" +) +``` + +#### `transformContext` + +**Location**: `AgentLoopConfig.transformContext` (optional) + +**Purpose**: Manipulate context before LLM conversion. + +**When called**: Before `convertToLlm`. + +**Use cases**: +- Context window management (pruning old messages) +- Injecting external context +- Message deduplication + +**Example**: +```typescript +transformContext: async (messages, signal) => { + if (estimateTokens(messages) > MAX_TOKENS) { + return pruneOldMessages(messages); + } + return messages; +} +``` + +--- + +### 2. Lifecycle Hooks + +#### `beforeToolCall` + +**Location**: `AgentLoopConfig.beforeToolCall` (optional) + +**Context**: +```typescript +interface BeforeToolCallContext { + assistantMessage: AssistantMessage; + toolCall: AgentToolCall; + args: unknown; // Validated against tool schema + context: AgentContext; +} +``` + +**Return**: +```typescript +interface BeforeToolCallResult { + block?: boolean; // If true, tool won't execute + reason?: string; // Error message shown in tool result +} +``` + +**When called**: After args validation, before tool execution. + +**Use cases**: +- Permission checks (user approval) +- Rate limiting +- Context-aware tool blocking + +**Example**: +```typescript +beforeToolCall: async ({ toolCall, args, context }, signal) => { + if (toolCall.name === "bash" && signal?.aborted) { + return { block: true, reason: "Operation aborted" }; + } + return undefined; // Allow execution +} +``` + +#### `afterToolCall` + +**Location**: `AgentLoopConfig.afterToolCall` (optional) + +**Context**: +```typescript +interface AfterToolCallContext { + assistantMessage: AssistantMessage; + toolCall: AgentToolCall; + args: unknown; + result: AgentToolResult; + isError: boolean; + context: AgentContext; +} +``` + +**Return**: +```typescript +interface AfterToolCallResult { + content?: (TextContent | ImageContent)[]; + details?: unknown; + isError?: boolean; + usage?: Usage; + terminate?: boolean; // Early termination hint +} +``` + +**When called**: After tool execution, before emitting `tool_execution_end`. + +**Use cases**: +- Modify tool results (redact sensitive data) +- Update usage tracking +- Trigger early termination + +**Example**: +```typescript +afterToolCall: async ({ result }, signal) => { + // Redact sensitive content + const content = result.content.map(c => { + if (c.type === "text") { + return { ...c, text: redactSecrets(c.text) }; + } + return c; + }); + + return { content }; +} +``` + +#### `shouldStopAfterTurn` + +**Location**: `AgentLoopConfig.shouldStopAfterTurn` (optional) + +**Context**: +```typescript +interface ShouldStopAfterTurnContext { + message: AssistantMessage; + toolResults: ToolResultMessage[]; + context: AgentContext; + newMessages: AgentMessage[]; +} +``` + +**Return**: `boolean` + +**When called**: After `turn_end`, before draining steering/follow-up queues. + +**Use cases**: +- Stop when goal achieved +- Stop before context gets too large +- Error recovery + +**Example**: +```typescript +shouldStopAfterTurn: async ({ message, toolResults, context }) => { + // Stop if model indicates task complete + if (message.content.some(c => + c.type === "text" && c.text.includes("TASK_COMPLETE"))) { + return true; + } + + // Stop if context too large + if (estimateTokens(context.messages) > MAX_TOKENS * 0.8) { + return true; + } + + return false; +} +``` + +#### `prepareNextTurn` + +**Location**: `AgentLoopConfig.prepareNextTurn` (optional) + +**Context**: Same as `ShouldStopAfterTurnContext` + +**Return**: +```typescript +interface AgentLoopTurnUpdate { + context?: AgentContext; + model?: Model; + thinkingLevel?: ThinkingLevel; +} +``` + +**When called**: After `shouldStopAfterTurn`, if not stopping. + +**Use cases**: +- Update model based on conversation context +- Switch thinking level +- Inject new context + +**Example**: +```typescript +prepareNextTurn: async ({ message, toolResults, context }) => { + // Switch to higher reasoning for complex tasks + if (toolResults.length > 3) { + return { + thinkingLevel: "high" + }; + } + + return undefined; // Keep current config +} +``` + +--- + +### 3. Queue Draining Hooks + +#### `getSteeringMessages` + +**Location**: `AgentLoopConfig.getSteeringMessages` (optional) + +**Return**: `Promise` + +**When called**: After turn ends, before next LLM call. + +**Purpose**: Inject messages to interrupt agent mid-workflow. + +**Mode**: `"all"` or `"one-at-a-time"` (controls how many messages injected) + +**Example**: +```typescript +getSteeringMessages: async () => { + // Check for user input while agent is working + if (userQueue.length > 0) { + return userQueue.splice(0, 1); // one-at-a-time mode + } + return []; +} +``` + +#### `getFollowUpMessages` + +**Location**: `AgentLoopConfig.getFollowUpMessages` (optional) + +**Return**: `Promise` + +**When called**: When agent would stop (no more tool calls, no steering messages). + +**Purpose**: Queue messages for after agent finishes. + +**Mode**: `"all"` or `"one-at-a-time"` + +**Example**: +```typescript +getFollowUpMessages: async () => { + // Check if user typed while agent was working + if (followUpQueue.length > 0) { + return followUpQueue.splice(0, 1); + } + return []; +} +``` + +--- + +## AgentHarness Hooks + +### 1. System Prompt Hooks + +#### `before_agent_start` + +**Location**: `AgentHarness.on("before_agent_start")` + +**Event**: +```typescript +{ + type: "before_agent_start"; + prompt: string; + images?: ImageContent[]; + systemPrompt: string; + resources: AgentHarnessResources; +} +``` + +**Return**: +```typescript +{ + messages?: AgentMessage[]; + systemPrompt?: string; +} +``` + +**When called**: Before agent starts, after system prompt generated. + +**Use cases**: +- Add conversation hints +- Inject images +- Modify system prompt + +### 2. Context Hooks + +#### `context` + +**Location**: `AgentHarness.on("context")` + +**Event**: +```typescript +{ + type: "context"; + messages: AgentMessage[]; +} +``` + +**Return**: +```typescript +{ + messages: AgentMessage[]; +} +``` + +**When called**: Before `convertToLlm`. + +**Use cases**: +- Message filtering +- Context window management +- Message augmentation + +### 3. Tool Hooks + +#### `tool_call` + +**Location**: `AgentHarness.on("tool_call")` + +**Event**: +```typescript +{ + type: "tool_call"; + toolCallId: string; + toolName: string; + input: Record; +} +``` + +**Return**: +```typescript +{ + block?: boolean; + reason?: string; +} +``` + +**When called**: Before tool execution. + +**Use cases**: +- Audit logging +- Approval workflows +- Input validation + +#### `tool_result` + +**Location**: `AgentHarness.on("tool_result")` + +**Event**: +```typescript +{ + type: "tool_result"; + toolCallId: string; + toolName: string; + input: Record; + content: (TextContent | ImageContent)[]; + details: unknown; + isError: boolean; + usage?: Usage; +} +``` + +**Return**: +```typescript +{ + content?: (TextContent | ImageContent)[]; + details?: unknown; + isError?: boolean; + usage?: Usage; + terminate?: boolean; +} +``` + +**When called**: After tool execution. + +**Use cases**: +- Result transformation +- Usage tracking +- Early termination + +### 4. Session Hooks + +#### `session_before_compact` + +**Location**: `AgentHarness.on("session_before_compact")` + +**Event**: +```typescript +{ + type: "session_before_compact"; + preparation: BranchPreparation; + branchEntries: SessionTreeEntry[]; + customInstructions?: string; + signal: AbortSignal; +} +``` + +**Return**: +```typescript +{ + cancel?: boolean; + compaction?: CompactionResult; +} +``` + +**When called**: Before compaction. + +**Use cases**: +- Skip compaction in certain conditions +- Provide custom summary +- Abort compaction + +#### `session_before_tree` + +**Location**: `AgentHarness.on("session_before_tree")` + +**Event**: +```typescript +{ + type: "session_before_tree"; + preparation: { + targetId: string; + oldLeafId: string; + commonAncestorId: string; + entriesToSummarize: SessionTreeEntry[]; + userWantsSummary: boolean; + customInstructions?: string; + replaceInstructions?: boolean; + label?: string; + }; + signal: AbortSignal; +} +``` + +**Return**: +```typescript +{ + cancel?: boolean; + summary?: { + summary: string; + details?: unknown; + usage?: Usage; + }; + customInstructions?: string; + replaceInstructions?: boolean; +} +``` + +**When called**: Before tree navigation (branching). + +**Use cases**: +- Skip branch summary +- Provide custom summary +- Cancel navigation + +### 5. Provider Hooks + +#### `before_provider_request` + +**Location**: `AgentHarness.on("before_provider_request")` + +**Event**: +```typescript +{ + type: "before_provider_request"; + model: Model; + sessionId: string; + streamOptions: AgentHarnessStreamOptions; +} +``` + +**Return**: +```typescript +{ + streamOptions: AgentHarnessStreamOptionsPatch; +} +``` + +**When called**: Just before each LLM request. + +**Use cases**: +- Add authentication headers +- Set request metadata +- Configure caching + +#### `before_provider_payload` + +**Location**: `AgentHarness.on("before_provider_payload")` + +**Event**: +```typescript +{ + type: "before_provider_payload"; + model: Model; + payload: unknown; +} +``` + +**Return**: +```typescript +{ + payload: unknown; +} +``` + +**When called**: Just before sending payload to LLM. + +**Use cases**: +- Payload transformation +- Debug logging +- Schema validation + +--- + +## Hook Execution Order + +### Full Turn Flow + +``` +1. AgentHarness.prompt() + │ + ├─► emit "before_agent_start" + │ └─► Hook can return new messages/systemPrompt + │ + ▼ +2. AgentLoopConfig creation + │ + ├─► transformContext hook → AgentLoop.transformContext + ├─► convertToLlm hook → AgentLoop.convertToLlm + ├─► beforeToolCall hook → AgentLoop.beforeToolCall + ├─► afterToolCall hook → AgentLoop.afterToolCall + ├─► prepareNextTurn hook → AgentLoop.prepareNextTurn + ├─► shouldStopAfterTurn hook → AgentLoop.shouldStopAfterTurn + ├─► getSteeringMessages hook → AgentLoop.getSteeringMessages + └─► getFollowUpMessages hook → AgentLoop.getFollowUpMessages + │ + ▼ +3. streamAssistantResponse() + │ + ├─► emit "before_provider_request" (harness) + │ └─► Hook can modify stream options + ├─► transformContext() (agent) + ├─► convertToLlm() (agent) + ├─► streamFn() → LLM call + └─► Emit message_start/update/end events + │ + ▼ +4. executeToolCalls() + │ + ├─► For each tool call: + │ ├─► emit "tool_call" (harness) + │ │ └─► Hook can block execution + │ ├─► tool.execute() + │ └─► emit "tool_result" (harness) + │ └─► Hook can override result + │ + ▼ +5. turn_end + │ + ├─► emit "turn_end" (agent) + ├─► shouldStopAfterTurn() (agent) + │ └─► Return true to exit + ├─► prepareNextTurn() (agent) + │ └─► Hook can update context/model/thinkingLevel + ├─► Drain steering queue + └─► Drain follow-up queue + │ + ├─► If steering/follow-up: repeat from #3 + └─► If no more: agent_end + │ + └─► emit "agent_end" (agent) +``` + +--- + +## Queue Mode Behavior + +### `"all"` Mode + +All queued messages are injected at once: + +``` +Agent would continue... + → getFollowUpMessages returns [msg1, msg2, msg3] + → All three injected together + → Agent processes all before next turn +``` + +### `"one-at-a-time"` Mode + +One message injected at a time: + +``` +Agent would continue... + → getFollowUpMessages returns [msg1] + → msg1 injected + → Agent processes msg1 + → After turn, getFollowUpMessages returns [msg2] + → msg2 injected + → Agent processes msg2 + → ...and so on +``` + +--- + +## Abort Signal Propagation + +All hooks receive an optional `AbortSignal`: + +```typescript +interface BeforeToolCallContext { + // ... other fields + // signal is NOT included - use agent.signal instead +} +``` + +**Agent hooks**: +- `transformContext`: receives `signal` +- `beforeToolCall`: receives `signal` +- `afterToolCall`: receives `signal` + +**Harness hooks**: +- `before_agent_start`: receives `signal` +- `context`: NO signal +- `tool_call`: NO signal +- `tool_result`: NO signal +- `session_before_compact`: receives `signal` +- `session_before_tree`: receives `signal` +- `before_provider_request`: receives `signal` +- `before_provider_payload`: NO signal + +--- + +## Error Handling + +### Hook Errors + +**Agent layer**: Hook errors are caught and encoded in tool results: + +```typescript +try { + const beforeResult = await config.beforeToolCall(...); + if (beforeResult?.block) { + return immediateError(beforeResult.reason); + } +} catch (error) { + return immediateError(error.message); +} +``` + +**Harness layer**: Hook errors are wrapped and re-thrown: + +```typescript +try { + const result = await handler(event); +} catch (error) { + throw normalizeHookError(error); +} +``` + +### Best Practices + +1. **Always handle errors**: Wrap async operations in try/catch +2. **Respect abort signals**: Check `signal.aborted` in long operations +3. **Return safe defaults**: Return empty arrays/objects on errors +4. **Don't block**: Hooks should be fast (no network calls) +5. **Idempotent**: Hooks should be safe to run multiple times + +--- + +## Common Patterns + +### 1. Context Window Management + +```typescript +transformContext: async (messages, signal) => { + if (signal?.aborted) return messages; + + const tokenCount = estimateTokens(messages); + if (tokenCount > MAX_TOKENS * 0.9) { + return pruneOldestMessages(messages, Math.floor(MAX_TOKENS * 0.3)); + } + return messages; +} +``` + +### 2. Permission-Gated Tools + +```typescript +beforeToolCall: async ({ toolCall, args }, signal) => { + if (toolCall.name === "bash" && signal?.aborted) { + return { block: true, reason: "Operation aborted" }; + } + + if (toolCall.name === "bash" && !await canExecuteBash(args)) { + return { block: true, reason: "Permission denied" }; + } + + return undefined; +} +``` + +### 3. Result Redaction + +```typescript +afterToolCall: async ({ result }) => { + const content = result.content.map(c => { + if (c.type === "text") { + return { ...c, text: redactSecrets(c.text) }; + } + return c; + }); + + return { content }; +} +``` + +### 4. Early Termination + +```typescript +shouldStopAfterTurn: async ({ message }) => { + // Check if model indicates completion + if (message.content.some(c => + c.type === "text" && c.text.includes("TASK_COMPLETE"))) { + return true; + } + + // Check if all tool calls set terminate + return false; +} +``` + +### 5. Audit Logging + +```typescript +tool_call: async ({ toolCallId, toolName, input }) => { + console.log(`[TOOL_CALL] ${toolName} (${toolCallId}):`, input); + return undefined; +} + +tool_result: async ({ toolCallId, toolName, content, isError }) => { + console.log(`[TOOL_RESULT] ${toolName} (${toolCallId}):`, { + hasError: isError, + contentLength: content.length + }); + return undefined; +} +``` + +--- + +## Summary + +| Hook | Layer | When | Can Block? | +|------|-------|------|------------| +| `convertToLlm` | Agent | Before LLM call | No (sync) | +| `transformContext` | Agent | Before `convertToLlm` | Yes (async) | +| `beforeToolCall` | Agent | After validation | Yes (async) | +| `afterToolCall` | Agent | After execution | Yes (async) | +| `shouldStopAfterTurn` | Agent | After turn_end | Yes (async) | +| `prepareNextTurn` | Agent | Before next turn | Yes (async) | +| `getSteeringMessages` | Agent | After turn_end | Yes (async) | +| `getFollowUpMessages` | Agent | When agent would stop | Yes (async) | + +All hooks are **optional** and have sensible defaults. diff --git a/packages/agent/learning/04-SESSION-ARCHITECTURE.md b/packages/agent/learning/04-SESSION-ARCHITECTURE.md new file mode 100644 index 00000000..32f546df --- /dev/null +++ b/packages/agent/learning/04-SESSION-ARCHITECTURE.md @@ -0,0 +1,705 @@ +# Session Architecture + +## Overview + +The session system provides **persistent, branchable conversation history**. It's the storage layer that enables: + +- Conversation persistence across restarts +- Branching to earlier points in conversation +- Context window compaction +- Session tree navigation + +--- + +## Core Concepts + +### 1. SessionTreeEntry + +The fundamental unit of session history: + +```typescript +type SessionTreeEntry = + | MessageEntry + | ModelChangeEntry + | ThinkingLevelChangeEntry + | ActiveToolsChangeEntry + | CompactionEntry + | BranchSummaryEntry + | CustomEntry + | CustomMessageEntry + | LabelEntry + | LeafEntry + | SessionInfoEntry; +``` + +**Key properties**: +- `id`: Unique identifier (UUID v7) +- `parentId`: Points to parent entry (forms tree structure) +- `timestamp`: ISO 8601 string + +### 2. Tree Structure + +``` +Entry tree (simplified): + + root (parentId: null) + ├─► message (user #1) [id: 1] + │ └─► message (assistant #1) [id: 2] + │ └─► tool_result [id: 3] + │ └─► message (user #2) [id: 4] + │ └─► compaction [id: 5] ← New root for future + │ ├─► retained messages here + │ └─► message (assistant #2) [id: 6] + │ └─► message (user #3) [id: 7] + │ └─► leaf [id: 8] ← Current head + │ + └─► branch_summary [id: 9] ← Point where branch was created + └─► message (user #4) [id: 10] + └─► message (assistant #4) [id: 11] + └─► leaf [id: 12] +``` + +### 3. Context Building + +**Context** = Current state needed for LLM call: + +```typescript +interface SessionContext { + systemPrompt: string; + messages: AgentMessage[]; + thinkingLevel: ThinkingLevel; + model: { provider: string; modelId: string } | null; + activeToolNames: string[] | null; +} +``` + +**Building context** involves: +1. Tracing from leaf to root (path entries) +2. Applying transforms (compaction, etc.) +3. Projecting entries to messages +4. Deriving state (model, thinking level, active tools) + +--- + +## Session Storage Interface + +### `SessionStorage` + +```typescript +interface SessionStorage { + // Metadata + readonly id: string; + readonly metadata: TMetadata; + + // Entry operations + getLeafId(): Promise; + setLeafId(id: string): Promise; + getEntry(id: string): Promise; + getEntries(options?: SessionEntryCursorOptions): Promise; + getBranch(): Promise; + + // Write operations + appendEntry(entry: SessionTreeEntry): Promise; + + // Branch operations + fork(targetId: string): Promise; + delete(): Promise; + + // Cleanup + cleanup(): Promise; +} +``` + +### Built-in Implementations + +#### MemoryStorage + +```typescript +class MemoryStorage implements SessionStorage { + // In-memory storage using Map + // Good for: Testing, short-lived sessions + // Not good for: Persistence across runs +} +``` + +#### JSONLStorage + +```typescript +class JSONLStorage implements SessionStorage { + // File-based storage using JSONL format + // One file per entry: entries/{id}.json + // Metadata file: metadata.json + + // Good for: Development, local sessions + // Not good for: High-concurrency, production + + // File structure: + // session/ + // metadata.json + // entries/ + // {id1}.json + // {id2}.json + // ... +} +``` + +--- + +## Session Class + +### `Session` + +High-level session API built on storage: + +```typescript +class Session { + // Metadata + readonly id: string; + readonly storage: SessionStorage; + + // Read operations + getMetadata(): Promise; + getLeafId(): Promise; + getEntry(id: string): Promise; + getBranch(): Promise; + buildContext(options?: SessionContextBuildOptions): Promise; + + // Write operations + appendMessage(message: AgentMessage): Promise; + appendModelChange(provider: string, modelId: string): Promise; + appendThinkingLevelChange(thinkingLevel: ThinkingLevel): Promise; + appendActiveToolsChange(activeToolNames: string[]): Promise; + appendCompaction(...): Promise; + appendBranchSummary(...): Promise; + appendCustomEntry(customType: string, data: unknown): Promise; + appendCustomMessageEntry(...): Promise; + appendLabel(targetId: string, label: string): Promise; + appendSessionName(name: string): Promise; + + // Branch operations + fork(targetId: string): Promise; + delete(): Promise; +} +``` + +--- + +## Context Building Details + +### Path Tracing + +**Goal**: Get all entries from leaf to root. + +```typescript +async function getPathEntries(session: Session): Promise { + const path: SessionTreeEntry[] = []; + let currentId = await session.getLeafId(); + + while (currentId !== null) { + const entry = await session.getEntry(currentId); + if (!entry) break; + + path.unshift(entry); + currentId = entry.parentId; + } + + return path; +} +``` + +### Default Transform + +**Purpose**: Apply compaction logic to context. + +```typescript +function defaultContextEntryTransform( + pathEntries: readonly SessionTreeEntry[] +): SessionTreeEntry[] { + let compaction: CompactionEntry | null = null; + for (const entry of pathEntries) { + if (entry.type === "compaction") { + compaction = entry; + } + } + + if (!compaction) { + return [...pathEntries]; // No compaction + } + + // Compaction retains either: + // 1. All entries after compaction (retainedTail) + // 2. Entries from firstKeptEntryId to compaction (inclusive) + + const entries: SessionTreeEntry[] = [compaction]; + const compactionIdx = pathEntries.findIndex(e => e.id === compaction.id); + + if (compaction.retainedTail) { + // Include everything after compaction + for (let i = compactionIdx + 1; i < pathEntries.length; i++) { + entries.push(pathEntries[i]!); + } + return entries; + } + + if (compaction.firstKeptEntryId) { + // Include entries from firstKeptEntryId to compaction + let foundFirstKept = false; + for (let i = compactionIdx - 1; i >= 0; i--) { + const entry = pathEntries[i]!; + if (entry.id === compaction.firstKeptEntryId) foundFirstKept = true; + if (foundFirstKept) entries.unshift(entry); + } + } + + // Always include entries after compaction + for (let i = compactionIdx + 1; i < pathEntries.length; i++) { + entries.push(pathEntries[i]!); + } + + return entries; +} +``` + +### Entry to Message Projection + +```typescript +function sessionEntryToContextMessages( + entry: SessionTreeEntry, + index: number, + entries: readonly SessionTreeEntry[], + options: SessionContextBuildOptions = {} +): AgentMessage[] { + if (entry.type === "message") { + return [entry.message as AgentMessage]; + } + + if (entry.type === "custom_message") { + return [createCustomMessage(...)]; + } + + if (entry.type === "compaction") { + return [ + createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp), + ...(entry.retainedTail ?? []) + ]; + } + + if (entry.type === "branch_summary" && entry.summary) { + return [createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp)]; + } + + if (entry.type === "custom") { + // Custom entry projectors can convert to messages + return [...(options.entryProjectors?.[entry.customType]?.(entry, index, entries) ?? [])]; + } + + return []; // Skip other entry types +} +``` + +--- + +## Branching + +### What is Branching? + +Branching creates a **new session tree** from an existing one, starting at a specific point. + +**Example use case**: +``` +Original tree: + root → A → B → C → D (leaf) + +Branch at B: + root → A → B → B' (leaf) ← New branch + \ + → C → D (leaf) ← Original branch +``` + +### Fork Operation + +```typescript +async function fork(session: Session, targetId: string): Promise { + // 1. Clone storage (copy entries up to targetId) + const newStorage = await session.storage.fork(targetId); + + // 2. Create new session from storage + const newSession = new Session({ storage: newStorage }); + + // 3. Set leaf to targetId + await newSession.getStorage().setLeafId(targetId); + + return newSession; +} +``` + +### Branch Summary + +When branching, a **branch_summary** entry is created: + +```typescript +interface BranchSummaryEntry extends SessionTreeEntryBase { + type: "branch_summary"; + summary: string; // Human-readable summary + details?: unknown; // Implementation details + usage?: Usage; // LLM usage for generating summary + fromId: string; // Entry ID where branch was created +} +``` + +**Purpose**: Help model understand what happened in the branch. + +--- + +## Compaction + +### What is Compaction? + +Compaction replaces old conversation history with a **summary**, reducing context size. + +**Before compaction**: +``` +message (user #1) +message (assistant #1) +tool_result +message (user #2) +message (assistant #2) +tool_result +... (many more messages) +``` + +**After compaction**: +``` +compaction (summary: "User asked X, assistant did Y, then Z...") +message (assistant #3) ← Recent messages retained +message (user #3) +``` + +### Compaction Entry + +```typescript +interface CompactionEntry extends SessionTreeEntryBase { + type: "compaction"; + summary: string; // Summarized history + firstKeptEntryId?: string; // First entry kept after compaction + tokensBefore: number; // Context size before compaction + details?: CompactionDetails; // File operations, etc. + usage?: Usage; // LLM usage for generating summary + retainedTail?: AgentMessage[]; // Recent messages stored inline +} +``` + +### Compaction Process + +```typescript +async function compact(session: Session): Promise { + // 1. Get branch entries + const entries = await session.getBranch(); + + // 2. Prepare compaction + const preparation = prepareCompaction(entries, settings); + // Identifies which messages to summarize, retained tail, etc. + + // 3. Generate summary using LLM + const summary = await generateSummary( + preparation.messagesToSummarize, + preparation.retainedTail + ); + + // 4. Create compaction entry + const compactionEntry: CompactionEntry = { + type: "compaction", + id: uuidv7(), + parentId: preparation.firstKeptEntry.parentId, + timestamp: new Date().toISOString(), + summary: summary.text, + firstKeptEntryId: preparation.firstKeptEntry.id, + tokensBefore: preparation.tokensBefore, + details: { + readFiles: preparation.fileOps.readFiles, + modifiedFiles: preparation.fileOps.modifiedFiles + }, + usage: summary.usage + }; + + // 5. Persist entry + const compactionId = await session.storage.appendEntry(compactionEntry); + + return { + summary: summary.text, + firstKeptEntryId: preparation.firstKeptEntry.id, + tokensBefore: preparation.tokensBefore, + usage: summary.usage, + retainedTail: preparation.retainedTail, + details: compactionEntry.details + }; +} +``` + +--- + +## Session Repositories + +### `SessionRepo` + +Repository pattern for session management: + +```typescript +interface SessionRepo { + // CRUD + create(options: CreateSessionOptions): Promise>; + open(id: string): Promise>; + list(): Promise; + delete(id: string): Promise; + + // Forking + fork(id: string, targetId: string): Promise>; + + // Cleanup + cleanup(): Promise; +} +``` + +### Built-in Implementations + +#### MemoryRepo + +```typescript +class MemoryRepo implements SessionRepo { + // In-memory storage using Map> + // Good for: Testing, ephemeral sessions +} +``` + +#### JSONLRepo + +```typescript +class JSONLRepo implements SessionRepo { + // File-based storage + // Sessions stored in: sessions/{id}/ + // Good for: Local development +} +``` + +--- + +## Entry Types Reference + +### MessageEntry + +```typescript +interface MessageEntry extends SessionTreeEntryBase { + type: "message"; + message: AgentMessage; +} +``` + +**Stored**: Every user/assistant/toolResult message + +### ModelChangeEntry + +```typescript +interface ModelChangeEntry extends SessionTreeEntryBase { + type: "model_change"; + provider: string; + modelId: string; +} +``` + +**Stored**: When model is changed via `setModel()` + +### ThinkingLevelChangeEntry + +```typescript +interface ThinkingLevelChangeEntry extends SessionTreeEntryBase { + type: "thinking_level_change"; + thinkingLevel: ThinkingLevel; +} +``` + +**Stored**: When thinking level is changed via `setThinkingLevel()` + +### ActiveToolsChangeEntry + +```typescript +interface ActiveToolsChangeEntry extends SessionTreeEntryBase { + type: "active_tools_change"; + activeToolNames: string[]; +} +``` + +**Stored**: When active tools are changed via `setActiveTools()` + +### CompactionEntry + +```typescript +interface CompactionEntry extends SessionTreeEntryBase { + type: "compaction"; + summary: string; + firstKeptEntryId?: string; + tokensBefore: number; + details?: CompactionDetails; + usage?: Usage; + retainedTail?: AgentMessage[]; +} +``` + +**Stored**: After compaction + +### BranchSummaryEntry + +```typescript +interface BranchSummaryEntry extends SessionTreeEntryBase { + type: "branch_summary"; + summary: string; + details?: unknown; + usage?: Usage; + fromId: string; +} +``` + +**Stored**: When creating a branch + +### CustomEntry + +```typescript +interface CustomEntry extends SessionTreeEntryBase { + type: "custom"; + customType: string; + data: unknown; +} +``` + +**Stored**: Custom application data (not visible to model) + +### CustomMessageEntry + +```typescript +interface CustomMessageEntry extends SessionTreeEntryBase { + type: "custom_message"; + customType: string; + content: string | (TextContent | ImageContent)[]; + display: string; + details: unknown; +} +``` + +**Stored**: Custom messages that appear in conversation + +### LabelEntry + +```typescript +interface LabelEntry extends SessionTreeEntryBase { + type: "label"; + targetId: string; // Entry ID being labeled + label: string; +} +``` + +**Stored**: User-assigned labels for entries + +### LeafEntry + +```typescript +interface LeafEntry extends SessionTreeEntryBase { + type: "leaf"; + targetId: string; // Current leaf entry ID +} +``` + +**Stored**: Updates to current session head + +### SessionInfoEntry + +```typescript +interface SessionInfoEntry extends SessionTreeEntryBase { + type: "session_info"; + name: string; +} +``` + +**Stored**: Session name/description + +--- + +## Best Practices + +### 1. Use Branching for Experiments + +```typescript +// Original branch +await harness.prompt("Build a web app"); + +// Experiment branch +const experimentalSession = await session.fork(leafId); +const experimentalHarness = new AgentHarness({ + ...options, + session: experimentalSession +}); + +await experimentalHarness.prompt("Try using React instead"); +``` + +### 2. Compact Regularly + +```typescript +// After each turn, check if compaction needed +if (estimateTokens(context) > MAX_TOKENS * 0.8) { + await harness.compact(); +} +``` + +### 3. Use Custom Entries for Metadata + +```typescript +// Store application state without exposing to model +await harness.appendMessage({ + role: "custom", + type: "task_progress", + taskId: "abc123", + steps: [...] +}); + +// Custom entry won't appear in model context +``` + +### 4. Label Important Points + +```typescript +// Mark important conversation points +await harness.appendLabel(messageId, "IMPORTANT_DECISION"); +await harness.appendLabel(messageId, "BLOCKER"); +``` + +### 5. Handle Branching Gracefully + +```typescript +try { + await harness.navigateTree(targetId, { summarize: true }); +} catch (error) { + if (error instanceof AgentHarnessError && error.code === "branch_summary") { + // Branch summary failed, navigate without summary + await harness.navigateTree(targetId, { summarize: false }); + } +} +``` + +--- + +## Summary + +**Session architecture provides**: +- Persistent conversation history (JSONL storage) +- Branchable conversation trees +- Context window compaction +- Custom metadata and messages + +**Key operations**: +- `buildContext()` → Get LLM context from tree +- `appendMessage()` → Add message to tree +- `fork()` → Create branch at point +- `compact()` → Summarize history + +**Storage layers**: +- `MemoryStorage` → Testing, ephemeral +- `JSONLStorage` → Development, local diff --git a/packages/agent/learning/05-TOOL-EXECUTION.md b/packages/agent/learning/05-TOOL-EXECUTION.md new file mode 100644 index 00000000..d0049e1d --- /dev/null +++ b/packages/agent/learning/05-TOOL-EXECUTION.md @@ -0,0 +1,709 @@ +# Tool Execution Guide + +## Overview + +Tools are how the agent **interacts with the external world**. They can read files, execute commands, make API calls, or perform any action. + +--- + +## Tool Definition + +### Basic Structure + +```typescript +interface AgentTool extends Tool { + label: string; // Human-readable name for UI + prepareArguments?: (args: unknown) => Static; // Optional arg transformation + execute( + toolCallId: string, + params: Static, + signal?: AbortSignal, + onUpdate?: AgentToolUpdateCallback + ): Promise>; +} +``` + +### Tool Result + +```typescript +interface AgentToolResult { + content: (TextContent | ImageContent)[]; // Returned to model + details: T; // Arbitrary data for logs/UI + usage?: Usage; // Tool-specific usage (not for LLM context) + addedToolNames?: string[]; // New tools introduced + terminate?: boolean; // Early termination hint +} +``` + +--- + +## Tool Execution Flow + +``` +1. LLM sends tool call + └─► AssistantMessage with toolCall content block + +2. prepareToolCall() + ├─► Find tool by name + ├─► prepareArguments() [optional] + ├─► validateToolArguments() + └─► beforeToolCall() hook + ├─► Return {block: true} → Error tool result + └─► Continue + +3. executePreparedToolCall() + ├─► tool.execute() with onUpdate callback + └─► onUpdate(partialResult) → Emit tool_execution_update + +4. finalizeExecutedToolCall() + └─► afterToolCall() hook + └─► Override result fields + +5. Emit events + ├─► tool_execution_end + ├─► message_start (toolResult) + └─► message_end (toolResult) +``` + +--- + +## Built-in Tools + +### 1. Bash Tool + +**Purpose**: Execute shell commands. + +**Parameters**: +```typescript +interface BashToolInput { + command: string; +} +``` + +**Returns**: Command output as text. + +**Options**: +- `cwd`: Working directory +- `timeout`: Command timeout in seconds +- `maxStdoutLines`: Truncate stdout after N lines +- `maxStderrLines`: Truncate stderr after N lines + +**Example**: +```typescript +const bashTool = createBashTool({ + cwd: "/home/user/project", + timeout: 30, + maxStdoutLines: 1000, + maxStderrLines: 100 +}); + +await bashTool.execute( + "run_123", + { command: "ls -la" }, + undefined, + onUpdate +); + +// Result: +// { +// content: [{ type: "text", text: "drwxr-xr-x ... " }], +// details: { +// command: "ls -la", +// cwd: "/home/user/project", +// exitCode: 0, +// stdout: "...", +// stderr: "" +// } +// } +``` + +### 2. Read Tool + +**Purpose**: Read files (text or binary). + +**Parameters**: +```typescript +interface ReadToolInput { + path: string; + startLine?: number; // Optional line range + endLine?: number; +} +``` + +**Returns**: File contents as text or images (for image files). + +**Options**: +- `maxSize`: Maximum file size in bytes +- `maxLines`: Maximum lines for text files +- `maxTotalSize`: Maximum total bytes for multiple files +- `imageProcessor`: Custom image handler + +**Example**: +```typescript +const readTool = createReadTool({ + maxSize: 1024 * 1024, // 1MB + maxLines: 5000, + imageProcessor: async (buffer) => ({ + type: "text", + text: `Image of ${buffer.length} bytes` + }) +}); + +await readTool.execute( + "read_456", + { path: "src/app.ts", startLine: 1, endLine: 50 }, + undefined, + onUpdate +); + +// Result: +// { +// content: [{ type: "text", text: "import React from 'react';\n..." }], +// details: { path: "src/app.ts", linesRead: 50 } +// } +``` + +### 3. Write Tool + +**Purpose**: Write files (create or overwrite). + +**Parameters**: +```typescript +interface WriteToolInput { + path: string; + content: string; +} +``` + +**Returns**: Success/failure message. + +**Example**: +```typescript +const writeTool = createWriteTool(); + +await writeTool.execute( + "write_789", + { path: "src/app.ts", content: "console.log('Hello');" }, + undefined, + onUpdate +); + +// Result: +// { +// content: [{ type: "text", text: "✓ Wrote 25 bytes to src/app.ts" }], +// details: { path: "src/app.ts", bytesWritten: 25 } +// } +``` + +### 4. Edit Tool + +**Purpose**: Make precise edits to files using line numbers or search/replace. + +**Parameters**: +```typescript +interface EditToolInput { + path: string; + startLine: number; + endLine: number; + content: string; +} +``` + +**Returns**: Success/failure message with diff. + +**Example**: +```typescript +const editTool = createEditTool(); + +await editTool.execute( + "edit_101", + { path: "src/app.ts", startLine: 5, endLine: 10, content: "const x = 42;" }, + undefined, + onUpdate +); + +// Result: +// { +// content: [{ type: "text", text: "✓ Edited lines 5-10 in src/app.ts" }], +// details: { +// path: "src/app.ts", +// startLine: 5, +// endLine: 10, +// linesChanged: 6, +// diff: "- const x = 1\n+ const x = 42" +// } +// } +``` + +--- + +## Creating Custom Tools + +### Basic Custom Tool + +```typescript +const weatherTool: AgentTool = { + name: "get_weather", + label: "Get Weather", + description: "Get current weather for a city", + parameters: Type.Object({ + city: Type.String({ description: "City name" }) + }), + execute: async (toolCallId, params, signal, onUpdate) => { + try { + const response = await fetch( + `https://api.weather.com/v1/weather?city=${params.city}`, + { signal } + ); + + if (!response.ok) { + throw new Error(`Weather API error: ${response.status}`); + } + + const data = await response.json(); + + return { + content: [{ type: "text", text: `Temperature: ${data.temp}°C` }], + details: { + city: params.city, + temp: data.temp, + humidity: data.humidity, + condition: data.condition + }, + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } + } + }; + } catch (error) { + if (error instanceof Error && error.name === "AbortError") { + throw error; // Re-throw abort + } + return { + content: [{ type: "text", text: `Error: ${error.message}` }], + details: { error: error.message }, + isError: true + }; + } + } +}; +``` + +### Tool with Streaming Updates + +```typescript +const backupTool: AgentTool = { + name: "backup_database", + label: "Backup Database", + description: "Create database backup with progress updates", + parameters: Type.Object({ + database: Type.String(), + destination: Type.String() + }), + execute: async (toolCallId, params, signal, onUpdate) => { + const totalSize = await getDatabaseSize(params.database); + let uploaded = 0; + + const stream = createBackupStream(params.database); + + for await (const chunk of stream) { + uploaded += chunk.length; + + // Stream progress updates + onUpdate({ + content: [{ + type: "text", + text: `Backup progress: ${(uploaded / totalSize * 100).toFixed(1)}%` + }], + details: { uploaded, total: totalSize } + }); + + if (signal?.aborted) { + throw new Error("Backup cancelled"); + } + } + + await uploadToStorage(stream, params.destination); + + return { + content: [{ type: "text", text: "Backup completed successfully" }], + details: { + database: params.database, + destination: params.destination, + size: uploaded, + duration: Date.now() - startTime + } + }; + } +}; +``` + +### Tool with Custom Error Handling + +```typescript +const apiTool: AgentTool = { + name: "make_api_call", + label: "Make API Call", + description: "Make HTTP request to external API", + parameters: Type.Object({ + url: Type.String({ format: "uri" }), + method: Type.Optional(Type.String({ enum: ["GET", "POST", "PUT", "DELETE"] })), + headers: Type.Optional(Type.Record(Type.String(), Type.String())), + body: Type.Optional(Type.String()) + }), + execute: async (toolCallId, params, signal, onUpdate) => { + try { + const response = await fetch(params.url, { + method: params.method || "GET", + headers: params.headers, + body: params.body, + signal + }); + + // Handle HTTP errors + if (!response.ok) { + const errorBody = await response.text(); + return { + content: [{ + type: "text", + text: `HTTP ${response.status}: ${response.statusText}\n${errorBody}` + }], + details: { + url: params.url, + method: params.method, + statusCode: response.status, + body: errorBody + }, + isError: true + }; + } + + const contentType = response.headers.get("content-type") || ""; + let responseText = await response.text(); + + // Handle JSON responses + if (contentType.includes("application/json")) { + try { + const jsonData = JSON.parse(responseText); + responseText = JSON.stringify(jsonData, null, 2); + } catch { + // Not valid JSON, use as-is + } + } + + return { + content: [{ type: "text", text: responseText }], + details: { + url: params.url, + method: params.method, + statusCode: response.status, + headers: Object.fromEntries(response.headers.entries()) + } + }; + } catch (error) { + // Handle network errors + return { + content: [{ type: "text", text: `Network error: ${error.message}` }], + details: { + url: params.url, + error: error.message + }, + isError: true + }; + } + } +}; +``` + +--- + +## Tool Configuration + +### Tool Options + +Tools can be configured with options: + +```typescript +const bashTool = createBashTool({ + cwd: "/home/user/project", + timeout: 30, + maxStdoutLines: 1000, + maxStderrLines: 100 +}); + +const readTool = createReadTool({ + maxSize: 1024 * 1024, // 1MB + maxLines: 5000, + maxTotalSize: 10 * 1024 * 1024 // 10MB total +}); +``` + +### Tool Context + +Tools can receive application context: + +```typescript +interface ToolContext { + userId: string; + environment: "dev" | "staging" | "prod"; + permissions: string[]; +} + +const tool: AgentHarnessTool = { + name: "deploy_service", + label: "Deploy Service", + description: "Deploy service to environment", + parameters: Type.Object({ + service: Type.String(), + environment: Type.String({ enum: ["dev", "staging", "prod"] }) + }), + execute: async (toolCallId, params, signal, onUpdate, context) => { + // Access context + if (!context.permissions.includes("deploy")) { + throw new Error("Permission denied"); + } + + if (context.environment === "prod" && !params.environment) { + throw new Error("Must specify environment for prod deployment"); + } + + // ... + } +}; + +const harness = new AgentHarness({ + tools: [tool], + toolContext: { + userId: "user123", + environment: "prod", + permissions: ["read", "write", "deploy"] + } +}); +``` + +--- + +## Tool Execution Modes + +### Sequential Mode + +Tools marked as sequential execute **one at a time**: + +```typescript +const sequentialTool: AgentTool = { + name: "sequential_tool", + label: "Sequential Tool", + description: "Must run one at a time", + parameters: Type.Object({}), + executionMode: "sequential", // Key point + execute: async (toolCallId, params, signal, onUpdate) => { + // This tool won't run concurrently with other sequential tools + // Even if LLM sends multiple tool calls + } +}; +``` + +### Parallel Mode (Default) + +Tools execute **concurrently** by default: + +```typescript +const parallelTool: AgentTool = { + name: "parallel_tool", + label: "Parallel Tool", + description: "Can run concurrently", + parameters: Type.Object({}), + // executionMode defaults to "parallel" + execute: async (toolCallId, params, signal, onUpdate) => { + // This tool can run alongside other parallel tools + } +}; +``` + +### Agent-Level Execution Mode + +```typescript +const agent = new Agent({ + initialState: {...}, + streamFn: ... + toolExecution: "sequential" // All tools sequential by default +}); +``` + +--- + +## Error Handling + +### Tool Errors + +Tools should **throw** on critical errors (abort, timeout) but **return error results** on recoverable errors: + +```typescript +execute: async (toolCallId, params, signal, onUpdate) => { + try { + // Check for abort first + if (signal?.aborted) { + throw new Error("Operation aborted"); + } + + // Do work... + + // Return error result for recoverable errors + return { + content: [{ type: "text", text: "Error: Invalid input" }], + details: { error: "Invalid input" }, + isError: true + }; + } catch (error) { + // Re-throw abort errors + if (error instanceof Error && error.name === "AbortError") { + throw error; + } + + // Return error result for other errors + return { + content: [{ type: "text", text: `Error: ${error.message}` }], + details: { error: error.message }, + isError: true + }; + } +} +``` + +### Blockable Tools + +Use `beforeToolCall` hook to block tool execution: + +```typescript +beforeToolCall: async ({ toolCall, args }, signal) => { + if (toolCall.name === "bash") { + // Check for dangerous commands + const dangerousPatterns = ["rm -rf", "sudo", "dd if="]; + for (const pattern of dangerousPatterns) { + if (args.command?.includes(pattern)) { + return { block: true, reason: "Dangerous command blocked" }; + } + } + } + + return undefined; // Allow execution +} +``` + +--- + +## Best Practices + +### 1. Respect Abort Signals + +```typescript +execute: async (toolCallId, params, signal, onUpdate) => { + if (signal?.aborted) { + throw new Error("Operation aborted"); + } + + // Long-running operation + for await (const item of longProcess()) { + if (signal?.aborted) { + throw new Error("Operation aborted"); + } + onUpdate({ content: [{ type: "text", text: "Processing..." }] }); + } +} +``` + +### 2. Return Meaningful Error Messages + +```typescript +// Bad +return { content: [{ type: "text", text: "Error" }], isError: true }; + +// Good +return { + content: [{ type: "text", text: "Failed to read file: permission denied" }], + details: { path: "/etc/passwd", error: "EACCES" }, + isError: true +}; +``` + +### 3. Stream Progress for Long Operations + +```typescript +execute: async (toolCallId, params, signal, onUpdate) => { + for (let i = 0; i < 100; i++) { + // Do work... + onUpdate({ + content: [{ type: "text", text: `Progress: ${i}%` }], + details: { progress: i } + }); + } + + return { + content: [{ type: "text", text: "Complete" }], + details: { progress: 100 } + }; +} +``` + +### 4. Use Proper Tool Result Types + +```typescript +interface BashDetails { + command: string; + cwd: string; + exitCode: number; + stdout: string; + stderr: string; +} + +return { + content: [{ type: "text", text: "Command executed" }], + details: { command, cwd, exitCode, stdout, stderr } as BashDetails +}; +``` + +### 5. Handle Large Outputs + +```typescript +execute: async (toolCallId, params, signal, onUpdate) => { + const stdoutLines: string[] = []; + const stderrLines: string[] = []; + + for await (const chunk of process.stdout) { + stdoutLines.push(chunk); + if (stdoutLines.length > MAX_LINES) { + break; // Truncate + } + } + + return { + content: [{ type: "text", text: truncate(stdoutLines.join("\n")) }], + details: { stdout: stdoutLines.join("\n") } + }; +} +``` + +--- + +## Summary + +**Tools are the bridge** between the agent and the external world. + +**Key principles**: +- Return `isError: true` for recoverable errors +- Throw on abort/timeout +- Stream progress for long operations +- Respect abort signals throughout +- Use detailed error messages + +**Built-in tools**: +- `bash`: Execute shell commands +- `read`: Read files +- `write`: Write files +- `edit`: Make precise edits + +**Custom tools** can do anything: API calls, database queries, file operations, etc. diff --git a/packages/agent/learning/06-AGENTHARNESS-REFERENCE.md b/packages/agent/learning/06-AGENTHARNESS-REFERENCE.md new file mode 100644 index 00000000..3c9b3e51 --- /dev/null +++ b/packages/agent/learning/06-AGENTHARNESS-REFERENCE.md @@ -0,0 +1,803 @@ +# AgentHarness Reference + +## Overview + +`AgentHarness` is the **high-level API** that wraps the core agent with session management, persistence, branching, and tool context binding. + +--- + +## Key Differences: Agent vs AgentHarness + +| Feature | Agent (Core) | AgentHarness | +|---------|-------------|--------------| +| **Session Persistence** | No | Yes (JSONL/Memory) | +| **Branching** | No | Yes | +| **Context Compaction** | No | Yes | +| **Tool Context** | Manual | Automatic binding | +| **Skills/Templates** | Manual | Built-in | +| **State Management** | Manual | Automatic | +| **Event Hooks** | Basic | Rich system | + +--- + +## AgentHarness Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ AgentHarness │ +├─────────────────────────────────────────────────────────────┤ +│ State │ +│ ├─ Session (persistence) │ +│ ├─ Model │ +│ ├─ ThinkingLevel │ +│ ├─ Tools (Map) │ +│ ├─ ActiveTools (string[]) │ +│ └─ SystemPrompt (string or function) │ +│ │ +│ Queues │ +│ ├─ steerQueue (messages to interrupt agent) │ +│ ├─ followUpQueue (messages after agent stops) │ +│ └─ nextTurnQueue (messages for next turn) │ +│ │ +│ Hooks │ +│ ├─ before_agent_start │ +│ ├─ context │ +│ ├─ tool_call │ +│ ├─ tool_result │ +│ ├─ session_before_compact │ +│ ├─ session_before_tree │ +│ ├─ before_provider_request │ +│ └─ before_provider_payload │ +│ │ +│ Methods │ +│ ├─ prompt() - Run new conversation │ +│ ├─ skill() - Execute skill │ +│ ├─ promptFromTemplate() - Run template │ +│ ├─ steer() - Interrupt agent │ +│ ├─ followUp() - Queue message │ +│ ├─ compact() - Compress context │ +│ ├─ navigateTree() - Branch session │ +│ └─ subscribe() - Add event listener │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Core Concepts + +### 1. Session + +The session holds **conversation history as a tree**: + +```typescript +interface Session { + readonly id: string; + readonly storage: SessionStorage; + + getMetadata(): Promise; + getLeafId(): Promise; + getEntry(id: string): Promise; + getBranch(): Promise; + buildContext(options?: SessionContextBuildOptions): Promise; + + appendMessage(message: AgentMessage): Promise; + appendModelChange(provider: string, modelId: string): Promise; + appendThinkingLevelChange(thinkingLevel: ThinkingLevel): Promise; + appendActiveToolsChange(activeToolNames: string[]): Promise; + appendCompaction(...): Promise; + appendBranchSummary(...): Promise; + + fork(targetId: string): Promise; +} +``` + +### 2. Resources + +Skills and prompt templates available to the agent: + +```typescript +interface AgentHarnessResources { + skills?: TSkill[]; + promptTemplates?: TPromptTemplate[]; +} + +interface Skill { + name: string; + description: string; + content: string; + filePath: string; + disableModelInvocation?: boolean; +} + +interface PromptTemplate { + name: string; + description?: string; + content: string; +} +``` + +### 3. Tool Context + +Context passed to all tool executions: + +```typescript +interface ToolContext { + userId: string; + environment: "dev" | "staging" | "prod"; + // ... custom properties +} + +// Zero-arg function for dynamic context +type ToolContextProvider = () => TContext | Promise; +``` + +--- + +## AgentHarness API + +### Constructor + +```typescript +constructor(options: AgentHarnessOptions) +``` + +**Options**: +```typescript +interface AgentHarnessOptions { + session: Session; // Session storage + models: Models; // LLM provider + resources?: AgentHarnessResources; + streamOptions?: AgentHarnessStreamOptions; + retry?: RetryPolicy; + + // System prompt + systemPrompt?: + | string // Static string + | AgentHarnessSystemPrompt; // Dynamic function + + // Tool context + toolContext?: AgentHarnessToolContextSource; + + // Tools + tools?: TTool[]; + + // Active tools + activeToolNames?: string[]; + + // Model and thinking + model: Model; + thinkingLevel?: ThinkingLevel; + + // Queue modes + steeringMode?: QueueMode; + followUpMode?: QueueMode; +} +``` + +**Example**: +```typescript +const harness = new AgentHarness({ + session: memorySession, + models: models, + resources: { + skills: [weatherSkill, gitSkill], + promptTemplates: [summaryTemplate] + }, + systemPrompt: async ({ session, model, activeTools, resources }) => { + const sessionMetadata = await session.getMetadata(); + const toolsList = activeTools.map(t => t.name).join(", "); + + return `You are an AI assistant with access to tools: ${toolsList}. + +Current session: ${sessionMetadata.id} +Date: ${new Date().toISOString()} + +Available skills: +${resources.skills?.map(s => `- ${s.name}: ${s.description}`).join("\n")} +`; + }, + toolContext: { userId: "user123", environment: "prod" }, + tools: [weatherTool, gitTool, readFileTool], + activeToolNames: ["weather", "git"], + model: gpt4Model, + thinkingLevel: "medium" +}); +``` + +### System Prompt + +**Static string**: +```typescript +systemPrompt: "You are a helpful assistant." +``` + +**Dynamic function**: +```typescript +systemPrompt: async ({ + session, + model, + thinkingLevel, + activeTools, + resources +}) => { + const metadata = await session.getMetadata(); + + return `System: ${metadata.id} +Model: ${model.id} +Date: ${new Date().toISOString()} + +Active tools: ${activeTools.map(t => t.name).join(", ")} +`; +}; +``` + +--- + +## Main Methods + +### `prompt()` + +Run a new prompt: + +```typescript +async prompt(text: string, options?: { images?: ImageContent[] }): Promise +``` + +**Flow**: +1. Validate harness is idle +2. Create turn state (context, tools, system prompt) +3. Emit `before_agent_start` hook +4. Run agent loop with prompt +5. Return assistant message + +**Example**: +```typescript +const message = await harness.prompt("What's the weather in London?"); +console.log(message.content); // Assistant response +``` + +### `skill()` + +Execute a named skill: + +```typescript +async skill(name: string, additionalInstructions?: string): Promise +``` + +**Example**: +```typescript +const message = await harness.skill("git", "Also create a PR for the changes"); +// Skill content injected into prompt +``` + +### `promptFromTemplate()` + +Execute a prompt template: + +```typescript +async promptFromTemplate( + name: string, + args: string[] = [] +): Promise +``` + +**Example**: +```typescript +// Template: "Fix the following error: {{0}}" +const message = await harness.promptFromTemplate("fix_error", ["TypeError: x is undefined"]); +``` + +### `steer()` + +Interrupt agent mid-execution: + +```typescript +async steer(text: string, options?: { images?: ImageContent[] }): Promise +``` + +**Example**: +```typescript +await harness.prompt("Write a long report..."); +// While agent is working... +await harness.steer("Wait, change focus to climate change"); +// Agent continues with new instructions +``` + +### `followUp()` + +Queue message for after agent stops: + +```typescript +async followUp(text: string, options?: { images?: ImageContent[] }): Promise +``` + +**Example**: +```typescript +await harness.prompt("Analyze this data..."); +// Agent finishes... +await harness.followUp("Now create a summary"); +// Agent continues with summary request +``` + +### `nextTurn()` + +Queue message for next turn (doesn't interrupt current turn): + +```typescript +async nextTurn(text: string, options?: { images?: ImageContent[] }): Promise +``` + +**Difference from `steer()`**: +- `steer()`: Interrupts immediately +- `nextTurn()`: Waits for current turn to finish + +### `compact()` + +Compress conversation history: + +```typescript +async compact(customInstructions?: string): Promise +``` + +**Returns**: +```typescript +interface CompactResult { + summary: string; + firstKeptEntryId?: string; + tokensBefore: number; + usage?: Usage; + retainedTail?: AgentMessage[]; + details?: unknown; +} +``` + +**Example**: +```typescript +const result = await harness.compact(); +console.log(`Compressed from ${result.tokensBefore} tokens to summary`); +``` + +### `navigateTree()` + +Navigate conversation tree (branching): + +```typescript +async navigateTree( + targetId: string, + options?: { + summarize?: boolean; + customInstructions?: string; + replaceInstructions?: boolean; + label?: string; + } +): Promise +``` + +**Returns**: +```typescript +interface NavigateTreeResult { + cancelled: boolean; + editorText?: string; // If target is user message + summaryEntry?: BranchSummaryEntry; +} +``` + +**Example**: +```typescript +// Navigate to earlier point in conversation +const result = await harness.navigateTree("entry_abc123", { summarize: true }); + +// Create branch from current point +const newHarness = createNewHarness(); +await newHarness.navigateTree("entry_xyz789"); +``` + +--- + +## State Management + +### Model + +```typescript +getModel(): Model; + +async setModel(model: Model): Promise; +``` + +**Example**: +```typescript +console.log(harness.getModel().id); // "gpt-4" + +await harness.setModel(gpt4oModel); +``` + +### Thinking Level + +```typescript +getThinkingLevel(): ThinkingLevel; + +async setThinkingLevel(level: ThinkingLevel): Promise; +``` + +**Levels**: `"off"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"` + +**Example**: +```typescript +await harness.setThinkingLevel("high"); // More reasoning for complex tasks +``` + +### Tools + +```typescript +getTools(): TTool[]; +getActiveTools(): TTool[]; + +async setTools(tools: TTool[], activeToolNames?: string[]): Promise; +async setActiveTools(toolNames: string[]): Promise; +``` + +**Example**: +```typescript +// Add new tool +await harness.setTools([...harness.getTools(), newTool]); + +// Change active tools +await harness.setActiveTools(["read", "write"]); +``` + +### Resources + +```typescript +getResources(): AgentHarnessResources; + +async setResources(resources: AgentHarnessResources): Promise; +``` + +**Example**: +```typescript +await harness.setResources({ + skills: [...harness.getResources().skills, newSkill] +}); +``` + +--- + +## Queue Management + +### Steering Queue + +```typescript +getSteeringMode(): QueueMode; + +async setSteeringMode(mode: QueueMode): Promise; +``` + +**Modes**: +- `"all"`: Drain all queued messages at once +- `"one-at-a-time"`: Drain one message at a time + +### Follow-up Queue + +```typescript +getFollowUpMode(): QueueMode; + +async setFollowUpMode(mode: QueueMode): Promise; +``` + +### Queue Helpers + +```typescript +// Clear all queued messages +harness.clearAllQueues(); + +// Check if queues have pending messages +harness.hasQueuedMessages(); // boolean +``` + +--- + +## Event Handling + +### Subscribe to All Events + +```typescript +subscribe( + listener: (event: AgentHarnessEvent, signal?: AbortSignal) => Promise | void +): () => void; +``` + +**Event types**: +```typescript +type AgentHarnessEvent = + // Agent events (forwarded from core agent) + | { type: "agent_start" } + | { type: "agent_end"; messages: AgentMessage[] } + | { type: "turn_start" } + | { type: "turn_end"; message: AgentMessage; toolResults: ToolResultMessage[] } + | { type: "message_start"; message: AgentMessage } + | { type: "message_update"; message: AgentMessage; assistantMessageEvent: AssistantMessageEvent } + | { type: "message_end"; message: AgentMessage } + | { type: "tool_execution_start"; toolCallId: string; toolName: string; args: any } + | { type: "tool_execution_update"; toolCallId: string; toolName: string; args: any; partialResult: any } + | { type: "tool_execution_end"; toolCallId: string; toolName: string; result: any; isError: boolean } + + // Harness-specific events + | { type: "before_agent_start"; ... } + | { type: "context"; messages: AgentMessage[] } + | { type: "tool_call"; ... } + | { type: "tool_result"; ... } + | { type: "session_before_compact"; ... } + | { type: "session_before_tree"; ... } + | { type: "before_provider_request"; ... } + | { type: "before_provider_payload"; ... } + | { type: "after_provider_response"; ... } + | { type: "save_point"; ... } + | { type: "settled"; ... } + | { type: "model_update"; ... } + | { type: "thinking_level_update"; ... } + | { type: "tools_update"; ... } + | { type: "resources_update"; ... } + | { type: "session_compact"; ... } + | { type: "session_tree"; ... } + | { type: "queue_update"; ... } + | { type: "retry_scheduled"; ... } + | { type: "retry_attempt_start"; ... } + | { type: "retry_finished"; ... } + | { type: "abort"; clearedSteer: UserMessage[]; clearedFollowUp: UserMessage[] }; +``` + +**Example**: +```typescript +const unsubscribe = harness.subscribe(async (event, signal) => { + if (event.type === "message_end") { + console.log("Message:", event.message.role); + } + + if (event.type === "agent_end") { + console.log("Conversation complete"); + } + + if (event.type === "tool_execution_end") { + console.log("Tool:", event.toolName, "completed"); + } +}); +``` + +### Subscribe to Specific Events + +```typescript +on( + type: TType, + handler: (event: Extract) => Promise | AgentHarnessEventResultMap[TType] +): () => void; +``` + +**Example**: +```typescript +// Handle tool calls +harness.on("tool_call", async ({ toolCallId, toolName, input }) => { + console.log(`Tool ${toolName} called with:`, input); + return undefined; // Allow execution +}); + +// Handle tool results +harness.on("tool_result", async ({ toolName, content, isError }) => { + console.log(`Tool ${toolName} result:`, isError ? "Error" : "Success"); + return undefined; // Use default result +}); + +// Modify system prompt +harness.on("before_agent_start", async ({ systemPrompt }) => { + return { + systemPrompt: `${systemPrompt}\n\nRemember to be concise.` + }; +}); +``` + +--- + +## Session Persistence + +### Append Message + +```typescript +async appendMessage(message: AgentMessage): Promise; +``` + +**Example**: +```typescript +// Manually add message to session +await harness.appendMessage({ + role: "user", + content: [{ type: "text", text: "Custom message" }], + timestamp: Date.now() +}); +``` + +### Flush Pending Writes + +```typescript +async abort(): Promise +``` + +**Returns**: +```typescript +interface AbortResult { + clearedSteer: UserMessage[]; + clearedFollowUp: UserMessage[]; +} +``` + +**Example**: +```typescript +const result = await harness.abort(); +console.log(`Cleared ${result.clearedSteer.length} steering messages`); +``` + +### Wait for Idle + +```typescript +async waitForIdle(): Promise; +``` + +**Example**: +```typescript +await harness.prompt("Do something..."); +await harness.waitForIdle(); // Wait for completion +console.log("Done"); +``` + +--- + +## Error Handling + +### Error Codes + +```typescript +type AgentHarnessErrorCode = + | "busy" // Agent is already processing + | "invalid_state" // Invalid state for operation + | "invalid_argument" // Invalid arguments + | "session" // Session error + | "hook" // Hook error + | "auth" // Authentication error + | "compaction" // Compaction error + | "branch_summary" // Branch summary error + | "unknown"; // Unknown error +``` + +### Error Handling Pattern + +```typescript +try { + await harness.prompt("Do something"); +} catch (error) { + if (error instanceof AgentHarnessError) { + switch (error.code) { + case "busy": + console.log("Agent busy, try again later"); + break; + case "compaction": + console.log("Compaction failed:", error.message); + break; + case "hook": + console.log("Hook error:", error.cause?.message); + break; + default: + console.log("Error:", error.message); + } + } +} +``` + +--- + +## Advanced Patterns + +### 1. Dynamic System Prompt + +```typescript +systemPrompt: async ({ session, model, activeTools, resources }) => { + const metadata = await session.getMetadata(); + + // Customize based on session type + if (metadata.type === "coding") { + return `You are a coding assistant. Use tools: ${activeTools.map(t => t.name).join(", ")}`; + } else if (metadata.type === "writing") { + return `You are a writing assistant. Focus on clarity and style.`; + } + + return "You are a helpful assistant."; +} +``` + +### 2. Conditional Tool Activation + +```typescript +// Enable tools based on user request +harness.on("before_agent_start", async ({ prompt }) => { + if (prompt.includes("weather")) { + return { + messages: [{ role: "user", content: [{ type: "text", text: "Enable weather tool" }] }] + }; + } + return undefined; +}); +``` + +### 3. Session Branching + +```typescript +async function exploreAlternative(harness: AgentHarness, prompt: string): Promise { + // Get current leaf + const leafId = await harness.session.getLeafId(); + + // Create branch + const branchSession = await harness.session.fork(leafId); + const branchHarness = new AgentHarness({ + ...harnessOptions, + session: branchSession + }); + + // Run alternative + return await branchHarness.prompt(prompt); +} +``` + +### 4. Custom Compaction + +```typescript +harness.on("session_before_compact", async ({ preparation }) => { + // Skip compaction for short sessions + if (preparation.tokensBefore < 1000) { + return { cancel: true }; + } + + // Provide custom summary + return { + compaction: { + summary: "User asked about X, Y, Z and assistant provided guidance.", + tokensBefore: preparation.tokensBefore, + firstKeptEntryId: preparation.firstKeptEntry.id, + details: { manual: true } + } + }; +}); +``` + +### 5. Tool Execution Logging + +```typescript +harness.on("tool_call", async ({ toolName, input }) => { + console.log(`[TOOL_CALL] ${toolName}:`, JSON.stringify(input, null, 2)); + return undefined; +}); + +harness.on("tool_result", async ({ toolName, content, isError }) => { + console.log(`[TOOL_RESULT] ${toolName}:`, isError ? "❌" : "✅"); + return undefined; +}); +``` + +--- + +## Summary + +**AgentHarness provides**: +- Session persistence and tree navigation +- Built-in tool context binding +- Rich hook system for customization +- Skills and prompt templates +- Context compaction and branching + +**Key methods**: +- `prompt()` - Main interaction +- `steer()` / `followUp()` - Queue management +- `compact()` - Context management +- `navigateTree()` - Branching + +**Key patterns**: +- Dynamic system prompts +- Conditional tool activation +- Session branching for experimentation +- Hook-based customization diff --git a/packages/agent/learning/07-DATA-FLOW-STATE.md b/packages/agent/learning/07-DATA-FLOW-STATE.md new file mode 100644 index 00000000..4996463d --- /dev/null +++ b/packages/agent/learning/07-DATA-FLOW-STATE.md @@ -0,0 +1,687 @@ +# Data Flow and State Management + +## Overview + +Understanding how data flows through the agent system is crucial for debugging and extending functionality. + +--- + +## Message Flow + +### 1. Input Messages + +```typescript +// User input +await harness.prompt("Build a web app"); + +// Internal messages +await harness.steer("Wait, use React"); +await harness.followUp("Now add tests"); +await harness.nextTurn("Also deploy to production"); +``` + +**Normalization**: +```typescript +function normalizePromptInput(input: string | AgentMessage | AgentMessage[]): AgentMessage[] { + if (Array.isArray(input)) return input; + + if (typeof input !== "string") { + return [input]; // Already a message + } + + // String → user message + return [{ + role: "user", + content: [{ type: "text", text: input }], + timestamp: Date.now() + }]; +} +``` + +### 2. AgentMessage Types + +```typescript +type AgentMessage = Message | CustomAgentMessages[keyof CustomAgentMessages] + +interface Message { + role: "user" | "assistant" | "toolResult"; + content: (TextContent | ImageContent)[]; + api?: string; + provider?: string; + model?: string; + usage?: Usage; + stopReason?: StopReason; + errorMessage?: string; + timestamp: number; +} + +interface TextContent { + type: "text"; + text: string; +} + +interface ImageContent { + type: "image"; + mediaType: string; + data: string; // Base64 +} +``` + +### 3. Message Lifecycle + +``` +User Input + │ + ▼ +normalizePromptInput() → AgentMessage[] + │ + ▼ +runPromptMessages() → runWithLifecycle() + │ + ├─► Set isStreaming=true + ├─► Create abort controller + └─► runAgentLoop() + │ + ▼ + runLoop() + │ + ├─► message_start (user prompt) + ├─► message_end + ├─► streamAssistantResponse() + │ ├─► message_start (assistant) + │ ├─► message_update (chunks) + │ └─► message_end + ├─► executeToolCalls() + │ └─► message_start/end (toolResults) + └─► turn_end + │ + ▼ + handleAgentEvent() (harness) + │ + ├─► session.appendMessage() + │ └─► Storage: write entry + └─► Emit: message_end (forwarded) +``` + +--- + +## State Management + +### Agent State + +```typescript +interface AgentState { + systemPrompt: string; + model: Model; + thinkingLevel: ThinkingLevel; + tools: AgentTool[]; + messages: AgentMessage[]; + isStreaming: boolean; + streamingMessage?: AgentMessage; + pendingToolCalls: Set; + errorMessage?: string; +} +``` + +**State changes**: + +| Event | State Changed | +|-------|--------------| +| `message_start` | `streamingMessage` = message | +| `message_update` | `streamingMessage` = message | +| `message_end` | `messages.push(message)`, `streamingMessage` = undefined | +| `tool_execution_start` | `pendingToolCalls.add(toolCallId)` | +| `tool_execution_end` | `pendingToolCalls.delete(toolCallId)` | +| `turn_end` | `errorMessage` (if error) | +| `agent_end` | `streamingMessage` = undefined | + +### State Mutation Example + +```typescript +// In Agent.processEvents() +private async processEvents(event: AgentEvent): Promise { + switch (event.type) { + case "message_start": + this._state.streamingMessage = event.message; + break; + + case "message_end": + this._state.streamingMessage = undefined; + this._state.messages.push(event.message); + break; + + case "tool_execution_start": { + const pending = new Set(this._state.pendingToolCalls); + pending.add(event.toolCallId); + this._state.pendingToolCalls = pending; + break; + } + + case "tool_execution_end": { + const pending = new Set(this._state.pendingToolCalls); + pending.delete(event.toolCallId); + this._state.pendingToolCalls = pending; + break; + } + } + + // Emit to listeners + for (const listener of this.listeners) { + await listener(event, signal); + } +} +``` + +--- + +## Context Flow + +### Context Snapshot + +```typescript +interface AgentContext { + systemPrompt: string; + messages: AgentMessage[]; + tools?: AgentTool[]; +} +``` + +**When created**: +1. `Agent.createContextSnapshot()` - before each LLM call +2. `AgentHarness.createContext()` - in turn state + +### Context Transformation + +```typescript +// 1. transformContext() hook (AgentMessage[]) +let messages = context.messages; +if (config.transformContext) { + messages = await config.transformContext(messages, signal); +} + +// 2. convertToLlm() hook (AgentMessage[] → Message[]) +const llmMessages = await config.convertToLlm(messages); + +// 3. Build LLM context (Message[]) +const llmContext: Context = { + systemPrompt: context.systemPrompt, + messages: llmMessages, + tools: context.tools +}; +``` + +### Context Transformations + +**Example: Prune old messages** + +```typescript +transformContext: async (messages) => { + if (estimateTokens(messages) > MAX_TOKENS) { + // Find cut point (preserve recent turns) + const cutIndex = findCutPoint(messages, MAX_TOKENS * 0.7); + return messages.slice(cutIndex); + } + return messages; +} +``` + +**Example: Inject external context** + +```typescript +transformContext: async (messages) => { + const externalData = await fetchExternalData(); + const contextMessage: AgentMessage = { + role: "user", + content: [{ type: "text", text: externalData }], + timestamp: Date.now() + }; + + return [contextMessage, ...messages]; +} +``` + +--- + +## Hook Context Flow + +### Hook Parameter Flow + +``` +Agent.prompt() + │ + ├─► transformContext(messages) [AgentLoopConfig] + │ └─► Messages before LLM call + │ + ├─► convertToLlm(messages) + │ └─► Messages to send to LLM + │ + ├─► beforeToolCall(context) [AgentLoopConfig] + │ ├─► assistantMessage + │ ├─► toolCall + │ ├─► args (validated) + │ └─► context (AgentContext) + │ + ├─► afterToolCall(context) [AgentLoopConfig] + │ ├─► assistantMessage + │ ├─► toolCall + │ ├─► args + │ ├─► result (executed) + │ ├─► isError + │ └─► context (AgentContext) + │ + ├─► shouldStopAfterTurn(context) [AgentLoopConfig] + │ ├─► message (assistant) + │ ├─► toolResults + │ ├─► context (AgentContext) + │ └─► newMessages + │ + ├─► prepareNextTurn(context) [AgentLoopConfig] + │ └─► Return: context/model/thinkingLevel + │ + ├─► getSteeringMessages() [AgentLoopConfig] + │ └─► Messages to inject now + │ + └─► getFollowUpMessages() [AgentLoopConfig] + └─► Messages for after agent stops +``` + +### Hook Return Value Flow + +``` +beforeToolCall() + │ + ├─► { block: true, reason } → Error tool result + └─► undefined → Allow execution + │ + ▼ + tool.execute() + │ + ▼ + afterToolCall() + │ + ├─► Override: content, details, isError, usage, terminate + └─► undefined → Use executed result + │ + ▼ + Emit: tool_execution_end + │ + ▼ + Create: ToolResultMessage + │ + ▼ + Emit: message_start/end (toolResult) +``` + +--- + +## Queue Flow + +### Steering Queue + +**Purpose**: Interrupt agent while working. + +**Flow**: +``` +steer("New instruction") + │ + ▼ +steeringQueue.enqueue(message) + │ + ▼ +After turn ends: + │ + ├─► getSteeringMessages() called + │ ├─► Drain queue (mode: "all" or "one-at-a-time") + │ └─► Return messages + │ + ▼ +Inject messages into context + │ + ▼ +Next LLM call includes steering messages +``` + +**Example**: +```typescript +// User types while agent is working +agent.steer("Wait, check this file first"); + +// Agent finishes current work +// → Steering messages injected +// → LLM sees: [original, ..., new user message] +``` + +### Follow-up Queue + +**Purpose**: Queue messages for after agent stops naturally. + +**Flow**: +``` +followUp("Next task") + │ + ▼ +followUpQueue.enqueue(message) + │ + ▼ +Agent would stop (no more tool calls) + │ + ├─► getFollowUpMessages() called + │ ├─► Drain queue + │ └─► Return messages + │ + ▼ +Set as pendingMessages + │ + ▼ +Inner loop continues +``` + +**Example**: +```typescript +agent.followUp("Now create a README"); + +// Agent finishes current task +// → Follow-up messages injected +// → Agent continues with new task +``` + +### Queue Modes + +**"all" Mode**: +``` +Queued: [msg1, msg2, msg3] + │ + ▼ +Drain: [msg1, msg2, msg3] + │ + ▼ +All injected together +``` + +**"one-at-a-time" Mode**: +``` +Queued: [msg1, msg2, msg3] + │ + ▼ +Drain: [msg1] + │ + ▼ +msg1 injected, msg2, msg3 remain + │ + ▼ +After next turn: + │ + ▼ +Drain: [msg2] + │ + ▼ +... and so on +``` + +--- + +## Session Flow + +### Session Tree Structure + +``` +root (parentId: null) + ├─► message [id: 1, parentId: null] + │ └─► message [id: 2, parentId: 1] + │ └─► tool_result [id: 3, parentId: 2] + │ └─► message [id: 4, parentId: 3] + │ └─► compaction [id: 5, parentId: 4] + │ ├─► retained: [msg6, msg7] + │ └─► message [id: 8, parentId: 5] + │ └─► leaf [id: 9, parentId: 8] +``` + +### Context Building + +```typescript +async function buildContext(session: Session): Promise { + // 1. Get path from leaf to root + const pathEntries = await session.getBranch(); + // [root, msg1, msg2, toolResult, msg4, compaction, msg8, leaf] + + // 2. Apply default transform (compaction logic) + const contextEntries = defaultContextEntryTransform(pathEntries); + // [compaction, retainedTail..., msg8] + + // 3. Project entries to messages + const messages = contextEntries.flatMap(sessionEntryToContextMessages); + // [compactionSummary, retainedMsgs..., msg8] + + // 4. Derive state + const state = deriveSessionContextState(pathEntries); + // { model, thinkingLevel, activeToolNames } + + return { ...state, messages }; +} +``` + +### Session Entry Types + +| Type | Stored When | +|------|-------------| +| `message` | Every user/assistant/toolResult | +| `model_change` | `setModel()` called | +| `thinking_level_change` | `setThinkingLevel()` called | +| `active_tools_change` | `setActiveTools()` called | +| `compaction` | `compact()` called | +| `branch_summary` | Branching with summary | +| `custom` | `appendCustomEntry()` | +| `custom_message` | `appendCustomMessageEntry()` | +| `label` | `appendLabel()` | +| `leaf` | `setLeafId()` | +| `session_info` | `appendSessionName()` | + +### Pending Writes + +During active turns, writes are buffered: + +```typescript +async function appendMessage(message: AgentMessage): Promise { + if (phase === "idle") { + // Direct write + await session.appendMessage(message); + } else { + // Buffer for later + pendingSessionWrites.push({ type: "message", message }); + } +} + +async function flushPendingSessionWrites(): Promise { + while (pendingSessionWrites.length > 0) { + const write = pendingSessionWrites.shift(); + + if (write.type === "message") { + await session.appendMessage(write.message); + } else if (write.type === "model_change") { + await session.appendModelChange(...); + } + // ... other types + } +} +``` + +--- + +## Tool Execution State Flow + +### Tool Call State + +```typescript +interface BeforeToolCallContext { + assistantMessage: AssistantMessage; + toolCall: AgentToolCall; + args: unknown; // Validated + context: AgentContext; // Snapshot +} + +interface AfterToolCallContext { + assistantMessage: AssistantMessage; + toolCall: AgentToolCall; + args: unknown; + result: AgentToolResult; // Executed + isError: boolean; + context: AgentContext; +} +``` + +### Tool Result State + +```typescript +interface AgentToolResult { + content: (TextContent | ImageContent)[]; // To model + details: T; // For logs/UI + usage?: Usage; // Tool-specific + addedToolNames?: string[]; // New tools + terminate?: boolean; // Early stop hint +} +``` + +### State Transition + +``` +Tool Call from LLM + │ + ▼ +prepareToolCall() + ├─► Find tool + ├─► Validate args + └─► beforeToolCall() + ├─► block: true → Error + └─► block: undefined → Continue + │ + ▼ + tool.execute() + ├─► onUpdate(partialResult) + └─► Return final result + │ + ▼ + afterToolCall() + ├─► Override result + └─► Use executed result + │ + ▼ + createToolResultMessage() + │ + ▼ + Emit: tool_execution_end + │ + ▼ + Emit: message_start/end (toolResult) + │ + ▼ + Push to context.messages +``` + +--- + +## Abort Flow + +### Abort Signal Propagation + +```typescript +// 1. Create abort controller +const abortController = new AbortController(); + +// 2. Pass to all async operations +await runAgentLoop(..., abortController.signal, ...); + +// 3. Check signal in long operations +execute: async (id, params, signal, onUpdate) => { + for await (const item of longProcess()) { + if (signal?.aborted) { + throw new Error("Aborted"); + } + } +} + +// 4. Abort +abortController.abort(); +``` + +### Abort in Hooks + +```typescript +// Check signal at start +beforeToolCall: async ({ toolCall }, signal) => { + if (signal?.aborted) { + return { block: true, reason: "Operation aborted" }; + } + return undefined; +} + +// Check signal in async operations +transformContext: async (messages, signal) => { + if (signal?.aborted) { + return messages; // Return safe fallback + } + + // Long operation + const result = await expensiveTransform(messages, signal); + return result; +} +``` + +--- + +## Event Flow Diagram + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ AGENT LIFECYCLE │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ Agent.prompt("Hello") │ +│ │ │ +│ ├─► agent_start (event) │ +│ ├─► turn_start (event) │ +│ ├─► message_start (user) (event) │ +│ ├─► message_end (user) (event) │ +│ │ │ +│ ├─► streamAssistantResponse() │ +│ │ ├─► message_start (assistant) (event) │ +│ │ ├─► message_update (text chunk 1) (event) │ +│ │ ├─► message_update (text chunk 2) (event) │ +│ │ ├─► message_update (toolCall) (event) │ +│ │ └─► message_end (assistant) (event) │ +│ │ │ +│ ├─► executeToolCalls() │ +│ │ ├─► tool_execution_start (event) │ +│ │ ├─► tool_execute() │ +│ │ │ └─► onUpdate(partial) (event) │ +│ │ ├─► tool_execution_end (event) │ +│ │ └─► message_start/end (toolResult) (events) │ +│ │ │ +│ ├─► turn_end (event) │ +│ │ ├─► Should stop? → agent_end │ +│ │ └─► Drain queues → another turn │ +│ │ │ +│ └─► agent_end (event) │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Summary + +**Key data flows**: +1. Input messages → Normalized → Agent messages +2. Agent messages → Context transform → LLM messages +3. LLM response → Streamed → Agent messages +4. Tool calls → Executed → Tool results → Agent messages +5. All messages → Session storage → Tree structure + +**State management**: +- Agent: In-memory state with mutation on events +- Session: Persistent tree with entries +- Hooks: Transform data at key points + +**Queue system**: +- Steering: Interrupt current work +- Follow-up: Queue for after agent stops +- Modes: "all" or "one-at-a-time" diff --git a/packages/agent/learning/08-LEARNING-PATH.md b/packages/agent/learning/08-LEARNING-PATH.md new file mode 100644 index 00000000..5418cf52 --- /dev/null +++ b/packages/agent/learning/08-LEARNING-PATH.md @@ -0,0 +1,522 @@ +# Learning Path and Study Guide + +## Overview + +This guide helps you learn the Pi Agent architecture **top-down**, starting from high-level concepts to implementation details. + +--- + +## Phase 1: Big Picture (1-2 hours) + +### Goal: Understand how components fit together + +### Resources +1. **01-ARCHITECTURE-OVERVIEW.md** - Read this first +2. **Diagrams** - Study the architecture diagrams + +### Key Questions to Answer + +✅ What are the two main layers? +✅ What does each layer do? +✅ How do messages flow through the system? +✅ What is the relationship between Agent and AgentHarness? +✅ What are the main event types? +✅ How do tools integrate with the agent? +✅ What is the purpose of hooks? + +### Exercises + +1. Draw the architecture diagram from memory +2. List 3 use cases for each hook type +3. Trace a message from input to LLM to output + +--- + +## Phase 2: Core Agent (2-3 hours) + +### Goal: Understand the low-level agent loop + +### Resources +1. **02-AGENT-LOOP-DETAILED.md** - Study the agent loop +2. Read `src/agent-loop.ts` (skim, focus on comments) +3. Read `src/types.ts` - Understand AgentEvent, AgentMessage, AgentTool + +### Key Concepts + +- **runAgentLoop()** - Starts a new conversation +- **runAgentLoopContinue()** - Continues existing conversation +- **runLoop()** - Main iteration (outer and inner loops) +- **streamAssistantResponse()** - Streams LLM response +- **executeToolCalls()** - Executes tool calls +- **prepareToolCall()** - Validates and prepares tools +- **executePreparedToolCall()** - Executes tool with updates +- **finalizeExecutedToolCall()** - Finalizes with hooks + +### Key Questions to Answer + +✅ What's the difference between outer and inner loop? +✅ How does streaming work? +✅ How are tool calls executed (sequential vs parallel)? +✅ What happens when a tool is blocked? +✅ How are errors handled? +✅ What are the four phases of tool execution? +✅ How does the loop know when to stop? + +### Exercises + +1. Trace through a conversation with 1 prompt + 2 tool calls +2. Draw the outer/inner loop flow +3. Explain how abort signals propagate +4. Explain queue draining (steering/follow-up) + +--- + +## Phase 3: Hooks System (2-3 hours) + +### Goal: Understand how to customize agent behavior + +### Resources +1. **03-HOOK-SYSTEM.md** - Study all hooks +2. Read `src/types.ts` - Hook types and contexts + +### Hook Categories + +**Message Transformation**: +- `convertToLlm` - Convert messages to LLM format +- `transformContext` - Manipulate context before LLM + +**Lifecycle Hooks**: +- `beforeToolCall` - Block or modify tool execution +- `afterToolCall` - Override tool results +- `shouldStopAfterTurn` - Request early termination +- `prepareNextTurn` - Update context/model/thinking + +**Queue Draining**: +- `getSteeringMessages` - Interrupt agent mid-work +- `getFollowUpMessages` - Queue messages for later + +### Key Questions to Answer + +✅ What hooks receive abort signals? +✅ What hooks can block execution? +✅ What is the execution order of hooks? +✅ What's the difference between beforeToolCall and afterToolCall? +✅ How do you implement context window management? +✅ How do you implement permission checks? +✅ What's the difference between steering and follow-up? + +### Exercises + +1. Implement a hook that logs all tool calls +2. Implement a hook that blocks dangerous commands +3. Implement a hook that summarizes conversation every 5 turns +4. Implement a hook that switches to high thinking for complex tasks + +--- + +## Phase 4: AgentHarness (3-4 hours) + +### Goal: Understand high-level API and session management + +### Resources +1. **06-AGENTHARNESS-REFERENCE.md** - Study the harness API +2. Read `src/harness/agent-harness.ts` (focus on public methods) + +### Key Concepts + +- **Session** - Persistent conversation history +- **SessionTreeEntry** - Individual entries in conversation +- **Context Building** - Derive LLM context from session +- **Branching** - Create new conversation paths +- **Compaction** - Summarize old history + +### API Methods + +**Core**: +- `prompt()` - Run new conversation +- `skill()` - Execute skill +- `promptFromTemplate()` - Run template + +**Queues**: +- `steer()` - Interrupt agent +- `followUp()` - Queue message +- `nextTurn()` - Queue for next turn + +**Session**: +- `compact()` - Compress context +- `navigateTree()` - Branch conversation + +**State**: +- `setModel()` - Change model +- `setThinkingLevel()` - Change reasoning level +- `setTools()` / `setActiveTools()` - Manage tools + +### Key Questions to Answer + +✅ What's the difference between steer() and followUp()? +✅ How does branching work? +✅ How does compaction work? +✅ What's the relationship between Session and SessionStorage? +✅ What's the difference between MessageEntry and CustomEntry? +✅ How are pending writes handled during active turns? +✅ What hooks does AgentHarness provide? + +### Exercises + +1. Create a session, add messages, and build context +2. Implement branching and navigate between branches +3. Implement compaction and verify it works +4. Set up hooks for tool call logging + +--- + +## Phase 5: Session Architecture (2-3 hours) + +### Goal: Understand persistence and tree structure + +### Resources +1. **04-SESSION-ARCHITECTURE.md** - Study session system +2. Read `src/harness/session/session.ts` + +### Key Concepts + +- **SessionTreeEntry** - Tree nodes +- **Path Tracing** - From leaf to root +- **Context Building** - Projection to messages +- **Default Transform** - Compaction logic +- **Forking** - Create branches + +### Key Questions to Answer + +✅ How is conversation history stored? +✅ What's the difference between ID and parentId? +✅ How does the session know the current head? +✅ What entries appear in the LLM context? +✅ How does compaction work at the session level? +✅ What's the difference between fork and navigateTree()? +✅ How are custom entries different from messages? + +### Exercises + +1. Create a session and trace its tree +2. Add custom entries and verify they don't appear in context +3. Fork a session and compare contexts +4. Compact a session and verify size reduction + +--- + +## Phase 6: Tool Execution (2-3 hours) + +### Goal: Understand how tools work + +### Resources +1. **05-TOOL-EXECUTION.md** - Study tool system +2. Read `src/harness/tools/` - Built-in tools + +### Key Concepts + +- **AgentTool** - Tool definition +- **Tool Execution Flow** - Prepare → Execute → Finalize +- **Sequential vs Parallel** - Execution modes +- **Streaming Updates** - Progress updates +- **Error Handling** - Throw vs return error + +### Tool Execution Flow + +``` +prepareToolCall() + ├─ Find tool + ├─ Validate args + └─ beforeToolCall() + +executePreparedToolCall() + └─ tool.execute() + +finalizeExecutedToolCall() + └─ afterToolCall() + +emitToolResult() +``` + +### Key Questions to Answer + +✅ What's the difference between prepareArguments and execute? +✅ How do streaming updates work? +✅ When do you throw vs return an error? +✅ How are sequential vs parallel tools different? +✅ What's in the ToolContext passed to execute()? +✅ How do you handle long-running operations? +✅ What's the terminate flag for? + +### Exercises + +1. Implement a custom tool (e.g., weather API) +2. Implement streaming updates for long operation +3. Implement tool with error handling +4. Test sequential vs parallel execution + +--- + +## Phase 7: Data Flow (2-3 hours) + +### Goal: Understand how data flows through the system + +### Resources +1. **07-DATA-FLOW-STATE.md** - Study data flow +2. Read `src/agent.ts` - State management + +### Key Concepts + +- **AgentMessage** - Unified message type +- **AgentEvent** - Event stream +- **AgentContext** - Snapshot for LLM +- **State Mutation** - How state changes on events +- **Queue Flow** - Steering and follow-up + +### Key Questions to Answer + +✅ How do messages flow from input to LLM? +✅ How is state mutated on events? +✅ What's the difference between AgentContext and AgentState? +✅ How do hooks transform data? +✅ How are abort signals propagated? +✅ What's the relationship between queue mode and draining? +✅ How are pending writes handled? + +### Exercises + +1. Trace a message through the entire flow +2. Trace a tool call through all hooks +3. Trace an abort through the system +4. Draw the complete data flow diagram + +--- + +## Phase 8: Implementation (4-6 hours) + +### Goal: Implement your own version + +### Steps + +1. **Design your data structures** (in Julia) + - AgentMessage equivalent + - AgentEvent equivalent + - AgentTool equivalent + +2. **Implement core agent loop** + - Message streaming + - Tool execution + - Event emission + +3. **Add hooks system** + - Hook registration + - Hook execution + - Return value handling + +4. **Implement session persistence** + - Tree structure + - Entry types + - Context building + +5. **Add harness layer** + - High-level API + - Queue management + - Branching + +### Recommended Order + +``` +1. Data Types (2h) + ├─ AgentMessage + ├─ AgentEvent + └─ AgentTool + +2. Core Loop (4h) + ├─ streamAssistantResponse + ├─ executeToolCalls + └─ runLoop + +3. State Management (2h) + ├─ AgentState + └─ Event handlers + +4. Hooks (3h) + ├─ Hook system + └─ Implement hooks + +5. Session (4h) + ├─ Tree structure + ├─ Persistence + └─ Context building + +6. Harness (4h) + ├─ Public API + ├─ Queue management + └─ Branching +``` + +### Tips + +- Start simple, iterate +- Test each component +- Follow TypeScript patterns +- Use your language's idioms + +--- + +## Quick Reference + +### Agent Layer + +| Function | Purpose | +|----------|---------| +| `runAgentLoop()` | Start new conversation | +| `runAgentLoopContinue()` | Continue existing | +| `runLoop()` | Main iteration | +| `streamAssistantResponse()` | Stream LLM | +| `executeToolCalls()` | Execute tools | + +### AgentHarness Layer + +| Method | Purpose | +|--------|---------| +| `prompt()` | Run conversation | +| `steer()` | Interrupt agent | +| `followUp()` | Queue message | +| `compact()` | Compress context | +| `navigateTree()` | Branch conversation | + +### Hooks + +| Hook | Purpose | +|------|---------| +| `convertToLlm` | Convert messages | +| `transformContext` | Manipulate context | +| `beforeToolCall` | Block tools | +| `afterToolCall` | Override results | +| `shouldStopAfterTurn` | Request stop | +| `prepareNextTurn` | Update config | +| `getSteeringMessages` | Interrupt | +| `getFollowUpMessages` | Queue for later | + +### Entry Types + +| Type | Purpose | +|------|---------| +| `message` | User/assistant/toolResult | +| `model_change` | Model switch | +| `thinking_level_change` | Reasoning level | +| `active_tools_change` | Tools change | +| `compaction` | History summary | +| `branch_summary` | Branch marker | +| `custom` | App data | +| `custom_message` | Custom message | +| `label` | User label | +| `leaf` | Current head | + +--- + +## Common Patterns + +### 1. Context Window Management + +```typescript +transformContext: async (messages, signal) => { + if (estimateTokens(messages) > MAX_TOKENS) { + return pruneOldMessages(messages); + } + return messages; +} +``` + +### 2. Tool Permission Checks + +```typescript +beforeToolCall: async ({ toolCall, args }, signal) => { + if (toolCall.name === "bash" && !await canExecute(args)) { + return { block: true, reason: "Permission denied" }; + } + return undefined; +} +``` + +### 3. Streaming Updates + +```typescript +execute: async (id, params, signal, onUpdate) => { + for await (const chunk of process()) { + onUpdate({ content: [{ type: "text", text: `Progress: ${chunk}%` }] }); + } + return finalResult; +} +``` + +### 4. Branching + +```typescript +const branchSession = await session.fork(leafId); +const branchHarness = new AgentHarness({ session: branchSession }); +``` + +--- + +## Study Schedule + +| Week | Focus | Hours | +|------|-------|-------| +| 1 | Phases 1-2 | 6-8 | +| 2 | Phases 3-4 | 8-10 | +| 3 | Phases 5-6 | 6-8 | +| 4 | Phase 7-8 | 8-10 | + +**Total**: 28-36 hours + +--- + +## Next Steps + +After understanding the architecture: + +1. **Implement in Julia** + - Start with data types + - Implement core loop + - Add hooks + - Implement session + +2. **Extend Functionality** + - Add new tool types + - Implement custom hooks + - Add new entry types + +3. **Optimize** + - Improve token estimation + - Optimize context pruning + - Parallelize operations + +4. **Production** + - Error handling + - Logging + - Monitoring + +--- + +## Questions to Test Understanding + +1. How would you implement a tool that requires user approval? +2. How would you implement conversation summarization every 10 turns? +3. How would you implement context pruning based on importance? +4. How would you implement branching with automatic summaries? +5. How would you implement tool execution rate limiting? + +--- + +## Summary + +**Top-down learning**: +1. Big picture (layers, components) +2. Core agent (loop, streaming) +3. Hooks (customization) +4. Harness (session, persistence) +5. Data flow (how everything connects) + +**Key insight**: The system is built on **messages** and **events** with hooks for customization. diff --git a/packages/agent/learning/09-DIAGRAMS.md b/packages/agent/learning/09-DIAGRAMS.md new file mode 100644 index 00000000..9ea36970 --- /dev/null +++ b/packages/agent/learning/09-DIAGRAMS.md @@ -0,0 +1,695 @@ +# Pi Agent Architecture - Visual Diagrams + +## 1. System Architecture + +``` +┌─────────────────────────────────────────────────────────────────────────────────────────────┐ +│ APPLICATION LAYER │ +│ │ +│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ Agent User │ │ AgentHarness │ │ AgentHarness │ │ +│ │ (Low-Level) │ │ (High-Level) │ │ (Custom App) │ │ +│ └────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘ │ +│ │ │ │ │ +│ └────────┬───────────────┴───────────────────────┬┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ Agent Core │ │ AgentHarness │ │ +│ │ │ │ │ │ +│ │ • State mgmt │ │ • Session │ │ +│ │ • Event stream │ │ • Compaction │ │ +│ │ • Queue mgmt │ │ • Branching │ │ +│ │ • Hook system │ │ • Skills │ │ +│ └────────┬─────────┘ └────────┬─────────┘ │ +└────────────────────┼─────────────────────────────────────┼─────────────────────────────────┘ + │ │ + ┌────────────┴────────────┐ ┌──────────────┴──────────────┐ + │ │ │ │ + ▼ ▼ ▼ ▼ +┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ +│ Agent Loop │ │ Agent Context │ │ Agent State │ │ Agent Event │ +│ │ │ │ │ │ │ │ +│ • runAgentLoop │ │ • Messages │ │ • Tools │ │ • agent_start │ +│ • runLoop │ │ • System prompt │ │ • Messages │ │ • agent_end │ +│ • streamResponse │ │ • Tools │ │ • isStreaming │ │ • turn_start │ +│ • executeTools │ │ │ │ • pendingCalls │ │ • turn_end │ +└────────┬─────────┘ └──────────────────┘ └──────────────────┘ │ • message_start │ + │ │ • message_update │ + ▼ │ • message_end │ +┌───────────────────────────────────────────────────────────────────────▼───────────────────┐ +│ AGENT CORE (agent.ts, agent-loop.ts) │ +└─────────────────────────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────────────────────────┐ +│ SESSION LAYER │ +│ │ +│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ Session │ │ SessionStorage │ │ SessionRepo │ │ +│ │ │ │ │ │ │ │ +│ │ • Tree structure │ │ • Memory │ │ • Create │ │ +│ │ • Context build │ │ • JSONL │ │ • Open │ │ +│ │ • Branching │ │ │ │ • List │ │ +│ │ • Compaction │ │ │ │ • Fork │ │ +│ └────────┬─────────┘ └──────────────────┘ └──────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌───────────────────────────────────────────────────────────────────────────────────┐ │ +│ │ Session Tree │ │ +│ │ │ │ +│ │ root (null) │ │ +│ │ ├─ message [id:1] ← User prompt │ │ +│ │ │ └─ message [id:2] ← Assistant response │ │ +│ │ │ └─ tool_result [id:3] ← Tool call result │ │ +│ │ │ └─ message [id:4] ← User continuation │ │ +│ │ │ └─ compaction [id:5] ← History summarized │ │ +│ │ │ ├─ retained: [msg6, msg7] ← Recent messages kept │ │ +│ │ │ └─ message [id:8] ← After compaction │ │ +│ │ │ └─ leaf [id:9] ← Current head (cursor) │ │ +│ │ │ │ │ +│ │ └─ branch_summary [id:10] ← Branch point with summary │ │ +│ │ └─ message [id:11] ← New branch message │ │ +│ │ └─ leaf [id:12] ← New branch head │ │ +│ │ │ │ +│ └────────────────────────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────────────────────────┐ +│ LLM PROVIDER LAYER │ +│ │ +│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ StreamFn │ │ Models API │ │ Provider API │ │ +│ │ │ │ │ │ │ │ +│ │ • streamSimple │ │ • completeSimple │ │ • OpenAI │ │ +│ │ • completeSimple │ │ • Models catalog │ │ • Anthropic │ │ +│ │ │ │ │ │ • Custom │ │ +│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 2. Message Flow Diagram + +``` +┌─────────────────────────────────────────────────────────────────────────────────────────────┐ +│ PROMPT FLOW │ +└─────────────────────────────────────────────────────────────────────────────────────────────┘ + +User Input + │ + ├─► string: "Build a web app" + │ + ├─► AgentMessage: { role: "user", content: [...] } + │ + └─► AgentMessage[]: [{...}, {...}] + │ + ▼ +Agent.prompt(input) + │ + ├─► normalizePromptInput() + │ ├─► string → { role: "user", content: [{ type: "text", text: input }] } + │ ├─► AgentMessage → [message] + │ └─► AgentMessage[] → messages + │ + └─► runPromptMessages() + │ + └─► runWithLifecycle() + │ + ├─► Set isStreaming = true + ├─► Create abort controller + │ + └─► runAgentLoop() + │ + ├─► emit: agent_start + ├─► emit: turn_start + ├─► emit: message_start (user prompt) + ├─► emit: message_end (user prompt) + │ + └─► runLoop() + │ + ├─► Check steering queue (drain if any) + ├─► Check follow-up queue (skip if first turn) + │ + └─► streamAssistantResponse() + │ + ├─► transformContext() [optional] + │ └─► AgentMessage[] → AgentMessage[] + │ + ├─► convertToLlm() + │ └─► AgentMessage[] → Message[] + │ + ├─► Build LLM Context + │ └─► { systemPrompt, messages, tools } + │ + ├─► Resolve API key (from hook) + │ + └─► Call streamFn() + │ + ├─► LLM Provider API + │ + └─► AssistantMessageEventStream + │ + ├─► message_start (assistant) + ├─► message_update (text chunk 1) + ├─► message_update (text chunk 2) + ├─► message_update (toolCall) + └─► message_end (assistant) + │ + └─► executeToolCalls() + │ + ├─► Sequential mode: tool calls one-by-one + │ + └─► Parallel mode: tool calls concurrently + │ + ├─► prepareToolCall() + │ ├─► Find tool by name + │ ├─► prepareArguments() [optional] + │ ├─► validateToolArguments() + │ └─► beforeToolCall() hook + │ ├─► Return {block: true, reason} + │ └─► Return undefined + │ + ├─► executePreparedToolCall() + │ ├─► onUpdate(partialResult) [streaming updates] + │ └─► tool.execute() + │ + └─► finalizeExecutedToolCall() + └─► afterToolCall() hook + ├─► Override: content, details, isError, usage + └─► Use executed result + │ + └─► Emit: tool_execution_start/update/end + │ + └─► Create ToolResultMessage + │ + └─► Emit: message_start/end (toolResult) + │ + └─► turn_end + │ + ├─► prepareNextTurn() hook + │ └─► Return: context/model/thinkingLevel + │ + ├─► shouldStopAfterTurn() hook + │ └─► Return: boolean + │ + ├─► Drain steering queue + │ └─► getSteeringMessages() → inject + │ + └─► Drain follow-up queue + └─► getFollowUpMessages() → inject + │ + ├─► Steering/follow-up exists? → Repeat from streamAssistantResponse() + └─► No more messages → emit: agent_end + │ + └─► finishRun() + └─► isStreaming = false + +┌─────────────────────────────────────────────────────────────────────────────────────────────┐ +│ CONTINUATION FLOW │ +└─────────────────────────────────────────────────────────────────────────────────────────────┘ + +Agent.continue() + │ + ├─► Validate last message (must be user/toolResult) + │ + └─► runAgentLoopContinue() + │ + └─► runLoop() from current context (no new prompts) + │ + └─► Same flow as above, starting from current context +``` + +--- + +## 3. Hook System Flow + +``` +┌─────────────────────────────────────────────────────────────────────────────────────────────┐ +│ HOOK EXECUTION ORDER │ +└─────────────────────────────────────────────────────────────────────────────────────────────┘ + +AgentHarness.prompt() + │ + ├─► before_agent_start (harness hook) + │ └─► Can return: messages, systemPrompt + │ + ├─► transformContext() (agent hook) + │ └─► AgentMessage[] → AgentMessage[] + │ + └─► streamAssistantResponse() + │ + ├─► before_provider_request (harness hook) + │ └─► Can modify: streamOptions + │ + ├─► convertToLlm() (agent hook) + │ └─► AgentMessage[] → Message[] + │ + ├─► streamFn() + │ + └─► message_end (assistant) + │ + └─► executeToolCalls() + │ + ├─► For each tool call: + │ + │ ├─► tool_call (harness hook) + │ │ └─► Can return: block, reason + │ │ + │ ├─► executePreparedToolCall() + │ │ + │ └─► tool_result (harness hook) + │ └─► Can return: content, details, isError, usage, terminate + │ + └─► turn_end + │ + ├─► shouldStopAfterTurn() (agent hook) + │ └─► Return: boolean + │ + ├─► prepareNextTurn() (agent hook) + │ └─► Return: context/model/thinkingLevel + │ + ├─► Drain steering queue + │ └─► getSteeringMessages() (agent hook) + │ + └─► Drain follow-up queue + └─► getFollowUpMessages() (agent hook) + │ + ├─► Continue? → Repeat from streamAssistantResponse() + └─► Stop? → agent_end (harness hook) +``` + +--- + +## 4. Tool Execution Flow + +``` +┌─────────────────────────────────────────────────────────────────────────────────────────────┐ +│ TOOL EXECUTION FLOW │ +└─────────────────────────────────────────────────────────────────────────────────────────────┘ + +Assistant Message with Tool Call + │ + ├─► { type: "toolCall", id: "tc_123", name: "bash", arguments: { command: "ls" } } + │ + ▼ +prepareToolCall() + │ + ├─► Find tool in currentContext.tools + │ └─► Not found? → immediate error result + │ + ├─► prepareToolCallArguments() [optional shim] + │ └─► Transform arguments before validation + │ + ├─► validateToolArguments() + │ └─► Validate against tool parameters schema + │ + └─► beforeToolCall() hook + │ + ├─► Return { block: true, reason: "..." } + │ └─► Emit: tool_execution_start/update/end (error) + │ └─► Tool NOT executed + │ + └─► Return undefined + │ + ▼ + executePreparedToolCall() + │ + ├─► tool.execute(toolCallId, validatedArgs, signal, onUpdate) + │ │ + │ ├─► Long-running operation + │ │ └─► onUpdate({ content: [...], details: {...} }) + │ │ └─► Emit: tool_execution_update + │ │ + │ └─► Return: { content, details, usage, ... } + │ + └─► Return: { result, isError } + │ + ▼ + finalizeExecutedToolCall() + │ + └─► afterToolCall() hook + │ + ├─► Return override: { content, details, isError, usage, terminate } + │ └─► Merge: result = { ...result, ...override } + │ + └─► Return: { toolCall, result, isError } + │ + ▼ + emitToolExecutionEnd() + │ + └─► Emit: tool_execution_end + │ + ▼ + createToolResultMessage() + │ + └─► Create ToolResultMessage + ├─► toolCallId: tc_123 + ├─► toolName: bash + ├─► content: result.content + ├─► details: result.details + ├─► usage: result.usage + ├─► isError: result.isError + └─► timestamp: Date.now() + │ + ▼ + emitToolResultMessage() + │ + ├─► Emit: message_start (toolResult) + └─► Emit: message_end (toolResult) + │ + ▼ + Push to context.messages + │ + ▼ + Available for next LLM call +``` + +--- + +## 5. Session Tree Navigation + +``` +┌─────────────────────────────────────────────────────────────────────────────────────────────┐ +│ SESSION BRANCHING │ +└─────────────────────────────────────────────────────────────────────────────────────────────┘ + +Original Session Tree: + │ + ├─ root + │ └─ message [user #1] [id: 1] + │ └─ message [assistant #1] [id: 2] + │ └─ tool_result [id: 3] + │ └─ message [user #2] [id: 4] + │ └─ leaf [id: 5] ← Current head + │ + ▼ +Navigate to entry [id: 2] with summarize=true + │ + ├─► Collect entries from leaf to target + │ └─► [leaf, msg4, tool_result, msg2] (path) + │ + ├─► Common ancestor: root + │ + ├─► Entries to summarize: [msg4, tool_result] + │ + ├─► Generate branch summary via LLM + │ + ├─► Create branch_summary entry + │ └─► { type: "branch_summary", summary: "...", fromId: 2 } + │ + └─► Fork session at target [id: 2] + │ + ├─► Clone entries up to target + │ └─► [root, msg1, msg2, branch_summary] + │ + └─► Set new leaf to [id: 2] + │ + ▼ + New Session Tree: + │ + ├─ root + │ └─ message [user #1] [id: 1] + │ └─ message [assistant #1] [id: 2] + │ └─ branch_summary [id: 6] ← New branch point + │ └─ leaf [id: 7] ← New head + │ + └─ Original branch (still exists) + └─ message [user #2] [id: 4] + └─ tool_result [id: 3] + └─ leaf [id: 5] ← Old head +``` + +--- + +## 6. Context Window Compaction + +``` +┌─────────────────────────────────────────────────────────────────────────────────────────────┐ +│ CONTEXT COMPACTION │ +└─────────────────────────────────────────────────────────────────────────────────────────────┘ + +Original Context (10,000 tokens): + │ + ├─ message [user #1] + ├─ message [assistant #1] + ├─ tool_result [id: 1] + ├─ message [user #2] + ├─ message [assistant #2] + ├─ tool_result [id: 2] + ├─ message [user #3] + ├─ message [assistant #3] + ├─ tool_result [id: 3] + ├─ message [user #4] + ├─ message [assistant #4] + ├─ tool_result [id: 4] + ├─ message [user #5] + ├─ message [assistant #5] + └─ leaf [current] + │ + ▼ +Compact (threshold: 8,000 tokens) + │ + ├─► prepareCompaction() + │ │ + │ ├─► Estimate tokens: 10,000 + │ ├─► Target: 6,000 (80% of 8,000) + │ ├─► Find cut point: after message [assistant #3] + │ ├─► Messages to summarize: [msg1, msg2, ..., msg3] + │ └─► Retained tail: [msg4, msg5, leaf] + │ + ├─► LLM call to generate summary + │ + └─► Create compaction entry + │ + ├─► summary: "User asked X, assistant did Y, then Z..." + ├─► firstKeptEntryId: msg4.id + ├─► tokensBefore: 10,000 + ├─► retainedTail: [msg4, msg5, leaf] + └─► details: { readFiles: [...], modifiedFiles: [...] } + │ + ▼ + Persisted Session Tree: + │ + ├─ root + │ └─ message [user #1] + │ └─ ... (original entries) + │ └─ compaction [id: new] ← New entry + │ ├─ summary: "User asked X..." + │ ├─ firstKeptEntryId: msg4.id + │ ├─ tokensBefore: 10000 + │ ├─ retainedTail: [msg4, msg5, leaf] + │ └─ details: {...} + │ └─ msg4 [id: msg4] + │ └─ message [assistant #4] + │ └─ tool_result [id: 4] + │ └─ message [user #5] + │ └─ message [assistant #5] + │ └─ leaf [id: leaf] + │ + └─ Context for LLM: + └─ [compaction summary, retainedTail messages] +``` + +--- + +## 7. State Mutation Flow + +``` +┌─────────────────────────────────────────────────────────────────────────────────────────────┐ +│ STATE MUTATION ON EVENTS │ +└─────────────────────────────────────────────────────────────────────────────────────────────┘ + +Agent State: + │ + ├─ systemPrompt: string + ├─ model: Model + ├─ thinkingLevel: ThinkingLevel + ├─ tools: AgentTool[] + ├─ messages: AgentMessage[] + ├─ isStreaming: boolean + ├─ streamingMessage: AgentMessage? ← Partial assistant message + ├─ pendingToolCalls: Set ← Currently executing + └─ errorMessage: string? + │ + ▼ +Events and State Changes: + │ + ├─ agent_start + │ ├─ isStreaming = true + │ ├─ streamingMessage = undefined + │ └─ errorMessage = undefined + │ + ├─ message_start (user/assistant/toolResult) + │ └─ No state change (just event emission) + │ + ├─ message_update (assistant only) + │ └─ streamingMessage = updatedMessage + │ + ├─ message_end + │ ├─ streamingMessage = undefined + │ └─ messages.push(message) + │ + ├─ tool_execution_start + │ └─ pendingToolCalls.add(toolCallId) + │ + ├─ tool_execution_end + │ └─ pendingToolCalls.delete(toolCallId) + │ + ├─ turn_end + │ └─ if (message.errorMessage) errorMessage = message.errorMessage + │ + └─ agent_end + ├─ streamingMessage = undefined + └─ (run finishes, state cleared on finishRun()) +``` + +--- + +## 8. Queue Flow + +``` +┌─────────────────────────────────────────────────────────────────────────────────────────────┐ +│ QUEUE DRAINING FLOW │ +└─────────────────────────────────────────────────────────────────────────────────────────────┘ + +Steering Queue (mode: "one-at-a-time"): + │ + ├─ Queue: [msg1, msg2, msg3] + │ + ├─ After turn ends: + │ + ├─► getSteeringMessages() + │ ├─► mode = "one-at-a-time" + │ ├─► Drain: [msg1] + │ └─► Queue remaining: [msg2, msg3] + │ + ├─► Inject msg1 into context + │ + └─► Next LLM call includes: [...original, msg1] + │ + ▼ + After next turn: + │ + ├─► getSteeringMessages() + │ ├─► Drain: [msg2] + │ └─► Queue remaining: [msg3] + │ + └─► Inject msg2 into context + │ + └─► ... and so on until queue empty + +Follow-up Queue (mode: "all"): + │ + ├─ Queue: [msg1, msg2, msg3] + │ + ├─ Agent would stop (no more tool calls) + │ + ├─► getFollowUpMessages() + │ ├─► mode = "all" + │ ├─► Drain: [msg1, msg2, msg3] + │ └─► Queue remaining: [] + │ + ├─► Set as pendingMessages + │ + └─► Inner loop continues with: [...original, msg1, msg2, msg3] + │ + └─► All three messages injected together +``` + +--- + +## 9. Event Sequence Examples + +### Example 1: Simple Prompt + +``` +agent_start +turn_start +message_start (user: "Hello") +message_end (user: "Hello") +message_start (assistant: "") +message_update (assistant: "H") +message_update (assistant: "He") +message_update (assistant: "Hel") +message_update (assistant: "Hell") +message_update (assistant: "Hello") +message_end (assistant: "Hello") +turn_end +agent_end +``` + +### Example 2: Tool Execution + +``` +agent_start +turn_start +message_start (user: "List files") +message_end (user: "List files") +message_start (assistant: "") +message_update (assistant: "") +message_update (assistant: "") +message_update (assistant: "") +message_update (assistant: "") +message_update (assistant: "") +message_end (assistant: "") +tool_execution_start (bash: { command: "ls -la" }) +tool_execution_update (bash: { progress: 0 }) +tool_execution_update (bash: { progress: 50 }) +tool_execution_update (bash: { progress: 100 }) +tool_execution_end (bash: { exitCode: 0 }) +message_start (toolResult: "drwxr-xr-x...") +message_end (toolResult: "drwxr-xr-x...") +turn_end +agent_end +``` + +### Example 3: Steering + +``` +agent_start +turn_start +message_start (user: "Build app") +message_end (user: "Build app") +message_start (assistant: "") +message_update (assistant: "Building...") +turn_end +│ +├─ User types: "Wait, use React" +│ +├─► steer("Wait, use React") +│ └─► steeringQueue.push(msg) +│ +├─► getSteeringMessages() +│ └─► Drain and return [msg] +│ +├─► Inject into context +│ +└─► Next turn with: [...original, "Wait, use React"] + +turn_start +message_start (user: "Wait, use React") +message_end (user: "Wait, use React") +message_start (assistant: "") +message_update (assistant: "Using React...") +turn_end +agent_end +``` + +--- + +## Summary + +**Key Diagrams**: +1. System Architecture - Layers and components +2. Message Flow - Complete prompt flow +3. Hook System - Hook execution order +4. Tool Execution - Tool call lifecycle +5. Session Branching - Tree navigation +6. Context Compaction - History summarization +7. State Mutation - Event-driven state changes +8. Queue Flow - Steering and follow-up draining +9. Event Sequences - Real examples + +These diagrams show how data flows through the agent system from user input to LLM response to tool execution and back. diff --git a/packages/agent/learning/10-QUICK-START.md b/packages/agent/learning/10-QUICK-START.md new file mode 100644 index 00000000..055539d5 --- /dev/null +++ b/packages/agent/learning/10-QUICK-START.md @@ -0,0 +1,403 @@ +# Quick Start Guide + +## Learning the Pi Agent Architecture + +This guide helps you quickly understand the agent system and prepare for reimplementation in Julia. + +--- + +## Phase 1: Core Concepts (30 minutes) + +### 1. Two-Layer Architecture + +``` +┌────────────────────────────────────────────────────────────────────┐ +│ Application Layer │ +│ ┌──────────────┐ ┌─────────────────┐ ┌───────────────────────┐ │ +│ │ Agent (Core) │ │ AgentHarness │ │ Your Custom App │ │ +│ └───────┬──────┘ └────────┬────────┘ └─────────┬────────────┘ │ +└──────────┼───────────────────┼─────────────────────┼────────────────┘ + │ │ │ + ▼ ▼ ▼ + ┌──────────────┐ ┌─────────────────┐ ┌────────────────┐ + │ agent-loop.ts│ │ agent-harness.ts│ │ Session Repo │ + │ types.ts │ │ │ │ │ + └──────────────┘ └─────────────────┘ └────────────────┘ +``` + +**Key Insight**: +- **Agent Core** = Low-level async iteration (messages in, messages out) +- **AgentHarness** = High-level session management with persistence + +--- + +### 2. Core Data Types + +```typescript +// Message: Basic unit of conversation +interface Message { + role: "user" | "assistant" | "toolResult"; + content: (TextContent | ImageContent)[]; + timestamp: number; +} + +// Tool: Function the agent can call +interface AgentTool { + name: string; + label: string; + description: string; + parameters: Schema; + execute(toolCallId, params, signal, onUpdate): Promise; +} + +// Event: Notification of state changes +type AgentEvent = + | { type: "agent_start" } + | { type: "agent_end"; messages: Message[] } + | { type: "turn_start" } + | { type: "turn_end"; message: Message; toolResults: Message[] } + | { type: "message_start"; message: Message } + | { type: "message_update"; message: Message } + | { type: "message_end"; message: Message } + | { type: "tool_execution_start"; ... } + | { type: "tool_execution_end"; ... }; +``` + +--- + +## Phase 2: Message Flow (45 minutes) + +### The Agent Loop + +``` +1. User Input + └─► Agent.prompt("Hello") + +2. Agent Start + └─► emit: agent_start, turn_start, message_start/end (user) + +3. LLM Streaming + └─► streamAssistantResponse() + └─► transformContext() → convertToLlm() → streamFn() + +4. Tool Execution + └─► executeToolCalls() + └─► prepare → execute → finalize (for each tool) + +5. Turn End + └─► emit: turn_end + └─► Check hooks, drain queues, decide next turn + +6. Repeat or End + └─► Loop continues until no more work +``` + +### Key Insight + +**Everything is a message**: User input, assistant response, tool calls, tool results, steering messages. + +**Everything is an event**: State changes are emitted as events for UI updates. + +--- + +## Phase 3: Hooks System (30 minutes) + +### Hook Categories + +| Category | Purpose | When Called | +|----------|---------|-------------| +| `convertToLlm` | Filter/transform messages | Before LLM call | +| `transformContext` | Manipulate context | Before LLM call | +| `beforeToolCall` | Block tool execution | Before tool runs | +| `afterToolCall` | Override tool results | After tool runs | +| `shouldStopAfterTurn` | Request early stop | After turn ends | +| `prepareNextTurn` | Update config | Before next turn | +| `getSteeringMessages` | Interrupt agent | After turn ends | +| `getFollowUpMessages` | Queue messages | When agent stops | + +### Hook Flow + +``` +Agent.prompt("Build app") + │ + ├─► transformContext() [hook] + │ + ├─► convertToLlm() [hook] + │ + ├─► LLM call + │ + ├─► executeToolCalls() + │ ├─► beforeToolCall() [hook] + │ ├─► tool.execute() + │ └─► afterToolCall() [hook] + │ + └─► turn_end + ├─► shouldStopAfterTurn() [hook] + ├─► prepareNextTurn() [hook] + ├─► getSteeringMessages() [hook] + └─► getFollowUpMessages() [hook] +``` + +--- + +## Phase 4: AgentHarness (45 minutes) + +### High-Level API + +```typescript +// Create harness +const harness = new AgentHarness({ + session: session, + models: models, + tools: [weatherTool, gitTool], + activeToolNames: ["weather", "git"], + model: gpt4Model, + thinkingLevel: "medium" +}); + +// Main operations +await harness.prompt("What's the weather in London?"); + +// Queue management +await harness.steer("Wait, check this first"); // Interrupt +await harness.followUp("Now summarize"); // After agent stops +await harness.nextTurn("Also deploy"); // Next turn + +// Session management +await harness.compact(); // Compress context +await harness.navigateTree(entryId); // Branch conversation +``` + +### Session Tree + +``` +Session = Conversation History as a Tree + +root + ├─ message [user prompt #1] + │ └─ message [assistant #1] + │ └─ tool_result [result] + │ └─ message [user prompt #2] + │ └─ compaction [summary] + │ ├─ retained: [recent messages] + │ └─ message [assistant continues] + │ └─ leaf [current head] + │ + └─ branch_summary [point where branch created] + └─ message [new branch] + └─ leaf [new head] +``` + +**Key Operations**: +- `buildContext()` → Get LLM context from tree +- `fork()` → Create branch at point +- `compact()` → Summarize history + +--- + +## Phase 5: Tool Execution (30 minutes) + +### Tool Lifecycle + +``` +1. LLM sends tool call + └─► AssistantMessage with toolCall block + +2. prepareToolCall() + ├─► Find tool by name + ├─► Validate arguments + └─► beforeToolCall() hook + +3. executePreparedToolCall() + └─► tool.execute() with onUpdate callback + +4. finalizeExecutedToolCall() + └─► afterToolCall() hook + +5. Emit events + ├─► tool_execution_start + ├─► tool_execution_update (streaming) + └─► tool_execution_end +``` + +### Tool Definition + +```typescript +const weatherTool: AgentTool = { + name: "get_weather", + label: "Get Weather", + description: "Get current weather for a city", + parameters: Type.Object({ city: Type.String() }), + execute: async (toolCallId, params, signal, onUpdate) => { + // Check for abort + if (signal?.aborted) throw new Error("Aborted"); + + // Long operation with streaming + const result = await fetchWeather(params.city); + onUpdate({ content: [{ type: "text", text: "Fetching..." }] }); + + return { + content: [{ type: "text", text: result }], + details: { city: params.city, temp: result.temp }, + usage: { input: 0, output: 0, ... } + }; + } +}; +``` + +--- + +## Phase 6: Session Persistence (30 minutes) + +### Entry Types + +| Type | Purpose | +|------|---------| +| `message` | User/assistant/toolResult | +| `model_change` | Model switch | +| `thinking_level_change` | Reasoning level | +| `active_tools_change` | Tools change | +| `compaction` | History summary | +| `branch_summary` | Branch point | +| `custom` | App data (not visible to model) | +| `custom_message` | Custom message | +| `label` | User label | +| `leaf` | Current head | + +### Context Building + +```typescript +// 1. Get path from leaf to root +const pathEntries = await session.getBranch(); + +// 2. Apply transforms (compaction) +const contextEntries = defaultContextEntryTransform(pathEntries); + +// 3. Project entries to messages +const messages = contextEntries.flatMap(sessionEntryToContextMessages); + +// 4. Derive state (model, thinking level, active tools) +const state = deriveSessionContextState(pathEntries); + +// 5. Return context +return { ...state, messages }; +``` + +--- + +## Summary + +### What to Remember + +1. **Two layers**: Agent (core) + AgentHarness (high-level) +2. **Messages everywhere**: Input, output, tools, events +3. **Hooks for customization**: Transform messages, block tools, override results +4. **Session = Tree**: Persistent conversation history with branching +5. **Events for UI**: All state changes emitted as events +6. **Tool lifecycle**: Prepare → Execute → Finalize → Emit + +### Next Steps + +1. **Read the detailed docs**: + - `01-ARCHITECTURE-OVERVIEW.md` - Big picture + - `02-AGENT-LOOP-DETAILED.md` - Core loop + - `03-HOOK-SYSTEM.md` - Hooks reference + - `04-SESSION-ARCHITECTURE.md` - Session system + - `05-TOOL-EXECUTION.md` - Tool system + - `06-AGENTHARNESS-REFERENCE.md` - API reference + - `07-DATA-FLOW-STATE.md` - Data flow + - `08-LEARNING-PATH.md` - Study guide + - `09-DIAGRAMS.md` - Visual diagrams + +2. **Design your Julia implementation**: + - Data types + - Core agent loop + - Hook system + - Session persistence + - Tool execution + +3. **Start coding**: + - Implement basic types + - Implement core loop + - Add hooks + - Add session + - Add harness + +### Common Patterns + +**Context window management**: +```typescript +transformContext: async (messages) => { + if (estimateTokens(messages) > MAX_TOKENS) { + return pruneOldMessages(messages); + } + return messages; +} +``` + +**Tool permission checks**: +```typescript +beforeToolCall: async ({ toolCall }) => { + if (toolCall.name === "bash" && !await canExecute()) { + return { block: true, reason: "Permission denied" }; + } + return undefined; +} +``` + +**Streaming updates**: +```typescript +execute: async (id, params, signal, onUpdate) => { + for await (const chunk of process()) { + onUpdate({ content: [{ type: "text", text: `Progress: ${chunk}%` }] }); + } + return finalResult; +} +``` + +--- + +## Quick Reference + +### Agent Core (agent.ts, agent-loop.ts) + +| Function | Purpose | +|----------|---------| +| `runAgentLoop()` | Start new conversation | +| `runAgentLoopContinue()` | Continue existing | +| `runLoop()` | Main iteration | +| `streamAssistantResponse()` | Stream LLM | +| `executeToolCalls()` | Execute tools | + +### AgentHarness API + +| Method | Purpose | +|--------|---------| +| `prompt()` | Run conversation | +| `steer()` | Interrupt agent | +| `followUp()` | Queue message | +| `compact()` | Compress context | +| `navigateTree()` | Branch conversation | + +### Hook Types + +| Hook | Purpose | +|------|---------| +| `convertToLlm` | Convert messages | +| `beforeToolCall` | Block tools | +| `afterToolCall` | Override results | +| `shouldStopAfterTurn` | Request stop | + +### Entry Types + +| Type | Purpose | +|------|---------| +| `message` | Conversation messages | +| `compaction` | History summary | +| `branch_summary` | Branch point | + +--- + +**You now have the foundation to reimplement the agent in Julia!** + +Start with data types and the core loop, then add hooks, session, and harness layers incrementally. diff --git a/packages/agent/learning/11-COMPLETE-SUMMARY.md b/packages/agent/learning/11-COMPLETE-SUMMARY.md new file mode 100644 index 00000000..06557eaa --- /dev/null +++ b/packages/agent/learning/11-COMPLETE-SUMMARY.md @@ -0,0 +1,596 @@ +# Pi Agent Architecture - Complete Summary + +## Quick Reference for Julia Reimplementation + +--- + +## 1. Core Architecture (Top-Down) + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ APPLICATION LAYER │ +│ • Agent (Low-level) │ +│ • AgentHarness (High-level) │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ┌────────────────────┼────────────────────┐ + │ │ │ + ▼ ▼ ▼ +┌───────────────┐ ┌──────────────────┐ ┌──────────────────┐ +│ Agent Core │ │ Session System │ │ Tool Execution │ +│ • Async loop │ │ • Tree storage │ │ • Prepare │ +│ • Event │ │ • Branching │ │ • Execute │ +│ • Message │ │ • Compaction │ │ • Finalize │ +│ • Hooks │ │ • Context │ │ • Streaming │ +└───────────────┘ └──────────────────┘ └──────────────────┘ + │ │ │ + ▼ ▼ ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ LLM PROVIDER LAYER │ +│ • StreamFn (streaming interface) │ +│ • Models (LLM catalog) │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 2. Key Components + +### Agent Core + +**Files**: `src/agent.ts`, `src/agent-loop.ts`, `src/types.ts` + +**Responsibilities**: +- State management (messages, tools, isStreaming, pendingToolCalls) +- Event streaming (agent_start, turn_start, message_start, etc.) +- Queue management (steering, follow-up) +- Hook execution (beforeToolCall, afterToolCall, etc.) + +**Key Types**: +```typescript +type AgentMessage = Message | CustomAgentMessages +type AgentEvent = + | { type: "agent_start" } + | { type: "agent_end"; messages: AgentMessage[] } + | { type: "turn_start" } + | { type: "turn_end"; message: AgentMessage; toolResults: ToolResultMessage[] } + | { type: "message_start"; message: AgentMessage } + | { type: "message_update"; message: AgentMessage } + | { type: "message_end"; message: AgentMessage } + | { type: "tool_execution_start"; ... } + | { type: "tool_execution_end"; ... } + +interface AgentContext { + systemPrompt: string + messages: AgentMessage[] + tools?: AgentTool[] +} +``` + +### AgentHarness + +**Files**: `src/harness/agent-harness.ts` + +**Responsibilities**: +- Session persistence (JSONL/Memory) +- Branching (create conversation paths) +- Compaction (summarize history) +- Tool context binding +- Hook system (before_agent_start, tool_call, tool_result, etc.) +- Queue management (steer, followUp, nextTurn) + +**Key Types**: +```typescript +interface AgentHarnessEvent = + | { type: "agent_start" } // From core + | { type: "before_agent_start" } // Harness-specific + | { type: "tool_call"; ... } + | { type: "tool_result"; ... } + | { type: "session_before_compact"; ... } + | { type: "session_before_tree"; ... } + // ... more harness events + +interface SessionContext { + systemPrompt: string + messages: AgentMessage[] + thinkingLevel: ThinkingLevel + model: { provider: string; modelId: string } | null + activeToolNames: string[] | null +} +``` + +### Session System + +**Files**: `src/harness/session/` + +**Responsibilities**: +- Conversation persistence as tree +- Context building from tree +- Branching and forking +- Compaction +- Entry types (message, model_change, compaction, branch_summary, etc.) + +**Key Types**: +```typescript +interface SessionTreeEntry { + id: string + parentId: string | null + timestamp: string + type: string // "message", "compaction", "branch_summary", etc. +} + +interface CompactionEntry extends SessionTreeEntry { + type: "compaction" + summary: string + firstKeptEntryId?: string + tokensBefore: number + retainedTail?: AgentMessage[] +} +``` + +### Tool System + +**Files**: `src/harness/tools/` + +**Responsibilities**: +- Tool definition and execution +- Sequential vs parallel execution +- Streaming updates +- Error handling +- Before/after hooks + +**Key Types**: +```typescript +interface AgentTool extends Tool { + label: string + execute( + toolCallId: string, + params: Static, + signal?: AbortSignal, + onUpdate?: AgentToolUpdateCallback + ): Promise> +} + +interface AgentToolResult { + content: (TextContent | ImageContent)[] + details: T + usage?: Usage + addedToolNames?: string[] + terminate?: boolean +} +``` + +--- + +## 3. Message Flow + +``` +User Input + │ + ├─► Agent.prompt("Hello") + │ └─► normalizePromptInput() → AgentMessage[] + │ + └─► runWithLifecycle() + ├─► isStreaming = true + └─► runAgentLoop() + │ + ├─► agent_start + ├─► turn_start + ├─► message_start/end (user) + │ + ├─► streamAssistantResponse() + │ ├─► transformContext() [optional] + │ ├─► convertToLlm() + │ └─► streamFn() → LLM + │ + ├─► executeToolCalls() + │ ├─► prepareToolCall() + │ │ ├─► Find tool + │ │ ├─► Validate args + │ │ └─► beforeToolCall() [hook] + │ │ + │ ├─► executePreparedToolCall() + │ │ └─► tool.execute() with onUpdate + │ │ + │ └─► finalizeExecutedToolCall() + │ └─► afterToolCall() [hook] + │ + └─► turn_end + ├─► prepareNextTurn() [hook] + ├─► shouldStopAfterTurn() [hook] + ├─► Drain steering queue + └─► Drain follow-up queue + + ┌─► Continue? → Repeat + └─► Stop? → agent_end +``` + +--- + +## 4. Hook System + +| Hook | Layer | When | Can Block? | Use Case | +|------|-------|------|------------|----------| +| `convertToLlm` | Agent | Before LLM | No | Filter messages | +| `transformContext` | Agent | Before LLM | Yes | Prune context | +| `beforeToolCall` | Agent | Before tool | Yes | Permission checks | +| `afterToolCall` | Agent | After tool | Yes | Override results | +| `shouldStopAfterTurn` | Agent | After turn | Yes | Request early stop | +| `prepareNextTurn` | Agent | Before next | Yes | Update config | +| `getSteeringMessages` | Agent | After turn | Yes | Interrupt agent | +| `getFollowUpMessages` | Agent | When stop | Yes | Queue messages | + +**Harness Hooks**: +- `before_agent_start` - Modify system prompt +- `context` - Transform context +- `tool_call` - Log/before tool +- `tool_result` - Log/after tool +- `session_before_compact` - Customize compaction +- `session_before_tree` - Customize branching +- `before_provider_request` - Modify stream options +- `before_provider_payload` - Modify LLM payload + +--- + +## 5. Session Tree + +``` +root (parentId: null) + ├─ message [id: 1] ← User prompt + │ └─ message [id: 2] ← Assistant + │ └─ tool_result [id: 3] + │ └─ message [id: 4] + │ └─ compaction [id: 5] + │ ├─ summary: "..." + │ ├─ firstKeptEntryId: msg6.id + │ ├─ tokensBefore: 10000 + │ ├─ retainedTail: [msg6, msg7] + │ └─ msg6 [id: 6] ← Retained + │ └─ ... (rest of retained) + │ └─ leaf [id: 8] ← Current head + │ + └─ branch_summary [id: 9] ← Branch point + └─ message [id: 10] ← New branch + └─ leaf [id: 11] ← New head +``` + +**Key Operations**: +- `getBranch()` → Get entries from leaf to root +- `buildContext()` → Project entries to messages +- `fork()` → Create branch at entry +- `compact()` → Summarize history + +--- + +## 6. Tool Execution Flow + +``` +1. LLM sends tool call + └─► AssistantMessage with toolCall block + +2. prepareToolCall() + ├─► Find tool + ├─► prepareArguments() [optional] + ├─► validateToolArguments() + └─► beforeToolCall() [hook] + ├─► block: true → Error + └─► block: undefined → Continue + +3. executePreparedToolCall() + └─► tool.execute(toolCallId, params, signal, onUpdate) + ├─► onUpdate(partialResult) [streaming] + └─► Return: { content, details, ... } + +4. finalizeExecutedToolCall() + └─► afterToolCall() [hook] + ├─► Override: content, details, isError, usage, terminate + └─► Use executed result + +5. Emit events + ├─► tool_execution_start + ├─► tool_execution_update [streaming] + └─► tool_execution_end + │ + └─► createToolResultMessage() + └─► Emit: message_start/end (toolResult) +``` + +--- + +## 7. Data Types + +### Messages + +```typescript +interface Message { + role: "user" | "assistant" | "toolResult" + content: (TextContent | ImageContent)[] + api?: string + provider?: string + model?: string + usage?: Usage + stopReason?: StopReason + errorMessage?: string + timestamp: number +} + +interface TextContent { type: "text"; text: string } +interface ImageContent { type: "image"; mediaType: string; data: string } +``` + +### Events + +```typescript +type AgentEvent = + | { type: "agent_start" } + | { type: "agent_end"; messages: AgentMessage[] } + | { type: "turn_start" } + | { type: "turn_end"; message: AgentMessage; toolResults: ToolResultMessage[] } + | { type: "message_start"; message: AgentMessage } + | { type: "message_update"; message: AgentMessage; assistantMessageEvent: AssistantMessageEvent } + | { type: "message_end"; message: AgentMessage } + | { type: "tool_execution_start"; toolCallId: string; toolName: string; args: any } + | { type: "tool_execution_update"; toolCallId: string; toolName: string; args: any; partialResult: any } + | { type: "tool_execution_end"; toolCallId: string; toolName: string; result: any; isError: boolean } +``` + +### Tools + +```typescript +interface AgentTool extends Tool { + label: string + prepareArguments?: (args: unknown) => Static + execute( + toolCallId: string, + params: Static, + signal?: AbortSignal, + onUpdate?: AgentToolUpdateCallback + ): Promise> +} + +interface AgentToolResult { + content: (TextContent | ImageContent)[] + details: T + usage?: Usage + addedToolNames?: string[] + terminate?: boolean +} +``` + +--- + +## 8. State Management + +### Agent State + +```typescript +interface AgentState { + systemPrompt: string + model: Model + thinkingLevel: ThinkingLevel + tools: AgentTool[] + messages: AgentMessage[] + isStreaming: boolean + streamingMessage?: AgentMessage + pendingToolCalls: Set + errorMessage?: string +} +``` + +### State Mutations + +| Event | State Change | +|-------|-------------| +| `message_start` | `streamingMessage = message` | +| `message_update` | `streamingMessage = message` | +| `message_end` | `messages.push(message)`, `streamingMessage = undefined` | +| `tool_execution_start` | `pendingToolCalls.add(toolCallId)` | +| `tool_execution_end` | `pendingToolCalls.delete(toolCallId)` | +| `turn_end` | `errorMessage = message.errorMessage` (if error) | +| `agent_end` | `streamingMessage = undefined` | + +--- + +## 9. Queue System + +### Steering Queue + +**Purpose**: Interrupt agent while working + +**Mode**: `"all"` or `"one-at-a-time"` + +**Flow**: After turn ends → Drain → Inject into context → Next LLM call + +### Follow-up Queue + +**Purpose**: Queue messages for after agent stops + +**Mode**: `"all"` or `"one-at-a-time"` + +**Flow**: When agent would stop → Drain → Set as pending → Continue loop + +--- + +## 10. Entry Types + +| Type | Purpose | +|------|---------| +| `message` | User/assistant/toolResult messages | +| `model_change` | Model switch (`setModel()`) | +| `thinking_level_change` | Reasoning level (`setThinkingLevel()`) | +| `active_tools_change` | Tools change (`setActiveTools()`) | +| `compaction` | History summary (`compact()`) | +| `branch_summary` | Branch point (branching) | +| `custom` | App data (not visible to model) | +| `custom_message` | Custom message | +| `label` | User-assigned label | +| `leaf` | Current session head | + +--- + +## 11. Common Patterns + +### Context Window Management + +```typescript +transformContext: async (messages, signal) => { + if (estimateTokens(messages) > MAX_TOKENS) { + return pruneOldestMessages(messages, Math.floor(MAX_TOKENS * 0.3)) + } + return messages +} +``` + +### Tool Permission Checks + +```typescript +beforeToolCall: async ({ toolCall, args }, signal) => { + if (toolCall.name === "bash" && signal?.aborted) { + return { block: true, reason: "Operation aborted" } + } + if (toolCall.name === "bash" && !await canExecute(args)) { + return { block: true, reason: "Permission denied" } + } + return undefined +} +``` + +### Streaming Updates + +```typescript +execute: async (id, params, signal, onUpdate) => { + for await (const item of longProcess()) { + if (signal?.aborted) throw new Error("Aborted") + onUpdate({ + content: [{ type: "text", text: `Progress: ${item}%` }], + details: { progress: item } + }) + } + return finalResult +} +``` + +### Early Termination + +```typescript +shouldStopAfterTurn: async ({ message, toolResults }) => { + // Check if model indicates completion + if (message.content.some(c => c.text?.includes("TASK_COMPLETE"))) { + return true + } + // Stop if all tool calls set terminate + return toolResults.every(r => r.terminate) +} +``` + +--- + +## 12. Learning Path + +1. **Start with types** - Understand AgentMessage, AgentEvent, AgentTool +2. **Study agent-loop** - See how messages flow through the loop +3. **Read hooks** - Understand customization points +4. **Explore session** - See persistence and tree structure +5. **Study tools** - Understand tool execution +6. **Read harness** - See high-level API +7. **Design in Julia** - Implement step by step + +--- + +## 13. Implementation Checklist + +### Phase 1: Data Types (Julia) +- [ ] AgentMessage equivalent +- [ ] AgentEvent types +- [ ] AgentTool interface +- [ ] AgentContext + +### Phase 2: Core Agent +- [ ] Agent class with state +- [ ] Event streaming +- [ ] Message queue (steering, follow-up) + +### Phase 3: Agent Loop +- [ ] runAgentLoop() +- [ ] streamAssistantResponse() +- [ ] executeToolCalls() +- [ ] Tool preparation and execution +- [ ] Event emission + +### Phase 4: Hooks +- [ ] Hook registration +- [ ] Hook execution +- [ ] Return value handling + +### Phase 5: Session +- [ ] SessionTreeEntry types +- [ ] Tree structure +- [ ] Context building +- [ ] Persistence + +### Phase 6: AgentHarness +- [ ] High-level API +- [ ] Queue management +- [ ] Branching +- [ ] Compaction + +--- + +## 14. Quick Reference Cards + +### Agent Core + +| Function | Purpose | +|----------|---------| +| `runAgentLoop()` | Start new conversation | +| `runAgentLoopContinue()` | Continue existing | +| `streamAssistantResponse()` | Stream LLM | +| `executeToolCalls()` | Execute tools | +| `prepareToolCall()` | Prepare tool execution | +| `executePreparedToolCall()` | Execute tool | +| `finalizeExecutedToolCall()` | Finalize tool | + +### AgentHarness + +| Method | Purpose | +|--------|---------| +| `prompt()` | Run conversation | +| `skill()` | Execute skill | +| `promptFromTemplate()` | Run template | +| `steer()` | Interrupt agent | +| `followUp()` | Queue message | +| `nextTurn()` | Queue next turn | +| `compact()` | Compress context | +| `navigateTree()` | Branch conversation | +| `setModel()` | Change model | +| `setThinkingLevel()` | Change reasoning | +| `setTools()` | Set tools | +| `setActiveTools()` | Set active tools | + +### Hooks + +| Hook | Layer | Purpose | +|------|-------|---------| +| `convertToLlm` | Agent | Convert messages | +| `transformContext` | Agent | Manipulate context | +| `beforeToolCall` | Agent | Block tools | +| `afterToolCall` | Agent | Override results | +| `shouldStopAfterTurn` | Agent | Request stop | +| `prepareNextTurn` | Agent | Update config | +| `getSteeringMessages` | Agent | Interrupt | +| `getFollowUpMessages` | Agent | Queue messages | + +### Session + +| Method | Purpose | +|--------|---------| +| `buildContext()` | Get LLM context | +| `appendMessage()` | Add message | +| `fork()` | Create branch | +| `compact()` | Compress history | + +--- + +**You now have a complete reference for reimplementing the Pi Agent in Julia!** + +Start with the data types, implement the core loop, add hooks, then build up to the harness and session layers. diff --git a/packages/agent/learning/README.md b/packages/agent/learning/README.md new file mode 100644 index 00000000..1c402c35 --- /dev/null +++ b/packages/agent/learning/README.md @@ -0,0 +1,270 @@ +# Pi Agent Learning Resources + +This folder contains comprehensive learning materials for understanding the Pi Agent architecture. + +--- + +## 📚 Documentation Files + +| File | Description | Time | +|------|-------------|------| +| **00-README.md** | This file | 5 min | +| **01-ARCHITECTURE-OVERVIEW.md** | Top-down architecture overview with diagrams | 30 min | +| **02-AGENT-LOOP-DETAILED.md** | Core agent loop implementation details | 45 min | +| **03-HOOK-SYSTEM.md** | Complete hook system reference | 45 min | +| **04-SESSION-ARCHITECTURE.md** | Session persistence and tree structure | 45 min | +| **05-TOOL-EXECUTION.md** | Tool execution mechanics | 45 min | +| **06-AGENTHARNESS-REFERENCE.md** | High-level API reference | 45 min | +| **07-DATA-FLOW-STATE.md** | Data flow and state management | 45 min | +| **08-LEARNING-PATH.md** | Step-by-step learning guide | 2 hrs | +| **09-DIAGRAMS.md** | Visual diagrams and flowcharts | 30 min | +| **10-QUICK-START.md** | Quick start guide for Julia reimplementation | 30 min | +| **11-COMPLETE-SUMMARY.md** | Complete reference summary | 20 min | + +--- + +## 🎯 Learning Paths + +### Path 1: Fast Track (4-5 hours) + +**Goal**: Understand enough to start implementing + +1. **01-ARCHITECTURE-OVERVIEW.md** - Big picture +2. **10-QUICK-START.md** - Quick start guide +3. **02-AGENT-LOOP-DETAILED.md** - Core loop (skim) +4. **11-COMPLETE-SUMMARY.md** - Reference + +**Then**: Start implementing in Julia + +### Path 2: Thorough (10-12 hours) + +**Goal**: Deep understanding before implementation + +1. **01-ARCHITECTURE-OVERVIEW.md** - 30 min +2. **02-AGENT-LOOP-DETAILED.md** - 45 min +3. **03-HOOK-SYSTEM.md** - 45 min +4. **04-SESSION-ARCHITECTURE.md** - 45 min +5. **05-TOOL-EXECUTION.md** - 45 min +6. **06-AGENTHARNESS-REFERENCE.md** - 45 min +7. **07-DATA-FLOW-STATE.md** - 45 min +8. **09-DIAGRAMS.md** - Reference throughout + +**Then**: Follow **08-LEARNING-PATH.md** for implementation + +### Path 3: Comprehensive (15-20 hours) + +**Goal**: Master the entire system + +1. **01-ARCHITECTURE-OVERVIEW.md** - 30 min +2. **02-AGENT-LOOP-DETAILED.md** - 2 hrs +3. **03-HOOK-SYSTEM.md** - 2 hrs +4. **04-SESSION-ARCHITECTURE.md** - 2 hrs +5. **05-TOOL-EXECUTION.md** - 2 hrs +6. **06-AGENTHARNESS-REFERENCE.md** - 2 hrs +7. **07-DATA-FLOW-STATE.md** - 2 hrs +8. **08-LEARNING-PATH.md** - Follow implementation guide +9. **Read source files** - `src/agent.ts`, `src/agent-loop.ts`, etc. + +--- + +## 🏗️ Architecture Overview + +``` +┌───────────────────────────────────────────────────────────────────────────────┐ +│ APPLICATION LAYER │ +│ ┌──────────────┐ ┌──────────────────┐ ┌──────────────────────────┐ │ +│ │ Agent │ │ AgentHarness │ │ Your Custom App │ │ +│ │ (Core) │ │ (High-Level) │ │ │ │ +│ └───────┬──────┘ └────────┬─────────┘ └───────────┬──────────────┘ │ +└──────────┼─────────────────────┼──────────────────────────┼──────────────────┘ + │ │ │ + ▼ ▼ ▼ +┌───────────────────┐ ┌─────────────────────┐ ┌───────────────────────────┐ +│ Agent Core │ │ Session System │ │ Tool System │ +│ • agent-loop.ts │ │ • session/ │ │ • tools/ │ +│ • agent.ts │ │ • compaction/ │ │ • bash.ts │ +│ • types.ts │ │ • session.ts │ │ • read.ts │ +└─────────┬─────────┘ └──────────┬──────────┘ │ • write.ts │ + │ │ │ • edit.ts │ + ▼ ▼ └──────────┬──────────────┘ +┌───────────────────────────────────────────────────────────┼──────────────────┐ +│ AGENT CORE LAYER │ │ +│ • Async iteration │ │ +│ • Event streaming │ │ +│ • Hook execution │ │ +│ • Tool execution │ │ +└────────────────────────────────────────────────────────────┴──────────────────┘ +``` + +--- + +## 🔑 Key Concepts + +### 1. Agent Core + +- **Low-level** async iteration +- **Message-based** communication +- **Event-driven** state changes +- **Hook system** for customization + +### 2. AgentHarness + +- **High-level** API +- **Session persistence** (tree structure) +- **Branching** support +- **Context compaction** +- **Tool context binding** + +### 3. Hooks + +- **Before/after** tool execution +- **Message transformation** +- **Context manipulation** +- **Queue draining** + +### 4. Session Tree + +- **Persistent** conversation history +- **Branchable** conversation paths +- **Context building** from tree +- **Compaction** for efficiency + +--- + +## 🎓 How to Use This Guide + +### For Top-Down Learning + +1. Start with **01-ARCHITECTURE-OVERVIEW.md** +2. Study **09-DIAGRAMS.md** for visual understanding +3. Read **02-AGENT-LOOP-DETAILED.md** for core implementation +4. Explore **03-HOOK-SYSTEM.md** for customization +5. Understand **04-SESSION-ARCHITECTURE.md** for persistence + +### For Quick Start + +1. Read **10-QUICK-START.md** +2. Use **11-COMPLETE-SUMMARY.md** as reference +3. Implement while referencing other docs + +### For Deep Dive + +1. Follow the learning path in **08-LEARNING-PATH.md** +2. Read source files alongside documentation +3. Implement incrementally +4. Test each component + +--- + +## 📝 Implementation Checklist + +### Phase 1: Data Types (Julia) +- [ ] AgentMessage type +- [ ] AgentEvent types +- [ ] AgentTool interface +- [ ] AgentContext +- [ ] AgentState + +### Phase 2: Core Agent +- [ ] Agent class +- [ ] State management +- [ ] Event streaming +- [ ] Queue management + +### Phase 3: Agent Loop +- [ ] `runAgentLoop()` +- [ ] `streamAssistantResponse()` +- [ ] `executeToolCalls()` +- [ ] `prepareToolCall()` +- [ ] Event emission + +### Phase 4: Hooks +- [ ] Hook registration +- [ ] Hook execution +- [ ] Return value handling + +### Phase 5: Session +- [ ] Tree structure +- [ ] Entry types +- [ ] Context building +- [ ] Persistence + +### Phase 6: AgentHarness +- [ ] High-level API +- [ ] Queue methods +- [ ] Branching +- [ ] Compaction + +--- + +## 🛠️ Recommended Implementation Order + +1. **Data Types** - Define all types in Julia +2. **Core Agent** - Implement Agent class with basic state +3. **Event System** - Implement event streaming +4. **Agent Loop** - Implement the main loop +5. **Tool System** - Implement tool execution +6. **Hooks** - Add hook system +7. **Session** - Implement session persistence +8. **Harness** - Add high-level API + +--- + +## 📚 Related Files + +- `packages/agent/src/` - Source files + - `agent.ts` - Agent class + - `agent-loop.ts` - Core loop + - `types.ts` - Type definitions + - `proxy.ts` - Proxy utilities + - `stream-fn.ts` - Default stream function + - `harness/` - Harness implementation + +--- + +## 💡 Tips + +### For Julia Implementation + +1. **Start simple** - Implement basic types first +2. **Test incrementally** - Test each component +3. **Follow patterns** - Use Julia's type system +4. **Use idioms** - Follow Julia conventions +5. **Refer to docs** - Use this guide as reference + +### Common Patterns + +- **Event-driven** - Use Julia's event system +- **Immutable data** - Prefer immutable structures +- **Multiple dispatch** - Leverage Julia's dispatch +- **Async/await** - Use Julia's async for streaming + +--- + +## 🎯 Success Criteria + +After learning, you should be able to: + +✅ Explain the two-layer architecture +✅ Trace a message through the system +✅ Identify when each hook is called +✅ Explain how session persistence works +✅ Describe the tool execution flow +✅ Implement a custom tool +✅ Create a conversation branch +✅ Compress conversation history + +--- + +## 📞 Getting Help + +- Read the documentation files +- Check the diagrams for visual understanding +- Follow the learning path for structured learning +- Refer to the complete summary for reference + +--- + +**Happy Learning! 🚀** + +Start with **01-ARCHITECTURE-OVERVIEW.md** and **09-DIAGRAMS.md** for the big picture.