update
This commit is contained in:
@@ -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