update
This commit is contained in:
@@ -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, "&" => "&")
|
||||
result = replace(result, "<" => "<")
|
||||
result = replace(result, ">" => ">")
|
||||
result = replace(result, "\"" => """)
|
||||
result = replace(result, "'" => "'")
|
||||
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
|
||||
@@ -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"
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user