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