This commit is contained in:
2026-07-29 10:59:18 +07:00
parent 1e5f7b8ff2
commit a7bb033eb8
41 changed files with 7312 additions and 6134 deletions
@@ -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
@@ -1,25 +0,0 @@
name = "AgentCore"
uuid = "6e2f7b3a-9a0b-4e8e-8f8f-8f8f8f8f8f8f"
authors = ["Mario Zechner <post@badlogicgames.com>"]
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"
@@ -1,22 +0,0 @@
name = "AgentCore"
uuid = "6e2f7b3a-9a0b-4e8e-8f8f-8f8f8f8f8f8f"
authors = ["Mario Zechner <post@badlogicgames.com>"]
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"]
@@ -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.
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large Load Diff
@@ -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:
<summary>
"""
const COMPACTION_SUMMARY_SUFFIX = """
</summary>"""
const BRANCH_SUMMARY_PREFIX = """The following is a summary of a branch that this conversation came back from:
<summary>
"""
const BRANCH_SUMMARY_SUFFIX = """</summary>"""
# ============================================================================
# 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
@@ -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<PromptTemplateFrontmatter>(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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 = "<skill name=\"$(skill.name)\" location=\"$(skill.filePath)\">\nReferences are relative to $(dirnameEnvPath(skill.filePath)).\n\n$(skill.content)\n</skill>"
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<SkillFrontmatter>(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
@@ -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
@@ -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.",
"",
"<available_skills>",
]
for skill in visible_skills
push!(lines, " <skill>")
push!(lines, " <name>$(escapeXml(skill.name))</name>")
push!(lines, " <description>$(escapeXml(skill.description))</description>")
push!(lines, " <location>$(escapeXml(skill.filePath))</location>")
push!(lines, " </skill>")
end
push!(lines, "</available_skills>")
return join(lines, "\n")
end
"""
escapeXml(value)
Escape special characters in a string for XML.
"""
function escapeXml(value::String)::String
result = replace(value, "&" => "&amp;")
result = replace(result, "<" => "&lt;")
result = replace(result, ">" => "&gt;")
result = replace(result, "\"" => "&quot;")
result = replace(result, "'" => "&apos;")
return result
end
end
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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<TParameters extends TSchema, TDetails> {
name: string;
label: string;
description: string;
parameters: TSchema;
execute(
toolCallId: string,
params: Static<TParameters>,
signal?: AbortSignal,
onUpdate?: AgentToolUpdateCallback<TDetails>
): Promise<AgentToolResult<TDetails>>;
}
```
---
## 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
@@ -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<AgentMessage[]>
```
**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<AgentMessage[]>
```
**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<void>
```
**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<AssistantMessage>
```
**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<ExecutedToolCallBatch> {
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<ExecutedToolCallBatch> {
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<ExecutedToolCallBatch> {
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<PreparedToolCall | ImmediateToolCallOutcome> {
// 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<ExecutedToolCallOutcome> {
const updateEvents: Promise<void>[] = [];
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<FinalizedToolCallOutcome> {
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<ExecutedToolCallBatch> {
// 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
+792
View File
@@ -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<any>;
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<any>;
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<AgentMessage[]>`
**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<AgentMessage[]>`
**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<string, unknown>;
}
```
**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<string, unknown>;
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<any>;
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<any>;
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.
@@ -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<TMetadata>`
```typescript
interface SessionStorage<TMetadata extends SessionMetadata = SessionMetadata> {
// Metadata
readonly id: string;
readonly metadata: TMetadata;
// Entry operations
getLeafId(): Promise<string | null>;
setLeafId(id: string): Promise<void>;
getEntry(id: string): Promise<SessionTreeEntry | undefined>;
getEntries(options?: SessionEntryCursorOptions): Promise<SessionTreeEntry[]>;
getBranch(): Promise<SessionTreeEntry[]>;
// Write operations
appendEntry(entry: SessionTreeEntry): Promise<string>;
// Branch operations
fork(targetId: string): Promise<SessionStorage>;
delete(): Promise<void>;
// Cleanup
cleanup(): Promise<void>;
}
```
### Built-in Implementations
#### MemoryStorage
```typescript
class MemoryStorage<TMetadata> implements SessionStorage<TMetadata> {
// In-memory storage using Map
// Good for: Testing, short-lived sessions
// Not good for: Persistence across runs
}
```
#### JSONLStorage
```typescript
class JSONLStorage<TMetadata> implements SessionStorage<TMetadata> {
// 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<TMetadata>`
High-level session API built on storage:
```typescript
class Session<TMetadata extends SessionMetadata = SessionMetadata> {
// Metadata
readonly id: string;
readonly storage: SessionStorage<TMetadata>;
// Read operations
getMetadata(): Promise<TMetadata>;
getLeafId(): Promise<string>;
getEntry(id: string): Promise<SessionTreeEntry | undefined>;
getBranch(): Promise<SessionTreeEntry[]>;
buildContext(options?: SessionContextBuildOptions): Promise<SessionContext>;
// Write operations
appendMessage(message: AgentMessage): Promise<string>;
appendModelChange(provider: string, modelId: string): Promise<string>;
appendThinkingLevelChange(thinkingLevel: ThinkingLevel): Promise<string>;
appendActiveToolsChange(activeToolNames: string[]): Promise<string>;
appendCompaction(...): Promise<string>;
appendBranchSummary(...): Promise<string>;
appendCustomEntry(customType: string, data: unknown): Promise<string>;
appendCustomMessageEntry(...): Promise<string>;
appendLabel(targetId: string, label: string): Promise<void>;
appendSessionName(name: string): Promise<string>;
// Branch operations
fork(targetId: string): Promise<Session>;
delete(): Promise<void>;
}
```
---
## Context Building Details
### Path Tracing
**Goal**: Get all entries from leaf to root.
```typescript
async function getPathEntries(session: Session): Promise<SessionTreeEntry[]> {
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<Session> {
// 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<CompactionResult> {
// 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<TMetadata>`
Repository pattern for session management:
```typescript
interface SessionRepo<TMetadata extends SessionMetadata = SessionMetadata> {
// CRUD
create(options: CreateSessionOptions<TMetadata>): Promise<Session<TMetadata>>;
open(id: string): Promise<Session<TMetadata>>;
list(): Promise<SessionInfo[]>;
delete(id: string): Promise<void>;
// Forking
fork(id: string, targetId: string): Promise<Session<TMetadata>>;
// Cleanup
cleanup(): Promise<void>;
}
```
### Built-in Implementations
#### MemoryRepo
```typescript
class MemoryRepo<TMetadata> implements SessionRepo<TMetadata> {
// In-memory storage using Map<string, Session<TMetadata>>
// Good for: Testing, ephemeral sessions
}
```
#### JSONLRepo
```typescript
class JSONLRepo<TMetadata> implements SessionRepo<TMetadata> {
// 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
@@ -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<TParameters extends TSchema, TDetails> extends Tool<TParameters> {
label: string; // Human-readable name for UI
prepareArguments?: (args: unknown) => Static<TParameters>; // Optional arg transformation
execute(
toolCallId: string,
params: Static<TParameters>,
signal?: AbortSignal,
onUpdate?: AgentToolUpdateCallback<TDetails>
): Promise<AgentToolResult<TDetails>>;
}
```
### Tool Result
```typescript
interface AgentToolResult<T> {
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<TSchema, WeatherDetails> = {
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<TSchema, BackupDetails> = {
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<TSchema, ApiDetails> = {
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<ToolContext> = {
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<TSchema> = {
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<TSchema> = {
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.
@@ -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<TMetadata extends SessionMetadata = SessionMetadata> {
readonly id: string;
readonly storage: SessionStorage<TMetadata>;
getMetadata(): Promise<TMetadata>;
getLeafId(): Promise<string>;
getEntry(id: string): Promise<SessionTreeEntry | undefined>;
getBranch(): Promise<SessionTreeEntry[]>;
buildContext(options?: SessionContextBuildOptions): Promise<SessionContext>;
appendMessage(message: AgentMessage): Promise<string>;
appendModelChange(provider: string, modelId: string): Promise<string>;
appendThinkingLevelChange(thinkingLevel: ThinkingLevel): Promise<string>;
appendActiveToolsChange(activeToolNames: string[]): Promise<string>;
appendCompaction(...): Promise<string>;
appendBranchSummary(...): Promise<string>;
fork(targetId: string): Promise<Session>;
}
```
### 2. Resources
Skills and prompt templates available to the agent:
```typescript
interface AgentHarnessResources<TSkill = Skill, TPromptTemplate = PromptTemplate> {
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> = () => TContext | Promise<TContext>;
```
---
## AgentHarness API
### Constructor
```typescript
constructor(options: AgentHarnessOptions<TContext, TSkill, TPromptTemplate, TTool>)
```
**Options**:
```typescript
interface AgentHarnessOptions<TContext, TSkill, TPromptTemplate, TTool> {
session: Session; // Session storage
models: Models; // LLM provider
resources?: AgentHarnessResources<TSkill, TPromptTemplate>;
streamOptions?: AgentHarnessStreamOptions;
retry?: RetryPolicy;
// System prompt
systemPrompt?:
| string // Static string
| AgentHarnessSystemPrompt<TContext, TSkill, TPromptTemplate, TTool>; // Dynamic function
// Tool context
toolContext?: AgentHarnessToolContextSource<TContext>;
// Tools
tools?: TTool[];
// Active tools
activeToolNames?: string[];
// Model and thinking
model: Model<any>;
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<AssistantMessage>
```
**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<AssistantMessage>
```
**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<AssistantMessage>
```
**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<void>
```
**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<void>
```
**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<void>
```
**Difference from `steer()`**:
- `steer()`: Interrupts immediately
- `nextTurn()`: Waits for current turn to finish
### `compact()`
Compress conversation history:
```typescript
async compact(customInstructions?: string): Promise<CompactResult>
```
**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<NavigateTreeResult>
```
**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<any>;
async setModel(model: Model<any>): Promise<void>;
```
**Example**:
```typescript
console.log(harness.getModel().id); // "gpt-4"
await harness.setModel(gpt4oModel);
```
### Thinking Level
```typescript
getThinkingLevel(): ThinkingLevel;
async setThinkingLevel(level: ThinkingLevel): Promise<void>;
```
**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<void>;
async setActiveTools(toolNames: string[]): Promise<void>;
```
**Example**:
```typescript
// Add new tool
await harness.setTools([...harness.getTools(), newTool]);
// Change active tools
await harness.setActiveTools(["read", "write"]);
```
### Resources
```typescript
getResources(): AgentHarnessResources<TSkill, TPromptTemplate>;
async setResources(resources: AgentHarnessResources<TSkill, TPromptTemplate>): Promise<void>;
```
**Example**:
```typescript
await harness.setResources({
skills: [...harness.getResources().skills, newSkill]
});
```
---
## Queue Management
### Steering Queue
```typescript
getSteeringMode(): QueueMode;
async setSteeringMode(mode: QueueMode): Promise<void>;
```
**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<void>;
```
### 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<TSkill, TPromptTemplate>, signal?: AbortSignal) => Promise<void> | void
): () => void;
```
**Event types**:
```typescript
type AgentHarnessEvent<TSkill, TPromptTemplate> =
// 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<TType extends keyof AgentHarnessEventResultMap>(
type: TType,
handler: (event: Extract<AgentHarnessOwnEvent, { type: TType }>) => Promise<AgentHarnessEventResultMap[TType]> | 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<void>;
```
**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<AbortResult>
```
**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<void>;
```
**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<AssistantMessage> {
// 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
@@ -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<any>;
thinkingLevel: ThinkingLevel;
tools: AgentTool<any>[];
messages: AgentMessage[];
isStreaming: boolean;
streamingMessage?: AgentMessage;
pendingToolCalls: Set<string>;
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<void> {
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<any>[];
}
```
**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<SessionContext> {
// 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<void> {
if (phase === "idle") {
// Direct write
await session.appendMessage(message);
} else {
// Buffer for later
pendingSessionWrites.push({ type: "message", message });
}
}
async function flushPendingSessionWrites(): Promise<void> {
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<any>; // Executed
isError: boolean;
context: AgentContext;
}
```
### Tool Result State
```typescript
interface AgentToolResult<T> {
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"
+522
View File
@@ -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.
+695
View File
@@ -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<string> ← 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: "<tool_call name=bash>")
message_update (assistant: "<tool_call name=bash>")
message_update (assistant: "<tool_call name=bash>")
message_end (assistant: "<tool_call name=bash>")
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.
+403
View File
@@ -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<Result>;
}
// 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.
@@ -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<any>[]
}
```
### 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<TSkill, TPromptTemplate> =
| { 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<TParameters, TDetails> extends Tool<TParameters> {
label: string
execute(
toolCallId: string,
params: Static<TParameters>,
signal?: AbortSignal,
onUpdate?: AgentToolUpdateCallback<TDetails>
): Promise<AgentToolResult<TDetails>>
}
interface AgentToolResult<T> {
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<TParameters, TDetails> extends Tool<TParameters> {
label: string
prepareArguments?: (args: unknown) => Static<TParameters>
execute(
toolCallId: string,
params: Static<TParameters>,
signal?: AbortSignal,
onUpdate?: AgentToolUpdateCallback<TDetails>
): Promise<AgentToolResult<TDetails>>
}
interface AgentToolResult<T> {
content: (TextContent | ImageContent)[]
details: T
usage?: Usage
addedToolNames?: string[]
terminate?: boolean
}
```
---
## 8. State Management
### Agent State
```typescript
interface AgentState {
systemPrompt: string
model: Model<any>
thinkingLevel: ThinkingLevel
tools: AgentTool<any>[]
messages: AgentMessage[]
isStreaming: boolean
streamingMessage?: AgentMessage
pendingToolCalls: Set<string>
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.
+270
View File
@@ -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.