This commit is contained in:
2026-07-29 10:59:18 +07:00
parent 1e5f7b8ff2
commit a7bb033eb8
41 changed files with 7312 additions and 6134 deletions
@@ -0,0 +1,433 @@
# Pi Agent Architecture - Top-Down Overview
## Executive Summary
The Pi Agent is a **stateful, event-driven agent framework** built in TypeScript. It provides:
1. **Core Agent** - Low-level agent loop with message/tool streaming
2. **Agent Harness** - High-level session management with persistence, branching, and compaction
Both layers follow the **same core pattern**: stream LLM response → execute tools → emit events → repeat.
---
## Architecture Layers
```
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ APPLICATION LAYER │
│ • Creates Agent/AgentHarness instances │
│ • Subscribes to events for UI updates │
│ • Provides tools and model configuration │
└─────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────┼─────────────────────────────┐
│ │ │
▼ ▼ ▼
┌────────────────────────┐ ┌────────────────────────────────┐ ┌─────────────────┐
│ Agent (Core) │ │ Agent Harness (High-Level) │ │ Agent-Loop │
│ │ │ │ │ │
│ • State management │ │ • Session persistence │ │ • Turn │
│ • Event streaming │ │ • Branching/compaction │ │ • Tool exec │
│ • Steering/follow-up │ │ • Skills/templates │ │ • Message │
│ queues │ │ • Tool context binding │ │ streaming │
│ • Hook system │ │ • State snapshots │ │ │
└────────────────────────┘ └────────────────────────────────┘ └─────────────────┘
│ │
▼ ▼
┌──────────────────────────┐ ┌─────────────────┐
│ LLM Provider API │ │ Session Repo │
│ (via @earendil-works) │ │ (JSONL/ │
└──────────────────────────┘ │ Memory) │
└─────────────────┘
```
---
## Core Concepts
### 1. AgentMessage
```typescript
type AgentMessage = Message | CustomAgentMessages[keyof CustomAgentMessages]
```
The unified message type that combines:
- **LLM messages**: `user`, `assistant`, `toolResult` (from pi-ai)
- **Custom messages**: Application-specific types (via declaration merging)
### 2. AgentEvent
```typescript
type AgentEvent =
| { type: "agent_start" }
| { type: "agent_end"; messages: AgentMessage[] }
| { type: "turn_start" }
| { type: "turn_end"; message: AgentMessage; toolResults: ToolResultMessage[] }
| { type: "message_start"; message: AgentMessage }
| { type: "message_update"; message: AgentMessage; assistantMessageEvent: AssistantMessageEvent }
| { type: "message_end"; message: AgentMessage }
| { type: "tool_execution_start"; toolCallId: string; toolName: string; args: any }
| { type: "tool_execution_update"; toolCallId: string; toolName: string; args: any; partialResult: any }
| { type: "tool_execution_end"; toolCallId: string; toolName: string; result: any; isError: boolean }
```
**Event Flow per Turn:**
```
turn_start
message_start (user prompt)
message_end
message_start (assistant streaming)
message_update (multiple - as chunks arrive)
message_end
tool_execution_start (if tool calls present)
tool_execution_update (if tool streams partial results)
tool_execution_end
turn_end
```
### 3. AgentTool
```typescript
interface AgentTool<TParameters extends TSchema, TDetails> {
name: string;
label: string;
description: string;
parameters: TSchema;
execute(
toolCallId: string,
params: Static<TParameters>,
signal?: AbortSignal,
onUpdate?: AgentToolUpdateCallback<TDetails>
): Promise<AgentToolResult<TDetails>>;
}
```
---
## Process Flow Diagrams
### Prompt Flow (High-Level)
```
User Input
Agent.prompt("Hello")
├─► normalizePromptInput() → AgentMessage[]
├─► runWithLifecycle()
│ ├─► Set isStreaming=true
│ └─► Create abort controller
runAgentLoop()
├─► Emit: agent_start
├─► Emit: turn_start
├─► Emit: message_start/end (prompts)
runLoop() - Main Loop
├─► Check steering queue (drain if any)
├─► Check follow-up queue (skip if not first turn)
streamAssistantResponse()
├─► transformContext() [optional]
├─► convertToLlm() → Message[]
├─► Build Context {systemPrompt, messages, tools}
├─► Resolve API key
├─► Call streamFn(model, context, options)
Assistant Message Stream
├─► message_start (assistant)
├─► message_update (text chunks)
├─► message_update (toolCall blocks)
├─► message_end
executeToolCalls()
├─► Check if sequential/parallel execution
├─► For each tool call:
│ ├─► prepareToolCall()
│ │ ├─► Find tool by name
│ │ ├─► Validate arguments
│ │ └─► beforeToolCall() hook
│ │
│ ├─► executePreparedToolCall()
│ │ └─► tool.execute() with onUpdate callback
│ │
│ └─► finalizeExecutedToolCall()
│ └─► afterToolCall() hook
├─► Emit: tool_execution_start/update/end
└─► Emit: message_start/end (toolResult)
turn_end
├─► Check prepareNextTurn hook
├─► Check shouldStopAfterTurn hook
├─► Drain steering queue
└─► Drain follow-up queue
├─► If steering/follow-up exists → repeat loop
└─► If no more messages → agent_end
```
### Tool Execution Flow (Detailed)
```
Tool Call from LLM
prepareToolCall()
├─► Find tool in currentContext.tools
│ └─► If not found → immediate error
├─► prepareToolCallArguments() [optional]
├─► validateToolArguments()
│ └─► If invalid → immediate error
└─► beforeToolCall() hook
├─► Return {block: true} → error
└─► Continue
executePreparedToolCall()
├─► Call tool.execute() with onUpdate callback
│ └─► tool calls onUpdate(partialResult) during execution
├─► onUpdate() → emit tool_execution_update
└─► Return {result, isError}
finalizeExecutedToolCall()
└─► afterToolCall() hook
├─► Override content/details/usage/terminate
└─► Return {toolCall, result, isError}
emitToolExecutionEnd()
└─► Emit: tool_execution_end
createToolResultMessage()
└─► Create ToolResultMessage with:
├─► toolCallId
├─► toolName
├─► content
├─► details
├─► usage
└─► isError
emitToolResultMessage()
├─► Emit: message_start
└─► Emit: message_end
```
### Session Persistence Flow
```
AgentHarness.handleAgentEvent()
├─► message_end → session.appendMessage()
│ └─► Storage: write entry to JSONL file
├─► turn_end → flushPendingSessionWrites()
│ ├─► Write all pending entries
│ ├─► Emit: save_point
│ └─► session.getStorage().setLeafId()
└─► agent_end → flushPendingSessionWrites()
├─► Write leaf entry pointing to last message
└─► Emit: settled
Session Tree Structure:
root
├─► message (user prompt #1)
├─► message (assistant #1)
├─► tool_result (result #1)
├─► turn_end
├─► message (user prompt #2)
├─► message (assistant #2)
├─► compaction (summary of history)
├─► message (assistant continues)
└─► leaf → points to current head
```
---
## Hook System
### Agent-Level Hooks (agent-loop.ts)
```typescript
interface AgentLoopConfig {
// Message transformation
convertToLlm: (messages: AgentMessage[]) => Message[]
transformContext?: (messages: AgentMessage[]) => AgentMessage[]
// Lifecycle hooks
beforeToolCall?: (context: BeforeToolCallContext) => BeforeToolCallResult
afterToolCall?: (context: AfterToolCallContext) => AfterToolCallResult
shouldStopAfterTurn?: (context: ShouldStopAfterTurnContext) => boolean
prepareNextTurn?: (context: PrepareNextTurnContext) => AgentLoopTurnUpdate
// Queue draining
getSteeringMessages?: () => AgentMessage[]
getFollowUpMessages?: () => AgentMessage[]
}
```
### Harness-Level Hooks (agent-harness.ts)
```typescript
// Hook types in AgentHarnessEventResultMap:
type HookName =
| "before_agent_start"
| "context"
| "tool_call"
| "tool_result"
| "session_before_compact"
| "session_before_tree"
| "before_provider_request"
| "before_provider_payload"
```
**Hook Execution Order per Turn:**
```
1. before_agent_start (harness)
2. context (harness) → transformContext
3. streamAssistantResponse
├─► Before provider request (harness)
├─► convertToLlm (agent)
└─► LLM call
4. For each tool call:
├─► tool_call (harness) → beforeToolCall
├─► Execute tool
└─► tool_result (harness) → afterToolCall
5. turn_end
6. shouldStopAfterTurn (agent)
7. prepareNextTurn (agent)
8. Drain steering/follow-up queues
```
---
## Data Flow Summary
```
┌────────────────────────────────────────────────────────────────────────────────┐
│ AGENT LIFECYCLE - DATA FLOW │
├────────────────────────────────────────────────────────────────────────────────┤
│ 1. INPUT │
│ • prompt("Hello") → normalizePromptInput() │
│ → AgentMessage[] │
│ 2. INITIATE │
│ • createMutableAgentState() │
│ • runWithLifecycle() │
│ 3. LOOP CONTROL │
│ • runLoop() │
│ ├─► Steering queue? → drain and inject │
│ └─► Follow-up queue? (after first turn) │
│ 4. LLM STREAMING │
│ • transformContext() [optional] │
│ • convertToLlm() │
│ • streamFn() │
│ → AssistantMessage stream (text + toolCalls) │
│ 5. TOOL EXECUTION │
│ • executeToolCalls() │
│ ├─► prepareToolCall() │
│ │ ├─► beforeToolCall() hook │
│ │ └─► Validate args │
│ ├─► executePreparedToolCall() │
│ │ └─► tool.execute() │
│ └─► finalizeExecutedToolCall() │
│ └─► afterToolCall() hook │
│ 6. UPDATE STATE │
│ • Push assistant message to state.messages │
│ • Push toolResult messages to state.messages │
│ 7. TERMINATION CHECK │
│ • shouldStopAfterTurn? → exit │
│ • prepareNextTurn? → update context/model │
│ • Drain steering/follow-up → continue │
│ 8. FINISH │
│ • emit agent_end │
│ • finishRun() → reset isStreaming │
└────────────────────────────────────────────────────────────────────────────────┘
```
---
## Key Design Patterns
### 1. Event-Driven Architecture
- All external communication via `AgentEvent` stream
- Hooks can be async and are awaited in order
- Abort signal propagated through all operations
### 2. State Isolation
- `AgentState` is read-only externally
- `AgentHarness` snapshots state per turn
- Context transforms return new arrays (immutability)
### 3. Layered Abstraction
```
Low-level (agent-loop.ts)
• Pure async iteration
• No session management
• No tool context binding
High-level (agent-harness.ts)
• Session persistence
• Branching/compaction
• Hook system for customization
```
### 4. Extensibility Points
- **Custom messages**: Extend `CustomAgentMessages` interface
- **Custom hooks**: Add handlers via `subscribe()`/`on()`
- **Tool context**: Pass `toolContext` to harness constructor
- **Storage**: Implement `SessionStorage` interface
---
## Learning Path
1. **Start with types.ts** - Understand `AgentMessage`, `AgentEvent`, `AgentTool`
2. **Read agent-loop.ts** - See how messages flow through the loop
3. **Study agent.ts** - See how Agent wraps the loop with state management
4. **Read agent-harness.ts** - See how session management hooks into the loop
5. **Explore session/* files** - Understand persistence and branching
6. **Study tools/* files** - See concrete tool implementations
---
## Next Steps
See individual markdown files in this folder for:
- `AGENT-LOOP-DETAILED.md` - Deep dive into the agent loop
- `HOOK-SYSTEM.md` - Complete hook documentation
- `SESSION-ARCHITECTURE.md` - Session persistence details
- `TOOL-EXECUTION.md` - Tool execution mechanics
@@ -0,0 +1,697 @@
# Agent Loop Deep Dive
## Overview
The `agent-loop.ts` file contains the **core async iteration logic** that drives the agent. It's intentionally low-level and stateless - it takes a snapshot of context and drives it to completion.
---
## Core Functions
### 1. `runAgentLoop()`
**Purpose**: Start a new agent run with initial prompt messages.
```typescript
async function runAgentLoop(
prompts: AgentMessage[],
context: AgentContext,
config: AgentLoopConfig,
emit: AgentEventSink,
signal: AbortSignal | undefined,
streamFn: StreamFn,
): Promise<AgentMessage[]>
```
**Flow**:
```
1. Create newMessages = [...prompts]
2. Append prompts to context.messages
3. Emit: agent_start
4. Emit: turn_start
5. For each prompt:
- Emit: message_start
- Emit: message_end
6. Call: runLoop() - main iteration logic
7. Return: newMessages
```
### 2. `runAgentLoopContinue()`
**Purpose**: Continue from existing context (no new prompts).
```typescript
async function runAgentLoopContinue(
context: AgentContext,
config: AgentLoopConfig,
emit: AgentEventSink,
signal: AbortSignal | undefined,
streamFn: StreamFn,
): Promise<AgentMessage[]>
```
**Constraints**:
- Last message must convert to `user` or `toolResult`
- Throws if context is empty or last message is `assistant`
**Flow**:
```
1. Validate context (non-empty, last message is not assistant)
2. Create newMessages = [] (empty - we continue)
3. Emit: agent_start
4. Emit: turn_start
5. Call: runLoop()
6. Return: newMessages
```
### 3. `runLoop()` - The Heart of the Agent
**Purpose**: Main iteration loop that drives conversation.
```typescript
async function runLoop(
initialContext: AgentContext,
newMessages: AgentMessage[],
initialConfig: AgentLoopConfig,
signal: AbortSignal | undefined,
emit: AgentEventSink,
streamFunction: StreamFn,
): Promise<void>
```
**Structure**:
```typescript
async function runLoop(...) {
let currentContext = initialContext;
let config = initialConfig;
let firstTurn = true;
let pendingMessages: AgentMessage[] = [];
// OUTER LOOP: Handles follow-up messages
while (true) {
let hasMoreToolCalls = true;
// INNER LOOP: Handles tool calls and steering
while (hasMoreToolCalls || pendingMessages.length > 0) {
if (!firstTurn) {
await emit({ type: "turn_start" });
} else {
firstTurn = false;
}
// 1. Process pending messages (steering/follow-up)
if (pendingMessages.length > 0) {
for (const message of pendingMessages) {
await emit({ type: "message_start", message });
await emit({ type: "message_end", message });
currentContext.messages.push(message);
newMessages.push(message);
}
pendingMessages = [];
}
// 2. Stream assistant response
const message = await streamAssistantResponse(...);
newMessages.push(message);
// 3. Check for errors
if (message.stopReason === "error" || message.stopReason === "aborted") {
await emit({ type: "turn_end", message, toolResults: [] });
await emit({ type: "agent_end", messages: newMessages });
return;
}
// 4. Execute tool calls
const toolCalls = message.content.filter(c => c.type === "toolCall");
const toolResults: ToolResultMessage[] = [];
hasMoreToolCalls = false;
if (toolCalls.length > 0) {
const executedBatch = await executeToolCalls(...);
toolResults.push(...executedBatch.messages);
hasMoreToolCalls = !executedBatch.terminate;
for (const result of toolResults) {
currentContext.messages.push(result);
newMessages.push(result);
}
}
// 5. Emit turn_end
await emit({ type: "turn_end", message, toolResults });
// 6. Prepare next turn
const nextTurnContext = { message, toolResults, context, newMessages };
const nextTurnSnapshot = await config.prepareNextTurn?.(nextTurnContext);
if (nextTurnSnapshot) {
currentContext = nextTurnSnapshot.context ?? currentContext;
config = { ...config, model: nextTurnSnapshot.model };
}
// 7. Check termination
if (await config.shouldStopAfterTurn?.(...)) {
await emit({ type: "agent_end", messages: newMessages });
return;
}
// 8. Drain steering queue
pendingMessages = (await config.getSteeringMessages?.()) || [];
}
// Outer loop: Check for follow-up messages
const followUpMessages = (await config.getFollowUpMessages?.()) || [];
if (followUpMessages.length > 0) {
pendingMessages = followUpMessages;
continue; // Back to inner loop
}
// No more messages - exit
break;
}
await emit({ type: "agent_end", messages: newMessages });
}
```
---
## Message Streaming
### `streamAssistantResponse()`
**Purpose**: Stream assistant response from LLM provider.
```typescript
async function streamAssistantResponse(
context: AgentContext,
config: AgentLoopConfig,
signal: AbortSignal | undefined,
emit: AgentEventSink,
streamFunction: StreamFn,
): Promise<AssistantMessage>
```
**Flow**:
```
1. Apply transformContext() if configured
├─► messages = await config.transformContext(messages)
└─► Returns new AgentMessage[]
2. Convert to LLM format
├─► llmMessages = await config.convertToLlm(messages)
└─► Returns Message[] (filters custom messages)
3. Build LLM Context
Context = {
systemPrompt: context.systemPrompt,
messages: llmMessages,
tools: context.tools
}
4. Resolve API key
├─► Get key from getApiKey() hook
└─► Fallback to config.apiKey
5. Call streamFn()
├─► StreamFn(model, context, options)
└─► Returns AssistantMessageEventStream
6. Process stream events
for await (const event of response) {
switch (event.type) {
case "start":
// Initialize partial message
partialMessage = event.partial
context.messages.push(partialMessage)
emit({ type: "message_start", message })
case "text_start" | "text_delta" | "text_end":
case "thinking_start" | "thinking_delta" | "thinking_end":
case "toolcall_start" | "toolcall_delta" | "toolcall_end":
// Update partial message
partialMessage = event.partial
emit({ type: "message_update", ... })
case "done" | "error":
const finalMessage = await response.result()
emit({ type: "message_end", message })
return finalMessage
}
}
```
---
## Tool Execution
### Sequential vs Parallel
**Sequential Mode**:
- Each tool call prepared, executed, finalized before next
- Emit `tool_execution_end` immediately after each
- Tool results in source order
**Parallel Mode**:
- All tool calls prepared sequentially
- Allowed tools execute concurrently
- Emit `tool_execution_end` in completion order
- Tool results in source order
### `executeToolCalls()`
```typescript
async function executeToolCalls(...): Promise<ExecutedToolCallBatch> {
const toolCalls = assistantMessage.content.filter(c => c.type === "toolCall");
// Check if any tool requires sequential execution
const hasSequentialToolCall = toolCalls.some(tc => {
const tool = currentContext.tools?.find(t => t.name === tc.name);
return tool?.executionMode === "sequential";
});
if (config.toolExecution === "sequential" || hasSequentialToolCall) {
return executeToolCallsSequential(...);
}
return executeToolCallsParallel(...);
}
```
### `executeToolCallsSequential()`
```typescript
async function executeToolCallsSequential(...): Promise<ExecutedToolCallBatch> {
const finalizedCalls: FinalizedToolCallOutcome[] = [];
const messages: ToolResultMessage[] = [];
for (const toolCall of toolCalls) {
// 1. Prepare
const preparation = await prepareToolCall(...);
let finalized: FinalizedToolCallOutcome;
if (preparation.kind === "immediate") {
// Validation/permission hook blocked execution
finalized = { toolCall, result: preparation.result, isError: preparation.isError };
} else {
// Execute
const executed = await executePreparedToolCall(preparation, signal, emit);
finalized = await finalizeExecutedToolCall(...);
}
// 2. Emit
await emitToolExecutionEnd(finalized, emit);
const toolResultMessage = createToolResultMessage(finalized);
await emitToolResultMessage(toolResultMessage, emit);
finalizedCalls.push(finalized);
messages.push(toolResultMessage);
if (signal?.aborted) break;
}
return {
messages,
terminate: shouldTerminateToolBatch(finalizedCalls)
};
}
```
### `executeToolCallsParallel()`
```typescript
async function executeToolCallsParallel(...): Promise<ExecutedToolCallBatch> {
const finalizedCalls: FinalizedToolCallEntry[] = [];
// Phase 1: Prepare all tool calls
for (const toolCall of toolCalls) {
const preparation = await prepareToolCall(...);
if (preparation.kind === "immediate") {
// Blocked or error - execute immediately
const finalized = {
toolCall,
result: preparation.result,
isError: preparation.isError
};
await emitToolExecutionEnd(finalized, emit);
finalizedCalls.push(finalized);
} else {
// Schedule for concurrent execution
finalizedCalls.push(async () => {
const executed = await executePreparedToolCall(preparation, signal, emit);
const finalized = await finalizeExecutedToolCall(...);
await emitToolExecutionEnd(finalized, emit);
return finalized;
});
}
if (signal?.aborted) break;
}
// Phase 2: Execute concurrent tools and collect results
const orderedFinalizedCalls = await Promise.all(
finalizedCalls.map(entry => typeof entry === "function" ? entry() : Promise.resolve(entry))
);
// Phase 3: Emit tool result messages in source order
const messages: ToolResultMessage[] = [];
for (const finalized of orderedFinalizedCalls) {
const toolResultMessage = createToolResultMessage(finalized);
await emitToolResultMessage(toolResultMessage, emit);
messages.push(toolResultMessage);
}
return {
messages,
terminate: shouldTerminateToolBatch(orderedFinalizedCalls)
};
}
```
---
## Tool Preparation Flow
### `prepareToolCall()`
```typescript
async function prepareToolCall(...): Promise<PreparedToolCall | ImmediateToolCallOutcome> {
// 1. Find tool
const tool = currentContext.tools?.find(t => t.name === toolCall.name);
if (!tool) {
return {
kind: "immediate",
result: createErrorToolResult(`Tool ${toolCall.name} not found`),
isError: true
};
}
try {
// 2. Prepare arguments (optional shim)
const preparedToolCall = prepareToolCallArguments(tool, toolCall);
// 3. Validate arguments
const validatedArgs = validateToolArguments(tool, preparedToolCall);
// 4. beforeToolCall hook
if (config.beforeToolCall) {
const beforeResult = await config.beforeToolCall(
{ assistantMessage, toolCall, args: validatedArgs, context: currentContext },
signal
);
if (signal?.aborted) {
return immediateError("Operation aborted");
}
if (beforeResult?.block) {
return {
kind: "immediate",
result: createErrorToolResult(beforeResult.reason || "Tool execution was blocked"),
isError: true
};
}
}
if (signal?.aborted) {
return immediateError("Operation aborted");
}
// 5. Return prepared call for execution
return {
kind: "prepared",
toolCall,
tool,
args: validatedArgs
};
} catch (error) {
return {
kind: "immediate",
result: createErrorToolResult(error.message),
isError: true
};
}
}
```
---
## Tool Execution Flow
### `executePreparedToolCall()`
```typescript
async function executePreparedToolCall(
prepared: PreparedToolCall,
signal: AbortSignal | undefined,
emit: AgentEventSink,
): Promise<ExecutedToolCallOutcome> {
const updateEvents: Promise<void>[] = [];
let acceptingUpdates = true;
try {
// Call tool.execute() with onUpdate callback
const result = await prepared.tool.execute(
prepared.toolCall.id,
prepared.args,
signal,
(partialResult) => {
if (!acceptingUpdates) return;
// Buffer update events to emit in order
updateEvents.push(
Promise.resolve(
emit({
type: "tool_execution_update",
toolCallId: prepared.toolCall.id,
toolName: prepared.toolCall.name,
args: prepared.toolCall.arguments,
partialResult
})
)
);
}
);
acceptingUpdates = false;
await Promise.all(updateEvents); // Wait for all updates to flush
return { result, isError: false };
} catch (error) {
acceptingUpdates = false;
await Promise.all(updateEvents);
return {
result: createErrorToolResult(error.message),
isError: true
};
} finally {
acceptingUpdates = false;
}
}
```
---
## Tool Finalization Flow
### `finalizeExecutedToolCall()`
```typescript
async function finalizeExecutedToolCall(
currentContext: AgentContext,
assistantMessage: AssistantMessage,
prepared: PreparedToolCall,
executed: ExecutedToolCallOutcome,
config: AgentLoopConfig,
signal: AbortSignal | undefined,
): Promise<FinalizedToolCallOutcome> {
let result = executed.result;
let isError = executed.isError;
// afterToolCall hook - can override result
if (config.afterToolCall) {
try {
const afterResult = await config.afterToolCall(
{
assistantMessage,
toolCall: prepared.toolCall,
args: prepared.args,
result,
isError,
context: currentContext
},
signal
);
if (afterResult) {
// Field-by-field override (no deep merge)
result = {
...result,
content: afterResult.content ?? result.content,
details: afterResult.details ?? result.details,
usage: afterResult.usage ?? result.usage,
terminate: afterResult.terminate ?? result.terminate,
};
isError = afterResult.isError ?? isError;
}
} catch (error) {
result = createErrorToolResult(error.message);
isError = true;
}
}
return {
toolCall: prepared.toolCall,
result,
isError
};
}
```
---
## Termination Logic
### `shouldTerminateToolBatch()`
```typescript
function shouldTerminateToolBatch(finalizedCalls: FinalizedToolCallOutcome[]): boolean {
return finalizedCalls.length > 0 &&
finalizedCalls.every(f => f.result.terminate === true);
}
```
**Key Points**:
- Only terminates if **ALL** tool calls set `terminate: true`
- Allows partial tool execution while signaling early termination
### `shouldStopAfterTurn()`
Called after `turn_end`, before checking steering/follow-up queues:
```typescript
if (await config.shouldStopAfterTurn?.({
message,
toolResults,
context: currentContext,
newMessages
})) {
await emit({ type: "agent_end", messages: newMessages });
return;
}
```
**Common use cases**:
- Stop before context gets too large
- Stop after completing a specific goal
- Stop on error
---
## Queue Management
### Steering Queue
**Purpose**: Interrupt agent while it's working.
**When drained**: After each turn ends, before next LLM call.
**Mode**: `"all"` or `"one-at-a-time"`
```typescript
// Example: Steer agent mid-execution
agent.steer("Wait, let me check something else first");
agent.steer("Also, use a different approach");
```
### Follow-up Queue
**Purpose**: Queue messages for after agent would naturally stop.
**When drained**: When agent has no more tool calls and no steering messages.
**Mode**: `"all"` or `"one-at-a-time"`
```typescript
// Example: Follow up after agent finishes
agent.followUp("Now summarize what you did");
agent.followUp("What's next?");
```
---
## Error Handling
### Truncated Tool Calls
```typescript
async function failToolCallsFromTruncatedMessage(
toolCalls: AgentToolCall[],
emit: AgentEventSink
): Promise<ExecutedToolCallBatch> {
// All tool calls from truncated assistant message fail
// Reason: tool call arguments may be incomplete
for (const toolCall of toolCalls) {
await emit({ type: "tool_execution_start", ... });
await emit({
type: "tool_execution_end",
toolCallId: toolCall.id,
toolName: toolCall.name,
result: createErrorToolResult(
`Tool call was not executed: response hit output token limit, arguments may be truncated.`
),
isError: true
});
}
return { messages: [], terminate: false };
}
```
---
## Abort Handling
All async operations respect the abort signal:
```typescript
// In prepareToolCall
if (signal?.aborted) {
return immediateError("Operation aborted");
}
// In executePreparedToolCall
const result = await tool.execute(id, args, signal, onUpdate);
// Tool can check signal.aborted and cancel long-running operations
// In streamAssistantResponse
for await (const event of response) {
if (signal?.aborted) {
throw new Error("Aborted");
}
// Process event
}
```
---
## Summary
The agent loop is a **two-level iterator**:
1. **Outer loop**: Handles follow-up messages after agent would stop
2. **Inner loop**: Handles tool calls and steering messages
Each iteration:
- Streams assistant response (LLM)
- Executes tool calls (sequential or parallel)
- Emits events for UI updates
- Updates context with new messages
The loop terminates when:
- `shouldStopAfterTurn()` returns true
- Error or abort occurs
- No more steering/follow-up messages
+792
View File
@@ -0,0 +1,792 @@
# Hook System Reference
## Overview
The hook system provides **extensibility points** at both the Agent and AgentHarness layers. Hooks are asynchronous, can be cancelled via abort signal, and run in subscription order.
---
## Hook Categories
### 1. Message Transformation Hooks
#### `convertToLlm`
**Location**: `AgentLoopConfig.convertToLlm`
**Purpose**: Convert `AgentMessage[]` to `Message[]` before LLM call.
**When called**: Just before each LLM request.
**Key contract**:
- Must not throw or reject
- Must handle all `AgentMessage` variants
- Filter out UI-only messages (notifications, artifacts, etc.)
**Example**:
```typescript
convertToLlm: (messages) => messages.filter(m =>
m.role === "user" ||
m.role === "assistant" ||
m.role === "toolResult"
)
```
#### `transformContext`
**Location**: `AgentLoopConfig.transformContext` (optional)
**Purpose**: Manipulate context before LLM conversion.
**When called**: Before `convertToLlm`.
**Use cases**:
- Context window management (pruning old messages)
- Injecting external context
- Message deduplication
**Example**:
```typescript
transformContext: async (messages, signal) => {
if (estimateTokens(messages) > MAX_TOKENS) {
return pruneOldMessages(messages);
}
return messages;
}
```
---
### 2. Lifecycle Hooks
#### `beforeToolCall`
**Location**: `AgentLoopConfig.beforeToolCall` (optional)
**Context**:
```typescript
interface BeforeToolCallContext {
assistantMessage: AssistantMessage;
toolCall: AgentToolCall;
args: unknown; // Validated against tool schema
context: AgentContext;
}
```
**Return**:
```typescript
interface BeforeToolCallResult {
block?: boolean; // If true, tool won't execute
reason?: string; // Error message shown in tool result
}
```
**When called**: After args validation, before tool execution.
**Use cases**:
- Permission checks (user approval)
- Rate limiting
- Context-aware tool blocking
**Example**:
```typescript
beforeToolCall: async ({ toolCall, args, context }, signal) => {
if (toolCall.name === "bash" && signal?.aborted) {
return { block: true, reason: "Operation aborted" };
}
return undefined; // Allow execution
}
```
#### `afterToolCall`
**Location**: `AgentLoopConfig.afterToolCall` (optional)
**Context**:
```typescript
interface AfterToolCallContext {
assistantMessage: AssistantMessage;
toolCall: AgentToolCall;
args: unknown;
result: AgentToolResult<any>;
isError: boolean;
context: AgentContext;
}
```
**Return**:
```typescript
interface AfterToolCallResult {
content?: (TextContent | ImageContent)[];
details?: unknown;
isError?: boolean;
usage?: Usage;
terminate?: boolean; // Early termination hint
}
```
**When called**: After tool execution, before emitting `tool_execution_end`.
**Use cases**:
- Modify tool results (redact sensitive data)
- Update usage tracking
- Trigger early termination
**Example**:
```typescript
afterToolCall: async ({ result }, signal) => {
// Redact sensitive content
const content = result.content.map(c => {
if (c.type === "text") {
return { ...c, text: redactSecrets(c.text) };
}
return c;
});
return { content };
}
```
#### `shouldStopAfterTurn`
**Location**: `AgentLoopConfig.shouldStopAfterTurn` (optional)
**Context**:
```typescript
interface ShouldStopAfterTurnContext {
message: AssistantMessage;
toolResults: ToolResultMessage[];
context: AgentContext;
newMessages: AgentMessage[];
}
```
**Return**: `boolean`
**When called**: After `turn_end`, before draining steering/follow-up queues.
**Use cases**:
- Stop when goal achieved
- Stop before context gets too large
- Error recovery
**Example**:
```typescript
shouldStopAfterTurn: async ({ message, toolResults, context }) => {
// Stop if model indicates task complete
if (message.content.some(c =>
c.type === "text" && c.text.includes("TASK_COMPLETE"))) {
return true;
}
// Stop if context too large
if (estimateTokens(context.messages) > MAX_TOKENS * 0.8) {
return true;
}
return false;
}
```
#### `prepareNextTurn`
**Location**: `AgentLoopConfig.prepareNextTurn` (optional)
**Context**: Same as `ShouldStopAfterTurnContext`
**Return**:
```typescript
interface AgentLoopTurnUpdate {
context?: AgentContext;
model?: Model<any>;
thinkingLevel?: ThinkingLevel;
}
```
**When called**: After `shouldStopAfterTurn`, if not stopping.
**Use cases**:
- Update model based on conversation context
- Switch thinking level
- Inject new context
**Example**:
```typescript
prepareNextTurn: async ({ message, toolResults, context }) => {
// Switch to higher reasoning for complex tasks
if (toolResults.length > 3) {
return {
thinkingLevel: "high"
};
}
return undefined; // Keep current config
}
```
---
### 3. Queue Draining Hooks
#### `getSteeringMessages`
**Location**: `AgentLoopConfig.getSteeringMessages` (optional)
**Return**: `Promise<AgentMessage[]>`
**When called**: After turn ends, before next LLM call.
**Purpose**: Inject messages to interrupt agent mid-workflow.
**Mode**: `"all"` or `"one-at-a-time"` (controls how many messages injected)
**Example**:
```typescript
getSteeringMessages: async () => {
// Check for user input while agent is working
if (userQueue.length > 0) {
return userQueue.splice(0, 1); // one-at-a-time mode
}
return [];
}
```
#### `getFollowUpMessages`
**Location**: `AgentLoopConfig.getFollowUpMessages` (optional)
**Return**: `Promise<AgentMessage[]>`
**When called**: When agent would stop (no more tool calls, no steering messages).
**Purpose**: Queue messages for after agent finishes.
**Mode**: `"all"` or `"one-at-a-time"`
**Example**:
```typescript
getFollowUpMessages: async () => {
// Check if user typed while agent was working
if (followUpQueue.length > 0) {
return followUpQueue.splice(0, 1);
}
return [];
}
```
---
## AgentHarness Hooks
### 1. System Prompt Hooks
#### `before_agent_start`
**Location**: `AgentHarness.on("before_agent_start")`
**Event**:
```typescript
{
type: "before_agent_start";
prompt: string;
images?: ImageContent[];
systemPrompt: string;
resources: AgentHarnessResources;
}
```
**Return**:
```typescript
{
messages?: AgentMessage[];
systemPrompt?: string;
}
```
**When called**: Before agent starts, after system prompt generated.
**Use cases**:
- Add conversation hints
- Inject images
- Modify system prompt
### 2. Context Hooks
#### `context`
**Location**: `AgentHarness.on("context")`
**Event**:
```typescript
{
type: "context";
messages: AgentMessage[];
}
```
**Return**:
```typescript
{
messages: AgentMessage[];
}
```
**When called**: Before `convertToLlm`.
**Use cases**:
- Message filtering
- Context window management
- Message augmentation
### 3. Tool Hooks
#### `tool_call`
**Location**: `AgentHarness.on("tool_call")`
**Event**:
```typescript
{
type: "tool_call";
toolCallId: string;
toolName: string;
input: Record<string, unknown>;
}
```
**Return**:
```typescript
{
block?: boolean;
reason?: string;
}
```
**When called**: Before tool execution.
**Use cases**:
- Audit logging
- Approval workflows
- Input validation
#### `tool_result`
**Location**: `AgentHarness.on("tool_result")`
**Event**:
```typescript
{
type: "tool_result";
toolCallId: string;
toolName: string;
input: Record<string, unknown>;
content: (TextContent | ImageContent)[];
details: unknown;
isError: boolean;
usage?: Usage;
}
```
**Return**:
```typescript
{
content?: (TextContent | ImageContent)[];
details?: unknown;
isError?: boolean;
usage?: Usage;
terminate?: boolean;
}
```
**When called**: After tool execution.
**Use cases**:
- Result transformation
- Usage tracking
- Early termination
### 4. Session Hooks
#### `session_before_compact`
**Location**: `AgentHarness.on("session_before_compact")`
**Event**:
```typescript
{
type: "session_before_compact";
preparation: BranchPreparation;
branchEntries: SessionTreeEntry[];
customInstructions?: string;
signal: AbortSignal;
}
```
**Return**:
```typescript
{
cancel?: boolean;
compaction?: CompactionResult;
}
```
**When called**: Before compaction.
**Use cases**:
- Skip compaction in certain conditions
- Provide custom summary
- Abort compaction
#### `session_before_tree`
**Location**: `AgentHarness.on("session_before_tree")`
**Event**:
```typescript
{
type: "session_before_tree";
preparation: {
targetId: string;
oldLeafId: string;
commonAncestorId: string;
entriesToSummarize: SessionTreeEntry[];
userWantsSummary: boolean;
customInstructions?: string;
replaceInstructions?: boolean;
label?: string;
};
signal: AbortSignal;
}
```
**Return**:
```typescript
{
cancel?: boolean;
summary?: {
summary: string;
details?: unknown;
usage?: Usage;
};
customInstructions?: string;
replaceInstructions?: boolean;
}
```
**When called**: Before tree navigation (branching).
**Use cases**:
- Skip branch summary
- Provide custom summary
- Cancel navigation
### 5. Provider Hooks
#### `before_provider_request`
**Location**: `AgentHarness.on("before_provider_request")`
**Event**:
```typescript
{
type: "before_provider_request";
model: Model<any>;
sessionId: string;
streamOptions: AgentHarnessStreamOptions;
}
```
**Return**:
```typescript
{
streamOptions: AgentHarnessStreamOptionsPatch;
}
```
**When called**: Just before each LLM request.
**Use cases**:
- Add authentication headers
- Set request metadata
- Configure caching
#### `before_provider_payload`
**Location**: `AgentHarness.on("before_provider_payload")`
**Event**:
```typescript
{
type: "before_provider_payload";
model: Model<any>;
payload: unknown;
}
```
**Return**:
```typescript
{
payload: unknown;
}
```
**When called**: Just before sending payload to LLM.
**Use cases**:
- Payload transformation
- Debug logging
- Schema validation
---
## Hook Execution Order
### Full Turn Flow
```
1. AgentHarness.prompt()
├─► emit "before_agent_start"
│ └─► Hook can return new messages/systemPrompt
2. AgentLoopConfig creation
├─► transformContext hook → AgentLoop.transformContext
├─► convertToLlm hook → AgentLoop.convertToLlm
├─► beforeToolCall hook → AgentLoop.beforeToolCall
├─► afterToolCall hook → AgentLoop.afterToolCall
├─► prepareNextTurn hook → AgentLoop.prepareNextTurn
├─► shouldStopAfterTurn hook → AgentLoop.shouldStopAfterTurn
├─► getSteeringMessages hook → AgentLoop.getSteeringMessages
└─► getFollowUpMessages hook → AgentLoop.getFollowUpMessages
3. streamAssistantResponse()
├─► emit "before_provider_request" (harness)
│ └─► Hook can modify stream options
├─► transformContext() (agent)
├─► convertToLlm() (agent)
├─► streamFn() → LLM call
└─► Emit message_start/update/end events
4. executeToolCalls()
├─► For each tool call:
│ ├─► emit "tool_call" (harness)
│ │ └─► Hook can block execution
│ ├─► tool.execute()
│ └─► emit "tool_result" (harness)
│ └─► Hook can override result
5. turn_end
├─► emit "turn_end" (agent)
├─► shouldStopAfterTurn() (agent)
│ └─► Return true to exit
├─► prepareNextTurn() (agent)
│ └─► Hook can update context/model/thinkingLevel
├─► Drain steering queue
└─► Drain follow-up queue
├─► If steering/follow-up: repeat from #3
└─► If no more: agent_end
└─► emit "agent_end" (agent)
```
---
## Queue Mode Behavior
### `"all"` Mode
All queued messages are injected at once:
```
Agent would continue...
→ getFollowUpMessages returns [msg1, msg2, msg3]
→ All three injected together
→ Agent processes all before next turn
```
### `"one-at-a-time"` Mode
One message injected at a time:
```
Agent would continue...
→ getFollowUpMessages returns [msg1]
→ msg1 injected
→ Agent processes msg1
→ After turn, getFollowUpMessages returns [msg2]
→ msg2 injected
→ Agent processes msg2
→ ...and so on
```
---
## Abort Signal Propagation
All hooks receive an optional `AbortSignal`:
```typescript
interface BeforeToolCallContext {
// ... other fields
// signal is NOT included - use agent.signal instead
}
```
**Agent hooks**:
- `transformContext`: receives `signal`
- `beforeToolCall`: receives `signal`
- `afterToolCall`: receives `signal`
**Harness hooks**:
- `before_agent_start`: receives `signal`
- `context`: NO signal
- `tool_call`: NO signal
- `tool_result`: NO signal
- `session_before_compact`: receives `signal`
- `session_before_tree`: receives `signal`
- `before_provider_request`: receives `signal`
- `before_provider_payload`: NO signal
---
## Error Handling
### Hook Errors
**Agent layer**: Hook errors are caught and encoded in tool results:
```typescript
try {
const beforeResult = await config.beforeToolCall(...);
if (beforeResult?.block) {
return immediateError(beforeResult.reason);
}
} catch (error) {
return immediateError(error.message);
}
```
**Harness layer**: Hook errors are wrapped and re-thrown:
```typescript
try {
const result = await handler(event);
} catch (error) {
throw normalizeHookError(error);
}
```
### Best Practices
1. **Always handle errors**: Wrap async operations in try/catch
2. **Respect abort signals**: Check `signal.aborted` in long operations
3. **Return safe defaults**: Return empty arrays/objects on errors
4. **Don't block**: Hooks should be fast (no network calls)
5. **Idempotent**: Hooks should be safe to run multiple times
---
## Common Patterns
### 1. Context Window Management
```typescript
transformContext: async (messages, signal) => {
if (signal?.aborted) return messages;
const tokenCount = estimateTokens(messages);
if (tokenCount > MAX_TOKENS * 0.9) {
return pruneOldestMessages(messages, Math.floor(MAX_TOKENS * 0.3));
}
return messages;
}
```
### 2. Permission-Gated Tools
```typescript
beforeToolCall: async ({ toolCall, args }, signal) => {
if (toolCall.name === "bash" && signal?.aborted) {
return { block: true, reason: "Operation aborted" };
}
if (toolCall.name === "bash" && !await canExecuteBash(args)) {
return { block: true, reason: "Permission denied" };
}
return undefined;
}
```
### 3. Result Redaction
```typescript
afterToolCall: async ({ result }) => {
const content = result.content.map(c => {
if (c.type === "text") {
return { ...c, text: redactSecrets(c.text) };
}
return c;
});
return { content };
}
```
### 4. Early Termination
```typescript
shouldStopAfterTurn: async ({ message }) => {
// Check if model indicates completion
if (message.content.some(c =>
c.type === "text" && c.text.includes("TASK_COMPLETE"))) {
return true;
}
// Check if all tool calls set terminate
return false;
}
```
### 5. Audit Logging
```typescript
tool_call: async ({ toolCallId, toolName, input }) => {
console.log(`[TOOL_CALL] ${toolName} (${toolCallId}):`, input);
return undefined;
}
tool_result: async ({ toolCallId, toolName, content, isError }) => {
console.log(`[TOOL_RESULT] ${toolName} (${toolCallId}):`, {
hasError: isError,
contentLength: content.length
});
return undefined;
}
```
---
## Summary
| Hook | Layer | When | Can Block? |
|------|-------|------|------------|
| `convertToLlm` | Agent | Before LLM call | No (sync) |
| `transformContext` | Agent | Before `convertToLlm` | Yes (async) |
| `beforeToolCall` | Agent | After validation | Yes (async) |
| `afterToolCall` | Agent | After execution | Yes (async) |
| `shouldStopAfterTurn` | Agent | After turn_end | Yes (async) |
| `prepareNextTurn` | Agent | Before next turn | Yes (async) |
| `getSteeringMessages` | Agent | After turn_end | Yes (async) |
| `getFollowUpMessages` | Agent | When agent would stop | Yes (async) |
All hooks are **optional** and have sensible defaults.
@@ -0,0 +1,705 @@
# Session Architecture
## Overview
The session system provides **persistent, branchable conversation history**. It's the storage layer that enables:
- Conversation persistence across restarts
- Branching to earlier points in conversation
- Context window compaction
- Session tree navigation
---
## Core Concepts
### 1. SessionTreeEntry
The fundamental unit of session history:
```typescript
type SessionTreeEntry =
| MessageEntry
| ModelChangeEntry
| ThinkingLevelChangeEntry
| ActiveToolsChangeEntry
| CompactionEntry
| BranchSummaryEntry
| CustomEntry
| CustomMessageEntry
| LabelEntry
| LeafEntry
| SessionInfoEntry;
```
**Key properties**:
- `id`: Unique identifier (UUID v7)
- `parentId`: Points to parent entry (forms tree structure)
- `timestamp`: ISO 8601 string
### 2. Tree Structure
```
Entry tree (simplified):
root (parentId: null)
├─► message (user #1) [id: 1]
│ └─► message (assistant #1) [id: 2]
│ └─► tool_result [id: 3]
│ └─► message (user #2) [id: 4]
│ └─► compaction [id: 5] ← New root for future
│ ├─► retained messages here
│ └─► message (assistant #2) [id: 6]
│ └─► message (user #3) [id: 7]
│ └─► leaf [id: 8] ← Current head
└─► branch_summary [id: 9] ← Point where branch was created
└─► message (user #4) [id: 10]
└─► message (assistant #4) [id: 11]
└─► leaf [id: 12]
```
### 3. Context Building
**Context** = Current state needed for LLM call:
```typescript
interface SessionContext {
systemPrompt: string;
messages: AgentMessage[];
thinkingLevel: ThinkingLevel;
model: { provider: string; modelId: string } | null;
activeToolNames: string[] | null;
}
```
**Building context** involves:
1. Tracing from leaf to root (path entries)
2. Applying transforms (compaction, etc.)
3. Projecting entries to messages
4. Deriving state (model, thinking level, active tools)
---
## Session Storage Interface
### `SessionStorage<TMetadata>`
```typescript
interface SessionStorage<TMetadata extends SessionMetadata = SessionMetadata> {
// Metadata
readonly id: string;
readonly metadata: TMetadata;
// Entry operations
getLeafId(): Promise<string | null>;
setLeafId(id: string): Promise<void>;
getEntry(id: string): Promise<SessionTreeEntry | undefined>;
getEntries(options?: SessionEntryCursorOptions): Promise<SessionTreeEntry[]>;
getBranch(): Promise<SessionTreeEntry[]>;
// Write operations
appendEntry(entry: SessionTreeEntry): Promise<string>;
// Branch operations
fork(targetId: string): Promise<SessionStorage>;
delete(): Promise<void>;
// Cleanup
cleanup(): Promise<void>;
}
```
### Built-in Implementations
#### MemoryStorage
```typescript
class MemoryStorage<TMetadata> implements SessionStorage<TMetadata> {
// In-memory storage using Map
// Good for: Testing, short-lived sessions
// Not good for: Persistence across runs
}
```
#### JSONLStorage
```typescript
class JSONLStorage<TMetadata> implements SessionStorage<TMetadata> {
// File-based storage using JSONL format
// One file per entry: entries/{id}.json
// Metadata file: metadata.json
// Good for: Development, local sessions
// Not good for: High-concurrency, production
// File structure:
// session/
// metadata.json
// entries/
// {id1}.json
// {id2}.json
// ...
}
```
---
## Session Class
### `Session<TMetadata>`
High-level session API built on storage:
```typescript
class Session<TMetadata extends SessionMetadata = SessionMetadata> {
// Metadata
readonly id: string;
readonly storage: SessionStorage<TMetadata>;
// Read operations
getMetadata(): Promise<TMetadata>;
getLeafId(): Promise<string>;
getEntry(id: string): Promise<SessionTreeEntry | undefined>;
getBranch(): Promise<SessionTreeEntry[]>;
buildContext(options?: SessionContextBuildOptions): Promise<SessionContext>;
// Write operations
appendMessage(message: AgentMessage): Promise<string>;
appendModelChange(provider: string, modelId: string): Promise<string>;
appendThinkingLevelChange(thinkingLevel: ThinkingLevel): Promise<string>;
appendActiveToolsChange(activeToolNames: string[]): Promise<string>;
appendCompaction(...): Promise<string>;
appendBranchSummary(...): Promise<string>;
appendCustomEntry(customType: string, data: unknown): Promise<string>;
appendCustomMessageEntry(...): Promise<string>;
appendLabel(targetId: string, label: string): Promise<void>;
appendSessionName(name: string): Promise<string>;
// Branch operations
fork(targetId: string): Promise<Session>;
delete(): Promise<void>;
}
```
---
## Context Building Details
### Path Tracing
**Goal**: Get all entries from leaf to root.
```typescript
async function getPathEntries(session: Session): Promise<SessionTreeEntry[]> {
const path: SessionTreeEntry[] = [];
let currentId = await session.getLeafId();
while (currentId !== null) {
const entry = await session.getEntry(currentId);
if (!entry) break;
path.unshift(entry);
currentId = entry.parentId;
}
return path;
}
```
### Default Transform
**Purpose**: Apply compaction logic to context.
```typescript
function defaultContextEntryTransform(
pathEntries: readonly SessionTreeEntry[]
): SessionTreeEntry[] {
let compaction: CompactionEntry | null = null;
for (const entry of pathEntries) {
if (entry.type === "compaction") {
compaction = entry;
}
}
if (!compaction) {
return [...pathEntries]; // No compaction
}
// Compaction retains either:
// 1. All entries after compaction (retainedTail)
// 2. Entries from firstKeptEntryId to compaction (inclusive)
const entries: SessionTreeEntry[] = [compaction];
const compactionIdx = pathEntries.findIndex(e => e.id === compaction.id);
if (compaction.retainedTail) {
// Include everything after compaction
for (let i = compactionIdx + 1; i < pathEntries.length; i++) {
entries.push(pathEntries[i]!);
}
return entries;
}
if (compaction.firstKeptEntryId) {
// Include entries from firstKeptEntryId to compaction
let foundFirstKept = false;
for (let i = compactionIdx - 1; i >= 0; i--) {
const entry = pathEntries[i]!;
if (entry.id === compaction.firstKeptEntryId) foundFirstKept = true;
if (foundFirstKept) entries.unshift(entry);
}
}
// Always include entries after compaction
for (let i = compactionIdx + 1; i < pathEntries.length; i++) {
entries.push(pathEntries[i]!);
}
return entries;
}
```
### Entry to Message Projection
```typescript
function sessionEntryToContextMessages(
entry: SessionTreeEntry,
index: number,
entries: readonly SessionTreeEntry[],
options: SessionContextBuildOptions = {}
): AgentMessage[] {
if (entry.type === "message") {
return [entry.message as AgentMessage];
}
if (entry.type === "custom_message") {
return [createCustomMessage(...)];
}
if (entry.type === "compaction") {
return [
createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp),
...(entry.retainedTail ?? [])
];
}
if (entry.type === "branch_summary" && entry.summary) {
return [createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp)];
}
if (entry.type === "custom") {
// Custom entry projectors can convert to messages
return [...(options.entryProjectors?.[entry.customType]?.(entry, index, entries) ?? [])];
}
return []; // Skip other entry types
}
```
---
## Branching
### What is Branching?
Branching creates a **new session tree** from an existing one, starting at a specific point.
**Example use case**:
```
Original tree:
root → A → B → C → D (leaf)
Branch at B:
root → A → B → B' (leaf) ← New branch
\
→ C → D (leaf) ← Original branch
```
### Fork Operation
```typescript
async function fork(session: Session, targetId: string): Promise<Session> {
// 1. Clone storage (copy entries up to targetId)
const newStorage = await session.storage.fork(targetId);
// 2. Create new session from storage
const newSession = new Session({ storage: newStorage });
// 3. Set leaf to targetId
await newSession.getStorage().setLeafId(targetId);
return newSession;
}
```
### Branch Summary
When branching, a **branch_summary** entry is created:
```typescript
interface BranchSummaryEntry extends SessionTreeEntryBase {
type: "branch_summary";
summary: string; // Human-readable summary
details?: unknown; // Implementation details
usage?: Usage; // LLM usage for generating summary
fromId: string; // Entry ID where branch was created
}
```
**Purpose**: Help model understand what happened in the branch.
---
## Compaction
### What is Compaction?
Compaction replaces old conversation history with a **summary**, reducing context size.
**Before compaction**:
```
message (user #1)
message (assistant #1)
tool_result
message (user #2)
message (assistant #2)
tool_result
... (many more messages)
```
**After compaction**:
```
compaction (summary: "User asked X, assistant did Y, then Z...")
message (assistant #3) ← Recent messages retained
message (user #3)
```
### Compaction Entry
```typescript
interface CompactionEntry extends SessionTreeEntryBase {
type: "compaction";
summary: string; // Summarized history
firstKeptEntryId?: string; // First entry kept after compaction
tokensBefore: number; // Context size before compaction
details?: CompactionDetails; // File operations, etc.
usage?: Usage; // LLM usage for generating summary
retainedTail?: AgentMessage[]; // Recent messages stored inline
}
```
### Compaction Process
```typescript
async function compact(session: Session): Promise<CompactionResult> {
// 1. Get branch entries
const entries = await session.getBranch();
// 2. Prepare compaction
const preparation = prepareCompaction(entries, settings);
// Identifies which messages to summarize, retained tail, etc.
// 3. Generate summary using LLM
const summary = await generateSummary(
preparation.messagesToSummarize,
preparation.retainedTail
);
// 4. Create compaction entry
const compactionEntry: CompactionEntry = {
type: "compaction",
id: uuidv7(),
parentId: preparation.firstKeptEntry.parentId,
timestamp: new Date().toISOString(),
summary: summary.text,
firstKeptEntryId: preparation.firstKeptEntry.id,
tokensBefore: preparation.tokensBefore,
details: {
readFiles: preparation.fileOps.readFiles,
modifiedFiles: preparation.fileOps.modifiedFiles
},
usage: summary.usage
};
// 5. Persist entry
const compactionId = await session.storage.appendEntry(compactionEntry);
return {
summary: summary.text,
firstKeptEntryId: preparation.firstKeptEntry.id,
tokensBefore: preparation.tokensBefore,
usage: summary.usage,
retainedTail: preparation.retainedTail,
details: compactionEntry.details
};
}
```
---
## Session Repositories
### `SessionRepo<TMetadata>`
Repository pattern for session management:
```typescript
interface SessionRepo<TMetadata extends SessionMetadata = SessionMetadata> {
// CRUD
create(options: CreateSessionOptions<TMetadata>): Promise<Session<TMetadata>>;
open(id: string): Promise<Session<TMetadata>>;
list(): Promise<SessionInfo[]>;
delete(id: string): Promise<void>;
// Forking
fork(id: string, targetId: string): Promise<Session<TMetadata>>;
// Cleanup
cleanup(): Promise<void>;
}
```
### Built-in Implementations
#### MemoryRepo
```typescript
class MemoryRepo<TMetadata> implements SessionRepo<TMetadata> {
// In-memory storage using Map<string, Session<TMetadata>>
// Good for: Testing, ephemeral sessions
}
```
#### JSONLRepo
```typescript
class JSONLRepo<TMetadata> implements SessionRepo<TMetadata> {
// File-based storage
// Sessions stored in: sessions/{id}/
// Good for: Local development
}
```
---
## Entry Types Reference
### MessageEntry
```typescript
interface MessageEntry extends SessionTreeEntryBase {
type: "message";
message: AgentMessage;
}
```
**Stored**: Every user/assistant/toolResult message
### ModelChangeEntry
```typescript
interface ModelChangeEntry extends SessionTreeEntryBase {
type: "model_change";
provider: string;
modelId: string;
}
```
**Stored**: When model is changed via `setModel()`
### ThinkingLevelChangeEntry
```typescript
interface ThinkingLevelChangeEntry extends SessionTreeEntryBase {
type: "thinking_level_change";
thinkingLevel: ThinkingLevel;
}
```
**Stored**: When thinking level is changed via `setThinkingLevel()`
### ActiveToolsChangeEntry
```typescript
interface ActiveToolsChangeEntry extends SessionTreeEntryBase {
type: "active_tools_change";
activeToolNames: string[];
}
```
**Stored**: When active tools are changed via `setActiveTools()`
### CompactionEntry
```typescript
interface CompactionEntry extends SessionTreeEntryBase {
type: "compaction";
summary: string;
firstKeptEntryId?: string;
tokensBefore: number;
details?: CompactionDetails;
usage?: Usage;
retainedTail?: AgentMessage[];
}
```
**Stored**: After compaction
### BranchSummaryEntry
```typescript
interface BranchSummaryEntry extends SessionTreeEntryBase {
type: "branch_summary";
summary: string;
details?: unknown;
usage?: Usage;
fromId: string;
}
```
**Stored**: When creating a branch
### CustomEntry
```typescript
interface CustomEntry extends SessionTreeEntryBase {
type: "custom";
customType: string;
data: unknown;
}
```
**Stored**: Custom application data (not visible to model)
### CustomMessageEntry
```typescript
interface CustomMessageEntry extends SessionTreeEntryBase {
type: "custom_message";
customType: string;
content: string | (TextContent | ImageContent)[];
display: string;
details: unknown;
}
```
**Stored**: Custom messages that appear in conversation
### LabelEntry
```typescript
interface LabelEntry extends SessionTreeEntryBase {
type: "label";
targetId: string; // Entry ID being labeled
label: string;
}
```
**Stored**: User-assigned labels for entries
### LeafEntry
```typescript
interface LeafEntry extends SessionTreeEntryBase {
type: "leaf";
targetId: string; // Current leaf entry ID
}
```
**Stored**: Updates to current session head
### SessionInfoEntry
```typescript
interface SessionInfoEntry extends SessionTreeEntryBase {
type: "session_info";
name: string;
}
```
**Stored**: Session name/description
---
## Best Practices
### 1. Use Branching for Experiments
```typescript
// Original branch
await harness.prompt("Build a web app");
// Experiment branch
const experimentalSession = await session.fork(leafId);
const experimentalHarness = new AgentHarness({
...options,
session: experimentalSession
});
await experimentalHarness.prompt("Try using React instead");
```
### 2. Compact Regularly
```typescript
// After each turn, check if compaction needed
if (estimateTokens(context) > MAX_TOKENS * 0.8) {
await harness.compact();
}
```
### 3. Use Custom Entries for Metadata
```typescript
// Store application state without exposing to model
await harness.appendMessage({
role: "custom",
type: "task_progress",
taskId: "abc123",
steps: [...]
});
// Custom entry won't appear in model context
```
### 4. Label Important Points
```typescript
// Mark important conversation points
await harness.appendLabel(messageId, "IMPORTANT_DECISION");
await harness.appendLabel(messageId, "BLOCKER");
```
### 5. Handle Branching Gracefully
```typescript
try {
await harness.navigateTree(targetId, { summarize: true });
} catch (error) {
if (error instanceof AgentHarnessError && error.code === "branch_summary") {
// Branch summary failed, navigate without summary
await harness.navigateTree(targetId, { summarize: false });
}
}
```
---
## Summary
**Session architecture provides**:
- Persistent conversation history (JSONL storage)
- Branchable conversation trees
- Context window compaction
- Custom metadata and messages
**Key operations**:
- `buildContext()` → Get LLM context from tree
- `appendMessage()` → Add message to tree
- `fork()` → Create branch at point
- `compact()` → Summarize history
**Storage layers**:
- `MemoryStorage` → Testing, ephemeral
- `JSONLStorage` → Development, local
@@ -0,0 +1,709 @@
# Tool Execution Guide
## Overview
Tools are how the agent **interacts with the external world**. They can read files, execute commands, make API calls, or perform any action.
---
## Tool Definition
### Basic Structure
```typescript
interface AgentTool<TParameters extends TSchema, TDetails> extends Tool<TParameters> {
label: string; // Human-readable name for UI
prepareArguments?: (args: unknown) => Static<TParameters>; // Optional arg transformation
execute(
toolCallId: string,
params: Static<TParameters>,
signal?: AbortSignal,
onUpdate?: AgentToolUpdateCallback<TDetails>
): Promise<AgentToolResult<TDetails>>;
}
```
### Tool Result
```typescript
interface AgentToolResult<T> {
content: (TextContent | ImageContent)[]; // Returned to model
details: T; // Arbitrary data for logs/UI
usage?: Usage; // Tool-specific usage (not for LLM context)
addedToolNames?: string[]; // New tools introduced
terminate?: boolean; // Early termination hint
}
```
---
## Tool Execution Flow
```
1. LLM sends tool call
└─► AssistantMessage with toolCall content block
2. prepareToolCall()
├─► Find tool by name
├─► prepareArguments() [optional]
├─► validateToolArguments()
└─► beforeToolCall() hook
├─► Return {block: true} → Error tool result
└─► Continue
3. executePreparedToolCall()
├─► tool.execute() with onUpdate callback
└─► onUpdate(partialResult) → Emit tool_execution_update
4. finalizeExecutedToolCall()
└─► afterToolCall() hook
└─► Override result fields
5. Emit events
├─► tool_execution_end
├─► message_start (toolResult)
└─► message_end (toolResult)
```
---
## Built-in Tools
### 1. Bash Tool
**Purpose**: Execute shell commands.
**Parameters**:
```typescript
interface BashToolInput {
command: string;
}
```
**Returns**: Command output as text.
**Options**:
- `cwd`: Working directory
- `timeout`: Command timeout in seconds
- `maxStdoutLines`: Truncate stdout after N lines
- `maxStderrLines`: Truncate stderr after N lines
**Example**:
```typescript
const bashTool = createBashTool({
cwd: "/home/user/project",
timeout: 30,
maxStdoutLines: 1000,
maxStderrLines: 100
});
await bashTool.execute(
"run_123",
{ command: "ls -la" },
undefined,
onUpdate
);
// Result:
// {
// content: [{ type: "text", text: "drwxr-xr-x ... " }],
// details: {
// command: "ls -la",
// cwd: "/home/user/project",
// exitCode: 0,
// stdout: "...",
// stderr: ""
// }
// }
```
### 2. Read Tool
**Purpose**: Read files (text or binary).
**Parameters**:
```typescript
interface ReadToolInput {
path: string;
startLine?: number; // Optional line range
endLine?: number;
}
```
**Returns**: File contents as text or images (for image files).
**Options**:
- `maxSize`: Maximum file size in bytes
- `maxLines`: Maximum lines for text files
- `maxTotalSize`: Maximum total bytes for multiple files
- `imageProcessor`: Custom image handler
**Example**:
```typescript
const readTool = createReadTool({
maxSize: 1024 * 1024, // 1MB
maxLines: 5000,
imageProcessor: async (buffer) => ({
type: "text",
text: `Image of ${buffer.length} bytes`
})
});
await readTool.execute(
"read_456",
{ path: "src/app.ts", startLine: 1, endLine: 50 },
undefined,
onUpdate
);
// Result:
// {
// content: [{ type: "text", text: "import React from 'react';\n..." }],
// details: { path: "src/app.ts", linesRead: 50 }
// }
```
### 3. Write Tool
**Purpose**: Write files (create or overwrite).
**Parameters**:
```typescript
interface WriteToolInput {
path: string;
content: string;
}
```
**Returns**: Success/failure message.
**Example**:
```typescript
const writeTool = createWriteTool();
await writeTool.execute(
"write_789",
{ path: "src/app.ts", content: "console.log('Hello');" },
undefined,
onUpdate
);
// Result:
// {
// content: [{ type: "text", text: "✓ Wrote 25 bytes to src/app.ts" }],
// details: { path: "src/app.ts", bytesWritten: 25 }
// }
```
### 4. Edit Tool
**Purpose**: Make precise edits to files using line numbers or search/replace.
**Parameters**:
```typescript
interface EditToolInput {
path: string;
startLine: number;
endLine: number;
content: string;
}
```
**Returns**: Success/failure message with diff.
**Example**:
```typescript
const editTool = createEditTool();
await editTool.execute(
"edit_101",
{ path: "src/app.ts", startLine: 5, endLine: 10, content: "const x = 42;" },
undefined,
onUpdate
);
// Result:
// {
// content: [{ type: "text", text: "✓ Edited lines 5-10 in src/app.ts" }],
// details: {
// path: "src/app.ts",
// startLine: 5,
// endLine: 10,
// linesChanged: 6,
// diff: "- const x = 1\n+ const x = 42"
// }
// }
```
---
## Creating Custom Tools
### Basic Custom Tool
```typescript
const weatherTool: AgentTool<TSchema, WeatherDetails> = {
name: "get_weather",
label: "Get Weather",
description: "Get current weather for a city",
parameters: Type.Object({
city: Type.String({ description: "City name" })
}),
execute: async (toolCallId, params, signal, onUpdate) => {
try {
const response = await fetch(
`https://api.weather.com/v1/weather?city=${params.city}`,
{ signal }
);
if (!response.ok) {
throw new Error(`Weather API error: ${response.status}`);
}
const data = await response.json();
return {
content: [{ type: "text", text: `Temperature: ${data.temp}°C` }],
details: {
city: params.city,
temp: data.temp,
humidity: data.humidity,
condition: data.condition
},
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
}
};
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
throw error; // Re-throw abort
}
return {
content: [{ type: "text", text: `Error: ${error.message}` }],
details: { error: error.message },
isError: true
};
}
}
};
```
### Tool with Streaming Updates
```typescript
const backupTool: AgentTool<TSchema, BackupDetails> = {
name: "backup_database",
label: "Backup Database",
description: "Create database backup with progress updates",
parameters: Type.Object({
database: Type.String(),
destination: Type.String()
}),
execute: async (toolCallId, params, signal, onUpdate) => {
const totalSize = await getDatabaseSize(params.database);
let uploaded = 0;
const stream = createBackupStream(params.database);
for await (const chunk of stream) {
uploaded += chunk.length;
// Stream progress updates
onUpdate({
content: [{
type: "text",
text: `Backup progress: ${(uploaded / totalSize * 100).toFixed(1)}%`
}],
details: { uploaded, total: totalSize }
});
if (signal?.aborted) {
throw new Error("Backup cancelled");
}
}
await uploadToStorage(stream, params.destination);
return {
content: [{ type: "text", text: "Backup completed successfully" }],
details: {
database: params.database,
destination: params.destination,
size: uploaded,
duration: Date.now() - startTime
}
};
}
};
```
### Tool with Custom Error Handling
```typescript
const apiTool: AgentTool<TSchema, ApiDetails> = {
name: "make_api_call",
label: "Make API Call",
description: "Make HTTP request to external API",
parameters: Type.Object({
url: Type.String({ format: "uri" }),
method: Type.Optional(Type.String({ enum: ["GET", "POST", "PUT", "DELETE"] })),
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
body: Type.Optional(Type.String())
}),
execute: async (toolCallId, params, signal, onUpdate) => {
try {
const response = await fetch(params.url, {
method: params.method || "GET",
headers: params.headers,
body: params.body,
signal
});
// Handle HTTP errors
if (!response.ok) {
const errorBody = await response.text();
return {
content: [{
type: "text",
text: `HTTP ${response.status}: ${response.statusText}\n${errorBody}`
}],
details: {
url: params.url,
method: params.method,
statusCode: response.status,
body: errorBody
},
isError: true
};
}
const contentType = response.headers.get("content-type") || "";
let responseText = await response.text();
// Handle JSON responses
if (contentType.includes("application/json")) {
try {
const jsonData = JSON.parse(responseText);
responseText = JSON.stringify(jsonData, null, 2);
} catch {
// Not valid JSON, use as-is
}
}
return {
content: [{ type: "text", text: responseText }],
details: {
url: params.url,
method: params.method,
statusCode: response.status,
headers: Object.fromEntries(response.headers.entries())
}
};
} catch (error) {
// Handle network errors
return {
content: [{ type: "text", text: `Network error: ${error.message}` }],
details: {
url: params.url,
error: error.message
},
isError: true
};
}
}
};
```
---
## Tool Configuration
### Tool Options
Tools can be configured with options:
```typescript
const bashTool = createBashTool({
cwd: "/home/user/project",
timeout: 30,
maxStdoutLines: 1000,
maxStderrLines: 100
});
const readTool = createReadTool({
maxSize: 1024 * 1024, // 1MB
maxLines: 5000,
maxTotalSize: 10 * 1024 * 1024 // 10MB total
});
```
### Tool Context
Tools can receive application context:
```typescript
interface ToolContext {
userId: string;
environment: "dev" | "staging" | "prod";
permissions: string[];
}
const tool: AgentHarnessTool<ToolContext> = {
name: "deploy_service",
label: "Deploy Service",
description: "Deploy service to environment",
parameters: Type.Object({
service: Type.String(),
environment: Type.String({ enum: ["dev", "staging", "prod"] })
}),
execute: async (toolCallId, params, signal, onUpdate, context) => {
// Access context
if (!context.permissions.includes("deploy")) {
throw new Error("Permission denied");
}
if (context.environment === "prod" && !params.environment) {
throw new Error("Must specify environment for prod deployment");
}
// ...
}
};
const harness = new AgentHarness({
tools: [tool],
toolContext: {
userId: "user123",
environment: "prod",
permissions: ["read", "write", "deploy"]
}
});
```
---
## Tool Execution Modes
### Sequential Mode
Tools marked as sequential execute **one at a time**:
```typescript
const sequentialTool: AgentTool<TSchema> = {
name: "sequential_tool",
label: "Sequential Tool",
description: "Must run one at a time",
parameters: Type.Object({}),
executionMode: "sequential", // Key point
execute: async (toolCallId, params, signal, onUpdate) => {
// This tool won't run concurrently with other sequential tools
// Even if LLM sends multiple tool calls
}
};
```
### Parallel Mode (Default)
Tools execute **concurrently** by default:
```typescript
const parallelTool: AgentTool<TSchema> = {
name: "parallel_tool",
label: "Parallel Tool",
description: "Can run concurrently",
parameters: Type.Object({}),
// executionMode defaults to "parallel"
execute: async (toolCallId, params, signal, onUpdate) => {
// This tool can run alongside other parallel tools
}
};
```
### Agent-Level Execution Mode
```typescript
const agent = new Agent({
initialState: {...},
streamFn: ...
toolExecution: "sequential" // All tools sequential by default
});
```
---
## Error Handling
### Tool Errors
Tools should **throw** on critical errors (abort, timeout) but **return error results** on recoverable errors:
```typescript
execute: async (toolCallId, params, signal, onUpdate) => {
try {
// Check for abort first
if (signal?.aborted) {
throw new Error("Operation aborted");
}
// Do work...
// Return error result for recoverable errors
return {
content: [{ type: "text", text: "Error: Invalid input" }],
details: { error: "Invalid input" },
isError: true
};
} catch (error) {
// Re-throw abort errors
if (error instanceof Error && error.name === "AbortError") {
throw error;
}
// Return error result for other errors
return {
content: [{ type: "text", text: `Error: ${error.message}` }],
details: { error: error.message },
isError: true
};
}
}
```
### Blockable Tools
Use `beforeToolCall` hook to block tool execution:
```typescript
beforeToolCall: async ({ toolCall, args }, signal) => {
if (toolCall.name === "bash") {
// Check for dangerous commands
const dangerousPatterns = ["rm -rf", "sudo", "dd if="];
for (const pattern of dangerousPatterns) {
if (args.command?.includes(pattern)) {
return { block: true, reason: "Dangerous command blocked" };
}
}
}
return undefined; // Allow execution
}
```
---
## Best Practices
### 1. Respect Abort Signals
```typescript
execute: async (toolCallId, params, signal, onUpdate) => {
if (signal?.aborted) {
throw new Error("Operation aborted");
}
// Long-running operation
for await (const item of longProcess()) {
if (signal?.aborted) {
throw new Error("Operation aborted");
}
onUpdate({ content: [{ type: "text", text: "Processing..." }] });
}
}
```
### 2. Return Meaningful Error Messages
```typescript
// Bad
return { content: [{ type: "text", text: "Error" }], isError: true };
// Good
return {
content: [{ type: "text", text: "Failed to read file: permission denied" }],
details: { path: "/etc/passwd", error: "EACCES" },
isError: true
};
```
### 3. Stream Progress for Long Operations
```typescript
execute: async (toolCallId, params, signal, onUpdate) => {
for (let i = 0; i < 100; i++) {
// Do work...
onUpdate({
content: [{ type: "text", text: `Progress: ${i}%` }],
details: { progress: i }
});
}
return {
content: [{ type: "text", text: "Complete" }],
details: { progress: 100 }
};
}
```
### 4. Use Proper Tool Result Types
```typescript
interface BashDetails {
command: string;
cwd: string;
exitCode: number;
stdout: string;
stderr: string;
}
return {
content: [{ type: "text", text: "Command executed" }],
details: { command, cwd, exitCode, stdout, stderr } as BashDetails
};
```
### 5. Handle Large Outputs
```typescript
execute: async (toolCallId, params, signal, onUpdate) => {
const stdoutLines: string[] = [];
const stderrLines: string[] = [];
for await (const chunk of process.stdout) {
stdoutLines.push(chunk);
if (stdoutLines.length > MAX_LINES) {
break; // Truncate
}
}
return {
content: [{ type: "text", text: truncate(stdoutLines.join("\n")) }],
details: { stdout: stdoutLines.join("\n") }
};
}
```
---
## Summary
**Tools are the bridge** between the agent and the external world.
**Key principles**:
- Return `isError: true` for recoverable errors
- Throw on abort/timeout
- Stream progress for long operations
- Respect abort signals throughout
- Use detailed error messages
**Built-in tools**:
- `bash`: Execute shell commands
- `read`: Read files
- `write`: Write files
- `edit`: Make precise edits
**Custom tools** can do anything: API calls, database queries, file operations, etc.
@@ -0,0 +1,803 @@
# AgentHarness Reference
## Overview
`AgentHarness` is the **high-level API** that wraps the core agent with session management, persistence, branching, and tool context binding.
---
## Key Differences: Agent vs AgentHarness
| Feature | Agent (Core) | AgentHarness |
|---------|-------------|--------------|
| **Session Persistence** | No | Yes (JSONL/Memory) |
| **Branching** | No | Yes |
| **Context Compaction** | No | Yes |
| **Tool Context** | Manual | Automatic binding |
| **Skills/Templates** | Manual | Built-in |
| **State Management** | Manual | Automatic |
| **Event Hooks** | Basic | Rich system |
---
## AgentHarness Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ AgentHarness │
├─────────────────────────────────────────────────────────────┤
│ State │
│ ├─ Session (persistence) │
│ ├─ Model │
│ ├─ ThinkingLevel │
│ ├─ Tools (Map) │
│ ├─ ActiveTools (string[]) │
│ └─ SystemPrompt (string or function) │
│ │
│ Queues │
│ ├─ steerQueue (messages to interrupt agent) │
│ ├─ followUpQueue (messages after agent stops) │
│ └─ nextTurnQueue (messages for next turn) │
│ │
│ Hooks │
│ ├─ before_agent_start │
│ ├─ context │
│ ├─ tool_call │
│ ├─ tool_result │
│ ├─ session_before_compact │
│ ├─ session_before_tree │
│ ├─ before_provider_request │
│ └─ before_provider_payload │
│ │
│ Methods │
│ ├─ prompt() - Run new conversation │
│ ├─ skill() - Execute skill │
│ ├─ promptFromTemplate() - Run template │
│ ├─ steer() - Interrupt agent │
│ ├─ followUp() - Queue message │
│ ├─ compact() - Compress context │
│ ├─ navigateTree() - Branch session │
│ └─ subscribe() - Add event listener │
└─────────────────────────────────────────────────────────────┘
```
---
## Core Concepts
### 1. Session
The session holds **conversation history as a tree**:
```typescript
interface Session<TMetadata extends SessionMetadata = SessionMetadata> {
readonly id: string;
readonly storage: SessionStorage<TMetadata>;
getMetadata(): Promise<TMetadata>;
getLeafId(): Promise<string>;
getEntry(id: string): Promise<SessionTreeEntry | undefined>;
getBranch(): Promise<SessionTreeEntry[]>;
buildContext(options?: SessionContextBuildOptions): Promise<SessionContext>;
appendMessage(message: AgentMessage): Promise<string>;
appendModelChange(provider: string, modelId: string): Promise<string>;
appendThinkingLevelChange(thinkingLevel: ThinkingLevel): Promise<string>;
appendActiveToolsChange(activeToolNames: string[]): Promise<string>;
appendCompaction(...): Promise<string>;
appendBranchSummary(...): Promise<string>;
fork(targetId: string): Promise<Session>;
}
```
### 2. Resources
Skills and prompt templates available to the agent:
```typescript
interface AgentHarnessResources<TSkill = Skill, TPromptTemplate = PromptTemplate> {
skills?: TSkill[];
promptTemplates?: TPromptTemplate[];
}
interface Skill {
name: string;
description: string;
content: string;
filePath: string;
disableModelInvocation?: boolean;
}
interface PromptTemplate {
name: string;
description?: string;
content: string;
}
```
### 3. Tool Context
Context passed to all tool executions:
```typescript
interface ToolContext {
userId: string;
environment: "dev" | "staging" | "prod";
// ... custom properties
}
// Zero-arg function for dynamic context
type ToolContextProvider<TContext> = () => TContext | Promise<TContext>;
```
---
## AgentHarness API
### Constructor
```typescript
constructor(options: AgentHarnessOptions<TContext, TSkill, TPromptTemplate, TTool>)
```
**Options**:
```typescript
interface AgentHarnessOptions<TContext, TSkill, TPromptTemplate, TTool> {
session: Session; // Session storage
models: Models; // LLM provider
resources?: AgentHarnessResources<TSkill, TPromptTemplate>;
streamOptions?: AgentHarnessStreamOptions;
retry?: RetryPolicy;
// System prompt
systemPrompt?:
| string // Static string
| AgentHarnessSystemPrompt<TContext, TSkill, TPromptTemplate, TTool>; // Dynamic function
// Tool context
toolContext?: AgentHarnessToolContextSource<TContext>;
// Tools
tools?: TTool[];
// Active tools
activeToolNames?: string[];
// Model and thinking
model: Model<any>;
thinkingLevel?: ThinkingLevel;
// Queue modes
steeringMode?: QueueMode;
followUpMode?: QueueMode;
}
```
**Example**:
```typescript
const harness = new AgentHarness({
session: memorySession,
models: models,
resources: {
skills: [weatherSkill, gitSkill],
promptTemplates: [summaryTemplate]
},
systemPrompt: async ({ session, model, activeTools, resources }) => {
const sessionMetadata = await session.getMetadata();
const toolsList = activeTools.map(t => t.name).join(", ");
return `You are an AI assistant with access to tools: ${toolsList}.
Current session: ${sessionMetadata.id}
Date: ${new Date().toISOString()}
Available skills:
${resources.skills?.map(s => `- ${s.name}: ${s.description}`).join("\n")}
`;
},
toolContext: { userId: "user123", environment: "prod" },
tools: [weatherTool, gitTool, readFileTool],
activeToolNames: ["weather", "git"],
model: gpt4Model,
thinkingLevel: "medium"
});
```
### System Prompt
**Static string**:
```typescript
systemPrompt: "You are a helpful assistant."
```
**Dynamic function**:
```typescript
systemPrompt: async ({
session,
model,
thinkingLevel,
activeTools,
resources
}) => {
const metadata = await session.getMetadata();
return `System: ${metadata.id}
Model: ${model.id}
Date: ${new Date().toISOString()}
Active tools: ${activeTools.map(t => t.name).join(", ")}
`;
};
```
---
## Main Methods
### `prompt()`
Run a new prompt:
```typescript
async prompt(text: string, options?: { images?: ImageContent[] }): Promise<AssistantMessage>
```
**Flow**:
1. Validate harness is idle
2. Create turn state (context, tools, system prompt)
3. Emit `before_agent_start` hook
4. Run agent loop with prompt
5. Return assistant message
**Example**:
```typescript
const message = await harness.prompt("What's the weather in London?");
console.log(message.content); // Assistant response
```
### `skill()`
Execute a named skill:
```typescript
async skill(name: string, additionalInstructions?: string): Promise<AssistantMessage>
```
**Example**:
```typescript
const message = await harness.skill("git", "Also create a PR for the changes");
// Skill content injected into prompt
```
### `promptFromTemplate()`
Execute a prompt template:
```typescript
async promptFromTemplate(
name: string,
args: string[] = []
): Promise<AssistantMessage>
```
**Example**:
```typescript
// Template: "Fix the following error: {{0}}"
const message = await harness.promptFromTemplate("fix_error", ["TypeError: x is undefined"]);
```
### `steer()`
Interrupt agent mid-execution:
```typescript
async steer(text: string, options?: { images?: ImageContent[] }): Promise<void>
```
**Example**:
```typescript
await harness.prompt("Write a long report...");
// While agent is working...
await harness.steer("Wait, change focus to climate change");
// Agent continues with new instructions
```
### `followUp()`
Queue message for after agent stops:
```typescript
async followUp(text: string, options?: { images?: ImageContent[] }): Promise<void>
```
**Example**:
```typescript
await harness.prompt("Analyze this data...");
// Agent finishes...
await harness.followUp("Now create a summary");
// Agent continues with summary request
```
### `nextTurn()`
Queue message for next turn (doesn't interrupt current turn):
```typescript
async nextTurn(text: string, options?: { images?: ImageContent[] }): Promise<void>
```
**Difference from `steer()`**:
- `steer()`: Interrupts immediately
- `nextTurn()`: Waits for current turn to finish
### `compact()`
Compress conversation history:
```typescript
async compact(customInstructions?: string): Promise<CompactResult>
```
**Returns**:
```typescript
interface CompactResult {
summary: string;
firstKeptEntryId?: string;
tokensBefore: number;
usage?: Usage;
retainedTail?: AgentMessage[];
details?: unknown;
}
```
**Example**:
```typescript
const result = await harness.compact();
console.log(`Compressed from ${result.tokensBefore} tokens to summary`);
```
### `navigateTree()`
Navigate conversation tree (branching):
```typescript
async navigateTree(
targetId: string,
options?: {
summarize?: boolean;
customInstructions?: string;
replaceInstructions?: boolean;
label?: string;
}
): Promise<NavigateTreeResult>
```
**Returns**:
```typescript
interface NavigateTreeResult {
cancelled: boolean;
editorText?: string; // If target is user message
summaryEntry?: BranchSummaryEntry;
}
```
**Example**:
```typescript
// Navigate to earlier point in conversation
const result = await harness.navigateTree("entry_abc123", { summarize: true });
// Create branch from current point
const newHarness = createNewHarness();
await newHarness.navigateTree("entry_xyz789");
```
---
## State Management
### Model
```typescript
getModel(): Model<any>;
async setModel(model: Model<any>): Promise<void>;
```
**Example**:
```typescript
console.log(harness.getModel().id); // "gpt-4"
await harness.setModel(gpt4oModel);
```
### Thinking Level
```typescript
getThinkingLevel(): ThinkingLevel;
async setThinkingLevel(level: ThinkingLevel): Promise<void>;
```
**Levels**: `"off"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"`
**Example**:
```typescript
await harness.setThinkingLevel("high"); // More reasoning for complex tasks
```
### Tools
```typescript
getTools(): TTool[];
getActiveTools(): TTool[];
async setTools(tools: TTool[], activeToolNames?: string[]): Promise<void>;
async setActiveTools(toolNames: string[]): Promise<void>;
```
**Example**:
```typescript
// Add new tool
await harness.setTools([...harness.getTools(), newTool]);
// Change active tools
await harness.setActiveTools(["read", "write"]);
```
### Resources
```typescript
getResources(): AgentHarnessResources<TSkill, TPromptTemplate>;
async setResources(resources: AgentHarnessResources<TSkill, TPromptTemplate>): Promise<void>;
```
**Example**:
```typescript
await harness.setResources({
skills: [...harness.getResources().skills, newSkill]
});
```
---
## Queue Management
### Steering Queue
```typescript
getSteeringMode(): QueueMode;
async setSteeringMode(mode: QueueMode): Promise<void>;
```
**Modes**:
- `"all"`: Drain all queued messages at once
- `"one-at-a-time"`: Drain one message at a time
### Follow-up Queue
```typescript
getFollowUpMode(): QueueMode;
async setFollowUpMode(mode: QueueMode): Promise<void>;
```
### Queue Helpers
```typescript
// Clear all queued messages
harness.clearAllQueues();
// Check if queues have pending messages
harness.hasQueuedMessages(); // boolean
```
---
## Event Handling
### Subscribe to All Events
```typescript
subscribe(
listener: (event: AgentHarnessEvent<TSkill, TPromptTemplate>, signal?: AbortSignal) => Promise<void> | void
): () => void;
```
**Event types**:
```typescript
type AgentHarnessEvent<TSkill, TPromptTemplate> =
// Agent events (forwarded from core agent)
| { type: "agent_start" }
| { type: "agent_end"; messages: AgentMessage[] }
| { type: "turn_start" }
| { type: "turn_end"; message: AgentMessage; toolResults: ToolResultMessage[] }
| { type: "message_start"; message: AgentMessage }
| { type: "message_update"; message: AgentMessage; assistantMessageEvent: AssistantMessageEvent }
| { type: "message_end"; message: AgentMessage }
| { type: "tool_execution_start"; toolCallId: string; toolName: string; args: any }
| { type: "tool_execution_update"; toolCallId: string; toolName: string; args: any; partialResult: any }
| { type: "tool_execution_end"; toolCallId: string; toolName: string; result: any; isError: boolean }
// Harness-specific events
| { type: "before_agent_start"; ... }
| { type: "context"; messages: AgentMessage[] }
| { type: "tool_call"; ... }
| { type: "tool_result"; ... }
| { type: "session_before_compact"; ... }
| { type: "session_before_tree"; ... }
| { type: "before_provider_request"; ... }
| { type: "before_provider_payload"; ... }
| { type: "after_provider_response"; ... }
| { type: "save_point"; ... }
| { type: "settled"; ... }
| { type: "model_update"; ... }
| { type: "thinking_level_update"; ... }
| { type: "tools_update"; ... }
| { type: "resources_update"; ... }
| { type: "session_compact"; ... }
| { type: "session_tree"; ... }
| { type: "queue_update"; ... }
| { type: "retry_scheduled"; ... }
| { type: "retry_attempt_start"; ... }
| { type: "retry_finished"; ... }
| { type: "abort"; clearedSteer: UserMessage[]; clearedFollowUp: UserMessage[] };
```
**Example**:
```typescript
const unsubscribe = harness.subscribe(async (event, signal) => {
if (event.type === "message_end") {
console.log("Message:", event.message.role);
}
if (event.type === "agent_end") {
console.log("Conversation complete");
}
if (event.type === "tool_execution_end") {
console.log("Tool:", event.toolName, "completed");
}
});
```
### Subscribe to Specific Events
```typescript
on<TType extends keyof AgentHarnessEventResultMap>(
type: TType,
handler: (event: Extract<AgentHarnessOwnEvent, { type: TType }>) => Promise<AgentHarnessEventResultMap[TType]> | AgentHarnessEventResultMap[TType]
): () => void;
```
**Example**:
```typescript
// Handle tool calls
harness.on("tool_call", async ({ toolCallId, toolName, input }) => {
console.log(`Tool ${toolName} called with:`, input);
return undefined; // Allow execution
});
// Handle tool results
harness.on("tool_result", async ({ toolName, content, isError }) => {
console.log(`Tool ${toolName} result:`, isError ? "Error" : "Success");
return undefined; // Use default result
});
// Modify system prompt
harness.on("before_agent_start", async ({ systemPrompt }) => {
return {
systemPrompt: `${systemPrompt}\n\nRemember to be concise.`
};
});
```
---
## Session Persistence
### Append Message
```typescript
async appendMessage(message: AgentMessage): Promise<void>;
```
**Example**:
```typescript
// Manually add message to session
await harness.appendMessage({
role: "user",
content: [{ type: "text", text: "Custom message" }],
timestamp: Date.now()
});
```
### Flush Pending Writes
```typescript
async abort(): Promise<AbortResult>
```
**Returns**:
```typescript
interface AbortResult {
clearedSteer: UserMessage[];
clearedFollowUp: UserMessage[];
}
```
**Example**:
```typescript
const result = await harness.abort();
console.log(`Cleared ${result.clearedSteer.length} steering messages`);
```
### Wait for Idle
```typescript
async waitForIdle(): Promise<void>;
```
**Example**:
```typescript
await harness.prompt("Do something...");
await harness.waitForIdle(); // Wait for completion
console.log("Done");
```
---
## Error Handling
### Error Codes
```typescript
type AgentHarnessErrorCode =
| "busy" // Agent is already processing
| "invalid_state" // Invalid state for operation
| "invalid_argument" // Invalid arguments
| "session" // Session error
| "hook" // Hook error
| "auth" // Authentication error
| "compaction" // Compaction error
| "branch_summary" // Branch summary error
| "unknown"; // Unknown error
```
### Error Handling Pattern
```typescript
try {
await harness.prompt("Do something");
} catch (error) {
if (error instanceof AgentHarnessError) {
switch (error.code) {
case "busy":
console.log("Agent busy, try again later");
break;
case "compaction":
console.log("Compaction failed:", error.message);
break;
case "hook":
console.log("Hook error:", error.cause?.message);
break;
default:
console.log("Error:", error.message);
}
}
}
```
---
## Advanced Patterns
### 1. Dynamic System Prompt
```typescript
systemPrompt: async ({ session, model, activeTools, resources }) => {
const metadata = await session.getMetadata();
// Customize based on session type
if (metadata.type === "coding") {
return `You are a coding assistant. Use tools: ${activeTools.map(t => t.name).join(", ")}`;
} else if (metadata.type === "writing") {
return `You are a writing assistant. Focus on clarity and style.`;
}
return "You are a helpful assistant.";
}
```
### 2. Conditional Tool Activation
```typescript
// Enable tools based on user request
harness.on("before_agent_start", async ({ prompt }) => {
if (prompt.includes("weather")) {
return {
messages: [{ role: "user", content: [{ type: "text", text: "Enable weather tool" }] }]
};
}
return undefined;
});
```
### 3. Session Branching
```typescript
async function exploreAlternative(harness: AgentHarness, prompt: string): Promise<AssistantMessage> {
// Get current leaf
const leafId = await harness.session.getLeafId();
// Create branch
const branchSession = await harness.session.fork(leafId);
const branchHarness = new AgentHarness({
...harnessOptions,
session: branchSession
});
// Run alternative
return await branchHarness.prompt(prompt);
}
```
### 4. Custom Compaction
```typescript
harness.on("session_before_compact", async ({ preparation }) => {
// Skip compaction for short sessions
if (preparation.tokensBefore < 1000) {
return { cancel: true };
}
// Provide custom summary
return {
compaction: {
summary: "User asked about X, Y, Z and assistant provided guidance.",
tokensBefore: preparation.tokensBefore,
firstKeptEntryId: preparation.firstKeptEntry.id,
details: { manual: true }
}
};
});
```
### 5. Tool Execution Logging
```typescript
harness.on("tool_call", async ({ toolName, input }) => {
console.log(`[TOOL_CALL] ${toolName}:`, JSON.stringify(input, null, 2));
return undefined;
});
harness.on("tool_result", async ({ toolName, content, isError }) => {
console.log(`[TOOL_RESULT] ${toolName}:`, isError ? "❌" : "✅");
return undefined;
});
```
---
## Summary
**AgentHarness provides**:
- Session persistence and tree navigation
- Built-in tool context binding
- Rich hook system for customization
- Skills and prompt templates
- Context compaction and branching
**Key methods**:
- `prompt()` - Main interaction
- `steer()` / `followUp()` - Queue management
- `compact()` - Context management
- `navigateTree()` - Branching
**Key patterns**:
- Dynamic system prompts
- Conditional tool activation
- Session branching for experimentation
- Hook-based customization
@@ -0,0 +1,687 @@
# Data Flow and State Management
## Overview
Understanding how data flows through the agent system is crucial for debugging and extending functionality.
---
## Message Flow
### 1. Input Messages
```typescript
// User input
await harness.prompt("Build a web app");
// Internal messages
await harness.steer("Wait, use React");
await harness.followUp("Now add tests");
await harness.nextTurn("Also deploy to production");
```
**Normalization**:
```typescript
function normalizePromptInput(input: string | AgentMessage | AgentMessage[]): AgentMessage[] {
if (Array.isArray(input)) return input;
if (typeof input !== "string") {
return [input]; // Already a message
}
// String → user message
return [{
role: "user",
content: [{ type: "text", text: input }],
timestamp: Date.now()
}];
}
```
### 2. AgentMessage Types
```typescript
type AgentMessage = Message | CustomAgentMessages[keyof CustomAgentMessages]
interface Message {
role: "user" | "assistant" | "toolResult";
content: (TextContent | ImageContent)[];
api?: string;
provider?: string;
model?: string;
usage?: Usage;
stopReason?: StopReason;
errorMessage?: string;
timestamp: number;
}
interface TextContent {
type: "text";
text: string;
}
interface ImageContent {
type: "image";
mediaType: string;
data: string; // Base64
}
```
### 3. Message Lifecycle
```
User Input
normalizePromptInput() → AgentMessage[]
runPromptMessages() → runWithLifecycle()
├─► Set isStreaming=true
├─► Create abort controller
└─► runAgentLoop()
runLoop()
├─► message_start (user prompt)
├─► message_end
├─► streamAssistantResponse()
│ ├─► message_start (assistant)
│ ├─► message_update (chunks)
│ └─► message_end
├─► executeToolCalls()
│ └─► message_start/end (toolResults)
└─► turn_end
handleAgentEvent() (harness)
├─► session.appendMessage()
│ └─► Storage: write entry
└─► Emit: message_end (forwarded)
```
---
## State Management
### Agent State
```typescript
interface AgentState {
systemPrompt: string;
model: Model<any>;
thinkingLevel: ThinkingLevel;
tools: AgentTool<any>[];
messages: AgentMessage[];
isStreaming: boolean;
streamingMessage?: AgentMessage;
pendingToolCalls: Set<string>;
errorMessage?: string;
}
```
**State changes**:
| Event | State Changed |
|-------|--------------|
| `message_start` | `streamingMessage` = message |
| `message_update` | `streamingMessage` = message |
| `message_end` | `messages.push(message)`, `streamingMessage` = undefined |
| `tool_execution_start` | `pendingToolCalls.add(toolCallId)` |
| `tool_execution_end` | `pendingToolCalls.delete(toolCallId)` |
| `turn_end` | `errorMessage` (if error) |
| `agent_end` | `streamingMessage` = undefined |
### State Mutation Example
```typescript
// In Agent.processEvents()
private async processEvents(event: AgentEvent): Promise<void> {
switch (event.type) {
case "message_start":
this._state.streamingMessage = event.message;
break;
case "message_end":
this._state.streamingMessage = undefined;
this._state.messages.push(event.message);
break;
case "tool_execution_start": {
const pending = new Set(this._state.pendingToolCalls);
pending.add(event.toolCallId);
this._state.pendingToolCalls = pending;
break;
}
case "tool_execution_end": {
const pending = new Set(this._state.pendingToolCalls);
pending.delete(event.toolCallId);
this._state.pendingToolCalls = pending;
break;
}
}
// Emit to listeners
for (const listener of this.listeners) {
await listener(event, signal);
}
}
```
---
## Context Flow
### Context Snapshot
```typescript
interface AgentContext {
systemPrompt: string;
messages: AgentMessage[];
tools?: AgentTool<any>[];
}
```
**When created**:
1. `Agent.createContextSnapshot()` - before each LLM call
2. `AgentHarness.createContext()` - in turn state
### Context Transformation
```typescript
// 1. transformContext() hook (AgentMessage[])
let messages = context.messages;
if (config.transformContext) {
messages = await config.transformContext(messages, signal);
}
// 2. convertToLlm() hook (AgentMessage[] → Message[])
const llmMessages = await config.convertToLlm(messages);
// 3. Build LLM context (Message[])
const llmContext: Context = {
systemPrompt: context.systemPrompt,
messages: llmMessages,
tools: context.tools
};
```
### Context Transformations
**Example: Prune old messages**
```typescript
transformContext: async (messages) => {
if (estimateTokens(messages) > MAX_TOKENS) {
// Find cut point (preserve recent turns)
const cutIndex = findCutPoint(messages, MAX_TOKENS * 0.7);
return messages.slice(cutIndex);
}
return messages;
}
```
**Example: Inject external context**
```typescript
transformContext: async (messages) => {
const externalData = await fetchExternalData();
const contextMessage: AgentMessage = {
role: "user",
content: [{ type: "text", text: externalData }],
timestamp: Date.now()
};
return [contextMessage, ...messages];
}
```
---
## Hook Context Flow
### Hook Parameter Flow
```
Agent.prompt()
├─► transformContext(messages) [AgentLoopConfig]
│ └─► Messages before LLM call
├─► convertToLlm(messages)
│ └─► Messages to send to LLM
├─► beforeToolCall(context) [AgentLoopConfig]
│ ├─► assistantMessage
│ ├─► toolCall
│ ├─► args (validated)
│ └─► context (AgentContext)
├─► afterToolCall(context) [AgentLoopConfig]
│ ├─► assistantMessage
│ ├─► toolCall
│ ├─► args
│ ├─► result (executed)
│ ├─► isError
│ └─► context (AgentContext)
├─► shouldStopAfterTurn(context) [AgentLoopConfig]
│ ├─► message (assistant)
│ ├─► toolResults
│ ├─► context (AgentContext)
│ └─► newMessages
├─► prepareNextTurn(context) [AgentLoopConfig]
│ └─► Return: context/model/thinkingLevel
├─► getSteeringMessages() [AgentLoopConfig]
│ └─► Messages to inject now
└─► getFollowUpMessages() [AgentLoopConfig]
└─► Messages for after agent stops
```
### Hook Return Value Flow
```
beforeToolCall()
├─► { block: true, reason } → Error tool result
└─► undefined → Allow execution
tool.execute()
afterToolCall()
├─► Override: content, details, isError, usage, terminate
└─► undefined → Use executed result
Emit: tool_execution_end
Create: ToolResultMessage
Emit: message_start/end (toolResult)
```
---
## Queue Flow
### Steering Queue
**Purpose**: Interrupt agent while working.
**Flow**:
```
steer("New instruction")
steeringQueue.enqueue(message)
After turn ends:
├─► getSteeringMessages() called
│ ├─► Drain queue (mode: "all" or "one-at-a-time")
│ └─► Return messages
Inject messages into context
Next LLM call includes steering messages
```
**Example**:
```typescript
// User types while agent is working
agent.steer("Wait, check this file first");
// Agent finishes current work
// → Steering messages injected
// → LLM sees: [original, ..., new user message]
```
### Follow-up Queue
**Purpose**: Queue messages for after agent stops naturally.
**Flow**:
```
followUp("Next task")
followUpQueue.enqueue(message)
Agent would stop (no more tool calls)
├─► getFollowUpMessages() called
│ ├─► Drain queue
│ └─► Return messages
Set as pendingMessages
Inner loop continues
```
**Example**:
```typescript
agent.followUp("Now create a README");
// Agent finishes current task
// → Follow-up messages injected
// → Agent continues with new task
```
### Queue Modes
**"all" Mode**:
```
Queued: [msg1, msg2, msg3]
Drain: [msg1, msg2, msg3]
All injected together
```
**"one-at-a-time" Mode**:
```
Queued: [msg1, msg2, msg3]
Drain: [msg1]
msg1 injected, msg2, msg3 remain
After next turn:
Drain: [msg2]
... and so on
```
---
## Session Flow
### Session Tree Structure
```
root (parentId: null)
├─► message [id: 1, parentId: null]
│ └─► message [id: 2, parentId: 1]
│ └─► tool_result [id: 3, parentId: 2]
│ └─► message [id: 4, parentId: 3]
│ └─► compaction [id: 5, parentId: 4]
│ ├─► retained: [msg6, msg7]
│ └─► message [id: 8, parentId: 5]
│ └─► leaf [id: 9, parentId: 8]
```
### Context Building
```typescript
async function buildContext(session: Session): Promise<SessionContext> {
// 1. Get path from leaf to root
const pathEntries = await session.getBranch();
// [root, msg1, msg2, toolResult, msg4, compaction, msg8, leaf]
// 2. Apply default transform (compaction logic)
const contextEntries = defaultContextEntryTransform(pathEntries);
// [compaction, retainedTail..., msg8]
// 3. Project entries to messages
const messages = contextEntries.flatMap(sessionEntryToContextMessages);
// [compactionSummary, retainedMsgs..., msg8]
// 4. Derive state
const state = deriveSessionContextState(pathEntries);
// { model, thinkingLevel, activeToolNames }
return { ...state, messages };
}
```
### Session Entry Types
| Type | Stored When |
|------|-------------|
| `message` | Every user/assistant/toolResult |
| `model_change` | `setModel()` called |
| `thinking_level_change` | `setThinkingLevel()` called |
| `active_tools_change` | `setActiveTools()` called |
| `compaction` | `compact()` called |
| `branch_summary` | Branching with summary |
| `custom` | `appendCustomEntry()` |
| `custom_message` | `appendCustomMessageEntry()` |
| `label` | `appendLabel()` |
| `leaf` | `setLeafId()` |
| `session_info` | `appendSessionName()` |
### Pending Writes
During active turns, writes are buffered:
```typescript
async function appendMessage(message: AgentMessage): Promise<void> {
if (phase === "idle") {
// Direct write
await session.appendMessage(message);
} else {
// Buffer for later
pendingSessionWrites.push({ type: "message", message });
}
}
async function flushPendingSessionWrites(): Promise<void> {
while (pendingSessionWrites.length > 0) {
const write = pendingSessionWrites.shift();
if (write.type === "message") {
await session.appendMessage(write.message);
} else if (write.type === "model_change") {
await session.appendModelChange(...);
}
// ... other types
}
}
```
---
## Tool Execution State Flow
### Tool Call State
```typescript
interface BeforeToolCallContext {
assistantMessage: AssistantMessage;
toolCall: AgentToolCall;
args: unknown; // Validated
context: AgentContext; // Snapshot
}
interface AfterToolCallContext {
assistantMessage: AssistantMessage;
toolCall: AgentToolCall;
args: unknown;
result: AgentToolResult<any>; // Executed
isError: boolean;
context: AgentContext;
}
```
### Tool Result State
```typescript
interface AgentToolResult<T> {
content: (TextContent | ImageContent)[]; // To model
details: T; // For logs/UI
usage?: Usage; // Tool-specific
addedToolNames?: string[]; // New tools
terminate?: boolean; // Early stop hint
}
```
### State Transition
```
Tool Call from LLM
prepareToolCall()
├─► Find tool
├─► Validate args
└─► beforeToolCall()
├─► block: true → Error
└─► block: undefined → Continue
tool.execute()
├─► onUpdate(partialResult)
└─► Return final result
afterToolCall()
├─► Override result
└─► Use executed result
createToolResultMessage()
Emit: tool_execution_end
Emit: message_start/end (toolResult)
Push to context.messages
```
---
## Abort Flow
### Abort Signal Propagation
```typescript
// 1. Create abort controller
const abortController = new AbortController();
// 2. Pass to all async operations
await runAgentLoop(..., abortController.signal, ...);
// 3. Check signal in long operations
execute: async (id, params, signal, onUpdate) => {
for await (const item of longProcess()) {
if (signal?.aborted) {
throw new Error("Aborted");
}
}
}
// 4. Abort
abortController.abort();
```
### Abort in Hooks
```typescript
// Check signal at start
beforeToolCall: async ({ toolCall }, signal) => {
if (signal?.aborted) {
return { block: true, reason: "Operation aborted" };
}
return undefined;
}
// Check signal in async operations
transformContext: async (messages, signal) => {
if (signal?.aborted) {
return messages; // Return safe fallback
}
// Long operation
const result = await expensiveTransform(messages, signal);
return result;
}
```
---
## Event Flow Diagram
```
┌─────────────────────────────────────────────────────────────────────┐
│ AGENT LIFECYCLE │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Agent.prompt("Hello") │
│ │ │
│ ├─► agent_start (event) │
│ ├─► turn_start (event) │
│ ├─► message_start (user) (event) │
│ ├─► message_end (user) (event) │
│ │ │
│ ├─► streamAssistantResponse() │
│ │ ├─► message_start (assistant) (event) │
│ │ ├─► message_update (text chunk 1) (event) │
│ │ ├─► message_update (text chunk 2) (event) │
│ │ ├─► message_update (toolCall) (event) │
│ │ └─► message_end (assistant) (event) │
│ │ │
│ ├─► executeToolCalls() │
│ │ ├─► tool_execution_start (event) │
│ │ ├─► tool_execute() │
│ │ │ └─► onUpdate(partial) (event) │
│ │ ├─► tool_execution_end (event) │
│ │ └─► message_start/end (toolResult) (events) │
│ │ │
│ ├─► turn_end (event) │
│ │ ├─► Should stop? → agent_end │
│ │ └─► Drain queues → another turn │
│ │ │
│ └─► agent_end (event) │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## Summary
**Key data flows**:
1. Input messages → Normalized → Agent messages
2. Agent messages → Context transform → LLM messages
3. LLM response → Streamed → Agent messages
4. Tool calls → Executed → Tool results → Agent messages
5. All messages → Session storage → Tree structure
**State management**:
- Agent: In-memory state with mutation on events
- Session: Persistent tree with entries
- Hooks: Transform data at key points
**Queue system**:
- Steering: Interrupt current work
- Follow-up: Queue for after agent stops
- Modes: "all" or "one-at-a-time"
+522
View File
@@ -0,0 +1,522 @@
# Learning Path and Study Guide
## Overview
This guide helps you learn the Pi Agent architecture **top-down**, starting from high-level concepts to implementation details.
---
## Phase 1: Big Picture (1-2 hours)
### Goal: Understand how components fit together
### Resources
1. **01-ARCHITECTURE-OVERVIEW.md** - Read this first
2. **Diagrams** - Study the architecture diagrams
### Key Questions to Answer
✅ What are the two main layers?
✅ What does each layer do?
✅ How do messages flow through the system?
✅ What is the relationship between Agent and AgentHarness?
✅ What are the main event types?
✅ How do tools integrate with the agent?
✅ What is the purpose of hooks?
### Exercises
1. Draw the architecture diagram from memory
2. List 3 use cases for each hook type
3. Trace a message from input to LLM to output
---
## Phase 2: Core Agent (2-3 hours)
### Goal: Understand the low-level agent loop
### Resources
1. **02-AGENT-LOOP-DETAILED.md** - Study the agent loop
2. Read `src/agent-loop.ts` (skim, focus on comments)
3. Read `src/types.ts` - Understand AgentEvent, AgentMessage, AgentTool
### Key Concepts
- **runAgentLoop()** - Starts a new conversation
- **runAgentLoopContinue()** - Continues existing conversation
- **runLoop()** - Main iteration (outer and inner loops)
- **streamAssistantResponse()** - Streams LLM response
- **executeToolCalls()** - Executes tool calls
- **prepareToolCall()** - Validates and prepares tools
- **executePreparedToolCall()** - Executes tool with updates
- **finalizeExecutedToolCall()** - Finalizes with hooks
### Key Questions to Answer
✅ What's the difference between outer and inner loop?
✅ How does streaming work?
✅ How are tool calls executed (sequential vs parallel)?
✅ What happens when a tool is blocked?
✅ How are errors handled?
✅ What are the four phases of tool execution?
✅ How does the loop know when to stop?
### Exercises
1. Trace through a conversation with 1 prompt + 2 tool calls
2. Draw the outer/inner loop flow
3. Explain how abort signals propagate
4. Explain queue draining (steering/follow-up)
---
## Phase 3: Hooks System (2-3 hours)
### Goal: Understand how to customize agent behavior
### Resources
1. **03-HOOK-SYSTEM.md** - Study all hooks
2. Read `src/types.ts` - Hook types and contexts
### Hook Categories
**Message Transformation**:
- `convertToLlm` - Convert messages to LLM format
- `transformContext` - Manipulate context before LLM
**Lifecycle Hooks**:
- `beforeToolCall` - Block or modify tool execution
- `afterToolCall` - Override tool results
- `shouldStopAfterTurn` - Request early termination
- `prepareNextTurn` - Update context/model/thinking
**Queue Draining**:
- `getSteeringMessages` - Interrupt agent mid-work
- `getFollowUpMessages` - Queue messages for later
### Key Questions to Answer
✅ What hooks receive abort signals?
✅ What hooks can block execution?
✅ What is the execution order of hooks?
✅ What's the difference between beforeToolCall and afterToolCall?
✅ How do you implement context window management?
✅ How do you implement permission checks?
✅ What's the difference between steering and follow-up?
### Exercises
1. Implement a hook that logs all tool calls
2. Implement a hook that blocks dangerous commands
3. Implement a hook that summarizes conversation every 5 turns
4. Implement a hook that switches to high thinking for complex tasks
---
## Phase 4: AgentHarness (3-4 hours)
### Goal: Understand high-level API and session management
### Resources
1. **06-AGENTHARNESS-REFERENCE.md** - Study the harness API
2. Read `src/harness/agent-harness.ts` (focus on public methods)
### Key Concepts
- **Session** - Persistent conversation history
- **SessionTreeEntry** - Individual entries in conversation
- **Context Building** - Derive LLM context from session
- **Branching** - Create new conversation paths
- **Compaction** - Summarize old history
### API Methods
**Core**:
- `prompt()` - Run new conversation
- `skill()` - Execute skill
- `promptFromTemplate()` - Run template
**Queues**:
- `steer()` - Interrupt agent
- `followUp()` - Queue message
- `nextTurn()` - Queue for next turn
**Session**:
- `compact()` - Compress context
- `navigateTree()` - Branch conversation
**State**:
- `setModel()` - Change model
- `setThinkingLevel()` - Change reasoning level
- `setTools()` / `setActiveTools()` - Manage tools
### Key Questions to Answer
✅ What's the difference between steer() and followUp()?
✅ How does branching work?
✅ How does compaction work?
✅ What's the relationship between Session and SessionStorage?
✅ What's the difference between MessageEntry and CustomEntry?
✅ How are pending writes handled during active turns?
✅ What hooks does AgentHarness provide?
### Exercises
1. Create a session, add messages, and build context
2. Implement branching and navigate between branches
3. Implement compaction and verify it works
4. Set up hooks for tool call logging
---
## Phase 5: Session Architecture (2-3 hours)
### Goal: Understand persistence and tree structure
### Resources
1. **04-SESSION-ARCHITECTURE.md** - Study session system
2. Read `src/harness/session/session.ts`
### Key Concepts
- **SessionTreeEntry** - Tree nodes
- **Path Tracing** - From leaf to root
- **Context Building** - Projection to messages
- **Default Transform** - Compaction logic
- **Forking** - Create branches
### Key Questions to Answer
✅ How is conversation history stored?
✅ What's the difference between ID and parentId?
✅ How does the session know the current head?
✅ What entries appear in the LLM context?
✅ How does compaction work at the session level?
✅ What's the difference between fork and navigateTree()?
✅ How are custom entries different from messages?
### Exercises
1. Create a session and trace its tree
2. Add custom entries and verify they don't appear in context
3. Fork a session and compare contexts
4. Compact a session and verify size reduction
---
## Phase 6: Tool Execution (2-3 hours)
### Goal: Understand how tools work
### Resources
1. **05-TOOL-EXECUTION.md** - Study tool system
2. Read `src/harness/tools/` - Built-in tools
### Key Concepts
- **AgentTool** - Tool definition
- **Tool Execution Flow** - Prepare → Execute → Finalize
- **Sequential vs Parallel** - Execution modes
- **Streaming Updates** - Progress updates
- **Error Handling** - Throw vs return error
### Tool Execution Flow
```
prepareToolCall()
├─ Find tool
├─ Validate args
└─ beforeToolCall()
executePreparedToolCall()
└─ tool.execute()
finalizeExecutedToolCall()
└─ afterToolCall()
emitToolResult()
```
### Key Questions to Answer
✅ What's the difference between prepareArguments and execute?
✅ How do streaming updates work?
✅ When do you throw vs return an error?
✅ How are sequential vs parallel tools different?
✅ What's in the ToolContext passed to execute()?
✅ How do you handle long-running operations?
✅ What's the terminate flag for?
### Exercises
1. Implement a custom tool (e.g., weather API)
2. Implement streaming updates for long operation
3. Implement tool with error handling
4. Test sequential vs parallel execution
---
## Phase 7: Data Flow (2-3 hours)
### Goal: Understand how data flows through the system
### Resources
1. **07-DATA-FLOW-STATE.md** - Study data flow
2. Read `src/agent.ts` - State management
### Key Concepts
- **AgentMessage** - Unified message type
- **AgentEvent** - Event stream
- **AgentContext** - Snapshot for LLM
- **State Mutation** - How state changes on events
- **Queue Flow** - Steering and follow-up
### Key Questions to Answer
✅ How do messages flow from input to LLM?
✅ How is state mutated on events?
✅ What's the difference between AgentContext and AgentState?
✅ How do hooks transform data?
✅ How are abort signals propagated?
✅ What's the relationship between queue mode and draining?
✅ How are pending writes handled?
### Exercises
1. Trace a message through the entire flow
2. Trace a tool call through all hooks
3. Trace an abort through the system
4. Draw the complete data flow diagram
---
## Phase 8: Implementation (4-6 hours)
### Goal: Implement your own version
### Steps
1. **Design your data structures** (in Julia)
- AgentMessage equivalent
- AgentEvent equivalent
- AgentTool equivalent
2. **Implement core agent loop**
- Message streaming
- Tool execution
- Event emission
3. **Add hooks system**
- Hook registration
- Hook execution
- Return value handling
4. **Implement session persistence**
- Tree structure
- Entry types
- Context building
5. **Add harness layer**
- High-level API
- Queue management
- Branching
### Recommended Order
```
1. Data Types (2h)
├─ AgentMessage
├─ AgentEvent
└─ AgentTool
2. Core Loop (4h)
├─ streamAssistantResponse
├─ executeToolCalls
└─ runLoop
3. State Management (2h)
├─ AgentState
└─ Event handlers
4. Hooks (3h)
├─ Hook system
└─ Implement hooks
5. Session (4h)
├─ Tree structure
├─ Persistence
└─ Context building
6. Harness (4h)
├─ Public API
├─ Queue management
└─ Branching
```
### Tips
- Start simple, iterate
- Test each component
- Follow TypeScript patterns
- Use your language's idioms
---
## Quick Reference
### Agent Layer
| Function | Purpose |
|----------|---------|
| `runAgentLoop()` | Start new conversation |
| `runAgentLoopContinue()` | Continue existing |
| `runLoop()` | Main iteration |
| `streamAssistantResponse()` | Stream LLM |
| `executeToolCalls()` | Execute tools |
### AgentHarness Layer
| Method | Purpose |
|--------|---------|
| `prompt()` | Run conversation |
| `steer()` | Interrupt agent |
| `followUp()` | Queue message |
| `compact()` | Compress context |
| `navigateTree()` | Branch conversation |
### Hooks
| Hook | Purpose |
|------|---------|
| `convertToLlm` | Convert messages |
| `transformContext` | Manipulate context |
| `beforeToolCall` | Block tools |
| `afterToolCall` | Override results |
| `shouldStopAfterTurn` | Request stop |
| `prepareNextTurn` | Update config |
| `getSteeringMessages` | Interrupt |
| `getFollowUpMessages` | Queue for later |
### Entry Types
| Type | Purpose |
|------|---------|
| `message` | User/assistant/toolResult |
| `model_change` | Model switch |
| `thinking_level_change` | Reasoning level |
| `active_tools_change` | Tools change |
| `compaction` | History summary |
| `branch_summary` | Branch marker |
| `custom` | App data |
| `custom_message` | Custom message |
| `label` | User label |
| `leaf` | Current head |
---
## Common Patterns
### 1. Context Window Management
```typescript
transformContext: async (messages, signal) => {
if (estimateTokens(messages) > MAX_TOKENS) {
return pruneOldMessages(messages);
}
return messages;
}
```
### 2. Tool Permission Checks
```typescript
beforeToolCall: async ({ toolCall, args }, signal) => {
if (toolCall.name === "bash" && !await canExecute(args)) {
return { block: true, reason: "Permission denied" };
}
return undefined;
}
```
### 3. Streaming Updates
```typescript
execute: async (id, params, signal, onUpdate) => {
for await (const chunk of process()) {
onUpdate({ content: [{ type: "text", text: `Progress: ${chunk}%` }] });
}
return finalResult;
}
```
### 4. Branching
```typescript
const branchSession = await session.fork(leafId);
const branchHarness = new AgentHarness({ session: branchSession });
```
---
## Study Schedule
| Week | Focus | Hours |
|------|-------|-------|
| 1 | Phases 1-2 | 6-8 |
| 2 | Phases 3-4 | 8-10 |
| 3 | Phases 5-6 | 6-8 |
| 4 | Phase 7-8 | 8-10 |
**Total**: 28-36 hours
---
## Next Steps
After understanding the architecture:
1. **Implement in Julia**
- Start with data types
- Implement core loop
- Add hooks
- Implement session
2. **Extend Functionality**
- Add new tool types
- Implement custom hooks
- Add new entry types
3. **Optimize**
- Improve token estimation
- Optimize context pruning
- Parallelize operations
4. **Production**
- Error handling
- Logging
- Monitoring
---
## Questions to Test Understanding
1. How would you implement a tool that requires user approval?
2. How would you implement conversation summarization every 10 turns?
3. How would you implement context pruning based on importance?
4. How would you implement branching with automatic summaries?
5. How would you implement tool execution rate limiting?
---
## Summary
**Top-down learning**:
1. Big picture (layers, components)
2. Core agent (loop, streaming)
3. Hooks (customization)
4. Harness (session, persistence)
5. Data flow (how everything connects)
**Key insight**: The system is built on **messages** and **events** with hooks for customization.
+695
View File
@@ -0,0 +1,695 @@
# Pi Agent Architecture - Visual Diagrams
## 1. System Architecture
```
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ APPLICATION LAYER │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Agent User │ │ AgentHarness │ │ AgentHarness │ │
│ │ (Low-Level) │ │ (High-Level) │ │ (Custom App) │ │
│ └────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │ │
│ └────────┬───────────────┴───────────────────────┬┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Agent Core │ │ AgentHarness │ │
│ │ │ │ │ │
│ │ • State mgmt │ │ • Session │ │
│ │ • Event stream │ │ • Compaction │ │
│ │ • Queue mgmt │ │ • Branching │ │
│ │ • Hook system │ │ • Skills │ │
│ └────────┬─────────┘ └────────┬─────────┘ │
└────────────────────┼─────────────────────────────────────┼─────────────────────────────────┘
│ │
┌────────────┴────────────┐ ┌──────────────┴──────────────┐
│ │ │ │
▼ ▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Agent Loop │ │ Agent Context │ │ Agent State │ │ Agent Event │
│ │ │ │ │ │ │ │
│ • runAgentLoop │ │ • Messages │ │ • Tools │ │ • agent_start │
│ • runLoop │ │ • System prompt │ │ • Messages │ │ • agent_end │
│ • streamResponse │ │ • Tools │ │ • isStreaming │ │ • turn_start │
│ • executeTools │ │ │ │ • pendingCalls │ │ • turn_end │
└────────┬─────────┘ └──────────────────┘ └──────────────────┘ │ • message_start │
│ │ • message_update │
▼ │ • message_end │
┌───────────────────────────────────────────────────────────────────────▼───────────────────┐
│ AGENT CORE (agent.ts, agent-loop.ts) │
└─────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ SESSION LAYER │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Session │ │ SessionStorage │ │ SessionRepo │ │
│ │ │ │ │ │ │ │
│ │ • Tree structure │ │ • Memory │ │ • Create │ │
│ │ • Context build │ │ • JSONL │ │ • Open │ │
│ │ • Branching │ │ │ │ • List │ │
│ │ • Compaction │ │ │ │ • Fork │ │
│ └────────┬─────────┘ └──────────────────┘ └──────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────────────────────┐ │
│ │ Session Tree │ │
│ │ │ │
│ │ root (null) │ │
│ │ ├─ message [id:1] ← User prompt │ │
│ │ │ └─ message [id:2] ← Assistant response │ │
│ │ │ └─ tool_result [id:3] ← Tool call result │ │
│ │ │ └─ message [id:4] ← User continuation │ │
│ │ │ └─ compaction [id:5] ← History summarized │ │
│ │ │ ├─ retained: [msg6, msg7] ← Recent messages kept │ │
│ │ │ └─ message [id:8] ← After compaction │ │
│ │ │ └─ leaf [id:9] ← Current head (cursor) │ │
│ │ │ │ │
│ │ └─ branch_summary [id:10] ← Branch point with summary │ │
│ │ └─ message [id:11] ← New branch message │ │
│ │ └─ leaf [id:12] ← New branch head │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ LLM PROVIDER LAYER │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ StreamFn │ │ Models API │ │ Provider API │ │
│ │ │ │ │ │ │ │
│ │ • streamSimple │ │ • completeSimple │ │ • OpenAI │ │
│ │ • completeSimple │ │ • Models catalog │ │ • Anthropic │ │
│ │ │ │ │ │ • Custom │ │
│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────┘
```
---
## 2. Message Flow Diagram
```
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ PROMPT FLOW │
└─────────────────────────────────────────────────────────────────────────────────────────────┘
User Input
├─► string: "Build a web app"
├─► AgentMessage: { role: "user", content: [...] }
└─► AgentMessage[]: [{...}, {...}]
Agent.prompt(input)
├─► normalizePromptInput()
│ ├─► string → { role: "user", content: [{ type: "text", text: input }] }
│ ├─► AgentMessage → [message]
│ └─► AgentMessage[] → messages
└─► runPromptMessages()
└─► runWithLifecycle()
├─► Set isStreaming = true
├─► Create abort controller
└─► runAgentLoop()
├─► emit: agent_start
├─► emit: turn_start
├─► emit: message_start (user prompt)
├─► emit: message_end (user prompt)
└─► runLoop()
├─► Check steering queue (drain if any)
├─► Check follow-up queue (skip if first turn)
└─► streamAssistantResponse()
├─► transformContext() [optional]
│ └─► AgentMessage[] → AgentMessage[]
├─► convertToLlm()
│ └─► AgentMessage[] → Message[]
├─► Build LLM Context
│ └─► { systemPrompt, messages, tools }
├─► Resolve API key (from hook)
└─► Call streamFn()
├─► LLM Provider API
└─► AssistantMessageEventStream
├─► message_start (assistant)
├─► message_update (text chunk 1)
├─► message_update (text chunk 2)
├─► message_update (toolCall)
└─► message_end (assistant)
└─► executeToolCalls()
├─► Sequential mode: tool calls one-by-one
└─► Parallel mode: tool calls concurrently
├─► prepareToolCall()
│ ├─► Find tool by name
│ ├─► prepareArguments() [optional]
│ ├─► validateToolArguments()
│ └─► beforeToolCall() hook
│ ├─► Return {block: true, reason}
│ └─► Return undefined
├─► executePreparedToolCall()
│ ├─► onUpdate(partialResult) [streaming updates]
│ └─► tool.execute()
└─► finalizeExecutedToolCall()
└─► afterToolCall() hook
├─► Override: content, details, isError, usage
└─► Use executed result
└─► Emit: tool_execution_start/update/end
└─► Create ToolResultMessage
└─► Emit: message_start/end (toolResult)
└─► turn_end
├─► prepareNextTurn() hook
│ └─► Return: context/model/thinkingLevel
├─► shouldStopAfterTurn() hook
│ └─► Return: boolean
├─► Drain steering queue
│ └─► getSteeringMessages() → inject
└─► Drain follow-up queue
└─► getFollowUpMessages() → inject
├─► Steering/follow-up exists? → Repeat from streamAssistantResponse()
└─► No more messages → emit: agent_end
└─► finishRun()
└─► isStreaming = false
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ CONTINUATION FLOW │
└─────────────────────────────────────────────────────────────────────────────────────────────┘
Agent.continue()
├─► Validate last message (must be user/toolResult)
└─► runAgentLoopContinue()
└─► runLoop() from current context (no new prompts)
└─► Same flow as above, starting from current context
```
---
## 3. Hook System Flow
```
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ HOOK EXECUTION ORDER │
└─────────────────────────────────────────────────────────────────────────────────────────────┘
AgentHarness.prompt()
├─► before_agent_start (harness hook)
│ └─► Can return: messages, systemPrompt
├─► transformContext() (agent hook)
│ └─► AgentMessage[] → AgentMessage[]
└─► streamAssistantResponse()
├─► before_provider_request (harness hook)
│ └─► Can modify: streamOptions
├─► convertToLlm() (agent hook)
│ └─► AgentMessage[] → Message[]
├─► streamFn()
└─► message_end (assistant)
└─► executeToolCalls()
├─► For each tool call:
│ ├─► tool_call (harness hook)
│ │ └─► Can return: block, reason
│ │
│ ├─► executePreparedToolCall()
│ │
│ └─► tool_result (harness hook)
│ └─► Can return: content, details, isError, usage, terminate
└─► turn_end
├─► shouldStopAfterTurn() (agent hook)
│ └─► Return: boolean
├─► prepareNextTurn() (agent hook)
│ └─► Return: context/model/thinkingLevel
├─► Drain steering queue
│ └─► getSteeringMessages() (agent hook)
└─► Drain follow-up queue
└─► getFollowUpMessages() (agent hook)
├─► Continue? → Repeat from streamAssistantResponse()
└─► Stop? → agent_end (harness hook)
```
---
## 4. Tool Execution Flow
```
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ TOOL EXECUTION FLOW │
└─────────────────────────────────────────────────────────────────────────────────────────────┘
Assistant Message with Tool Call
├─► { type: "toolCall", id: "tc_123", name: "bash", arguments: { command: "ls" } }
prepareToolCall()
├─► Find tool in currentContext.tools
│ └─► Not found? → immediate error result
├─► prepareToolCallArguments() [optional shim]
│ └─► Transform arguments before validation
├─► validateToolArguments()
│ └─► Validate against tool parameters schema
└─► beforeToolCall() hook
├─► Return { block: true, reason: "..." }
│ └─► Emit: tool_execution_start/update/end (error)
│ └─► Tool NOT executed
└─► Return undefined
executePreparedToolCall()
├─► tool.execute(toolCallId, validatedArgs, signal, onUpdate)
│ │
│ ├─► Long-running operation
│ │ └─► onUpdate({ content: [...], details: {...} })
│ │ └─► Emit: tool_execution_update
│ │
│ └─► Return: { content, details, usage, ... }
└─► Return: { result, isError }
finalizeExecutedToolCall()
└─► afterToolCall() hook
├─► Return override: { content, details, isError, usage, terminate }
│ └─► Merge: result = { ...result, ...override }
└─► Return: { toolCall, result, isError }
emitToolExecutionEnd()
└─► Emit: tool_execution_end
createToolResultMessage()
└─► Create ToolResultMessage
├─► toolCallId: tc_123
├─► toolName: bash
├─► content: result.content
├─► details: result.details
├─► usage: result.usage
├─► isError: result.isError
└─► timestamp: Date.now()
emitToolResultMessage()
├─► Emit: message_start (toolResult)
└─► Emit: message_end (toolResult)
Push to context.messages
Available for next LLM call
```
---
## 5. Session Tree Navigation
```
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ SESSION BRANCHING │
└─────────────────────────────────────────────────────────────────────────────────────────────┘
Original Session Tree:
├─ root
│ └─ message [user #1] [id: 1]
│ └─ message [assistant #1] [id: 2]
│ └─ tool_result [id: 3]
│ └─ message [user #2] [id: 4]
│ └─ leaf [id: 5] ← Current head
Navigate to entry [id: 2] with summarize=true
├─► Collect entries from leaf to target
│ └─► [leaf, msg4, tool_result, msg2] (path)
├─► Common ancestor: root
├─► Entries to summarize: [msg4, tool_result]
├─► Generate branch summary via LLM
├─► Create branch_summary entry
│ └─► { type: "branch_summary", summary: "...", fromId: 2 }
└─► Fork session at target [id: 2]
├─► Clone entries up to target
│ └─► [root, msg1, msg2, branch_summary]
└─► Set new leaf to [id: 2]
New Session Tree:
├─ root
│ └─ message [user #1] [id: 1]
│ └─ message [assistant #1] [id: 2]
│ └─ branch_summary [id: 6] ← New branch point
│ └─ leaf [id: 7] ← New head
└─ Original branch (still exists)
└─ message [user #2] [id: 4]
└─ tool_result [id: 3]
└─ leaf [id: 5] ← Old head
```
---
## 6. Context Window Compaction
```
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ CONTEXT COMPACTION │
└─────────────────────────────────────────────────────────────────────────────────────────────┘
Original Context (10,000 tokens):
├─ message [user #1]
├─ message [assistant #1]
├─ tool_result [id: 1]
├─ message [user #2]
├─ message [assistant #2]
├─ tool_result [id: 2]
├─ message [user #3]
├─ message [assistant #3]
├─ tool_result [id: 3]
├─ message [user #4]
├─ message [assistant #4]
├─ tool_result [id: 4]
├─ message [user #5]
├─ message [assistant #5]
└─ leaf [current]
Compact (threshold: 8,000 tokens)
├─► prepareCompaction()
│ │
│ ├─► Estimate tokens: 10,000
│ ├─► Target: 6,000 (80% of 8,000)
│ ├─► Find cut point: after message [assistant #3]
│ ├─► Messages to summarize: [msg1, msg2, ..., msg3]
│ └─► Retained tail: [msg4, msg5, leaf]
├─► LLM call to generate summary
└─► Create compaction entry
├─► summary: "User asked X, assistant did Y, then Z..."
├─► firstKeptEntryId: msg4.id
├─► tokensBefore: 10,000
├─► retainedTail: [msg4, msg5, leaf]
└─► details: { readFiles: [...], modifiedFiles: [...] }
Persisted Session Tree:
├─ root
│ └─ message [user #1]
│ └─ ... (original entries)
│ └─ compaction [id: new] ← New entry
│ ├─ summary: "User asked X..."
│ ├─ firstKeptEntryId: msg4.id
│ ├─ tokensBefore: 10000
│ ├─ retainedTail: [msg4, msg5, leaf]
│ └─ details: {...}
│ └─ msg4 [id: msg4]
│ └─ message [assistant #4]
│ └─ tool_result [id: 4]
│ └─ message [user #5]
│ └─ message [assistant #5]
│ └─ leaf [id: leaf]
└─ Context for LLM:
└─ [compaction summary, retainedTail messages]
```
---
## 7. State Mutation Flow
```
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ STATE MUTATION ON EVENTS │
└─────────────────────────────────────────────────────────────────────────────────────────────┘
Agent State:
├─ systemPrompt: string
├─ model: Model
├─ thinkingLevel: ThinkingLevel
├─ tools: AgentTool[]
├─ messages: AgentMessage[]
├─ isStreaming: boolean
├─ streamingMessage: AgentMessage? ← Partial assistant message
├─ pendingToolCalls: Set<string> ← Currently executing
└─ errorMessage: string?
Events and State Changes:
├─ agent_start
│ ├─ isStreaming = true
│ ├─ streamingMessage = undefined
│ └─ errorMessage = undefined
├─ message_start (user/assistant/toolResult)
│ └─ No state change (just event emission)
├─ message_update (assistant only)
│ └─ streamingMessage = updatedMessage
├─ message_end
│ ├─ streamingMessage = undefined
│ └─ messages.push(message)
├─ tool_execution_start
│ └─ pendingToolCalls.add(toolCallId)
├─ tool_execution_end
│ └─ pendingToolCalls.delete(toolCallId)
├─ turn_end
│ └─ if (message.errorMessage) errorMessage = message.errorMessage
└─ agent_end
├─ streamingMessage = undefined
└─ (run finishes, state cleared on finishRun())
```
---
## 8. Queue Flow
```
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ QUEUE DRAINING FLOW │
└─────────────────────────────────────────────────────────────────────────────────────────────┘
Steering Queue (mode: "one-at-a-time"):
├─ Queue: [msg1, msg2, msg3]
├─ After turn ends:
├─► getSteeringMessages()
│ ├─► mode = "one-at-a-time"
│ ├─► Drain: [msg1]
│ └─► Queue remaining: [msg2, msg3]
├─► Inject msg1 into context
└─► Next LLM call includes: [...original, msg1]
After next turn:
├─► getSteeringMessages()
│ ├─► Drain: [msg2]
│ └─► Queue remaining: [msg3]
└─► Inject msg2 into context
└─► ... and so on until queue empty
Follow-up Queue (mode: "all"):
├─ Queue: [msg1, msg2, msg3]
├─ Agent would stop (no more tool calls)
├─► getFollowUpMessages()
│ ├─► mode = "all"
│ ├─► Drain: [msg1, msg2, msg3]
│ └─► Queue remaining: []
├─► Set as pendingMessages
└─► Inner loop continues with: [...original, msg1, msg2, msg3]
└─► All three messages injected together
```
---
## 9. Event Sequence Examples
### Example 1: Simple Prompt
```
agent_start
turn_start
message_start (user: "Hello")
message_end (user: "Hello")
message_start (assistant: "")
message_update (assistant: "H")
message_update (assistant: "He")
message_update (assistant: "Hel")
message_update (assistant: "Hell")
message_update (assistant: "Hello")
message_end (assistant: "Hello")
turn_end
agent_end
```
### Example 2: Tool Execution
```
agent_start
turn_start
message_start (user: "List files")
message_end (user: "List files")
message_start (assistant: "")
message_update (assistant: "")
message_update (assistant: "")
message_update (assistant: "<tool_call name=bash>")
message_update (assistant: "<tool_call name=bash>")
message_update (assistant: "<tool_call name=bash>")
message_end (assistant: "<tool_call name=bash>")
tool_execution_start (bash: { command: "ls -la" })
tool_execution_update (bash: { progress: 0 })
tool_execution_update (bash: { progress: 50 })
tool_execution_update (bash: { progress: 100 })
tool_execution_end (bash: { exitCode: 0 })
message_start (toolResult: "drwxr-xr-x...")
message_end (toolResult: "drwxr-xr-x...")
turn_end
agent_end
```
### Example 3: Steering
```
agent_start
turn_start
message_start (user: "Build app")
message_end (user: "Build app")
message_start (assistant: "")
message_update (assistant: "Building...")
turn_end
├─ User types: "Wait, use React"
├─► steer("Wait, use React")
│ └─► steeringQueue.push(msg)
├─► getSteeringMessages()
│ └─► Drain and return [msg]
├─► Inject into context
└─► Next turn with: [...original, "Wait, use React"]
turn_start
message_start (user: "Wait, use React")
message_end (user: "Wait, use React")
message_start (assistant: "")
message_update (assistant: "Using React...")
turn_end
agent_end
```
---
## Summary
**Key Diagrams**:
1. System Architecture - Layers and components
2. Message Flow - Complete prompt flow
3. Hook System - Hook execution order
4. Tool Execution - Tool call lifecycle
5. Session Branching - Tree navigation
6. Context Compaction - History summarization
7. State Mutation - Event-driven state changes
8. Queue Flow - Steering and follow-up draining
9. Event Sequences - Real examples
These diagrams show how data flows through the agent system from user input to LLM response to tool execution and back.
+403
View File
@@ -0,0 +1,403 @@
# Quick Start Guide
## Learning the Pi Agent Architecture
This guide helps you quickly understand the agent system and prepare for reimplementation in Julia.
---
## Phase 1: Core Concepts (30 minutes)
### 1. Two-Layer Architecture
```
┌────────────────────────────────────────────────────────────────────┐
│ Application Layer │
│ ┌──────────────┐ ┌─────────────────┐ ┌───────────────────────┐ │
│ │ Agent (Core) │ │ AgentHarness │ │ Your Custom App │ │
│ └───────┬──────┘ └────────┬────────┘ └─────────┬────────────┘ │
└──────────┼───────────────────┼─────────────────────┼────────────────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌─────────────────┐ ┌────────────────┐
│ agent-loop.ts│ │ agent-harness.ts│ │ Session Repo │
│ types.ts │ │ │ │ │
└──────────────┘ └─────────────────┘ └────────────────┘
```
**Key Insight**:
- **Agent Core** = Low-level async iteration (messages in, messages out)
- **AgentHarness** = High-level session management with persistence
---
### 2. Core Data Types
```typescript
// Message: Basic unit of conversation
interface Message {
role: "user" | "assistant" | "toolResult";
content: (TextContent | ImageContent)[];
timestamp: number;
}
// Tool: Function the agent can call
interface AgentTool {
name: string;
label: string;
description: string;
parameters: Schema;
execute(toolCallId, params, signal, onUpdate): Promise<Result>;
}
// Event: Notification of state changes
type AgentEvent =
| { type: "agent_start" }
| { type: "agent_end"; messages: Message[] }
| { type: "turn_start" }
| { type: "turn_end"; message: Message; toolResults: Message[] }
| { type: "message_start"; message: Message }
| { type: "message_update"; message: Message }
| { type: "message_end"; message: Message }
| { type: "tool_execution_start"; ... }
| { type: "tool_execution_end"; ... };
```
---
## Phase 2: Message Flow (45 minutes)
### The Agent Loop
```
1. User Input
└─► Agent.prompt("Hello")
2. Agent Start
└─► emit: agent_start, turn_start, message_start/end (user)
3. LLM Streaming
└─► streamAssistantResponse()
└─► transformContext() → convertToLlm() → streamFn()
4. Tool Execution
└─► executeToolCalls()
└─► prepare → execute → finalize (for each tool)
5. Turn End
└─► emit: turn_end
└─► Check hooks, drain queues, decide next turn
6. Repeat or End
└─► Loop continues until no more work
```
### Key Insight
**Everything is a message**: User input, assistant response, tool calls, tool results, steering messages.
**Everything is an event**: State changes are emitted as events for UI updates.
---
## Phase 3: Hooks System (30 minutes)
### Hook Categories
| Category | Purpose | When Called |
|----------|---------|-------------|
| `convertToLlm` | Filter/transform messages | Before LLM call |
| `transformContext` | Manipulate context | Before LLM call |
| `beforeToolCall` | Block tool execution | Before tool runs |
| `afterToolCall` | Override tool results | After tool runs |
| `shouldStopAfterTurn` | Request early stop | After turn ends |
| `prepareNextTurn` | Update config | Before next turn |
| `getSteeringMessages` | Interrupt agent | After turn ends |
| `getFollowUpMessages` | Queue messages | When agent stops |
### Hook Flow
```
Agent.prompt("Build app")
├─► transformContext() [hook]
├─► convertToLlm() [hook]
├─► LLM call
├─► executeToolCalls()
│ ├─► beforeToolCall() [hook]
│ ├─► tool.execute()
│ └─► afterToolCall() [hook]
└─► turn_end
├─► shouldStopAfterTurn() [hook]
├─► prepareNextTurn() [hook]
├─► getSteeringMessages() [hook]
└─► getFollowUpMessages() [hook]
```
---
## Phase 4: AgentHarness (45 minutes)
### High-Level API
```typescript
// Create harness
const harness = new AgentHarness({
session: session,
models: models,
tools: [weatherTool, gitTool],
activeToolNames: ["weather", "git"],
model: gpt4Model,
thinkingLevel: "medium"
});
// Main operations
await harness.prompt("What's the weather in London?");
// Queue management
await harness.steer("Wait, check this first"); // Interrupt
await harness.followUp("Now summarize"); // After agent stops
await harness.nextTurn("Also deploy"); // Next turn
// Session management
await harness.compact(); // Compress context
await harness.navigateTree(entryId); // Branch conversation
```
### Session Tree
```
Session = Conversation History as a Tree
root
├─ message [user prompt #1]
│ └─ message [assistant #1]
│ └─ tool_result [result]
│ └─ message [user prompt #2]
│ └─ compaction [summary]
│ ├─ retained: [recent messages]
│ └─ message [assistant continues]
│ └─ leaf [current head]
└─ branch_summary [point where branch created]
└─ message [new branch]
└─ leaf [new head]
```
**Key Operations**:
- `buildContext()` → Get LLM context from tree
- `fork()` → Create branch at point
- `compact()` → Summarize history
---
## Phase 5: Tool Execution (30 minutes)
### Tool Lifecycle
```
1. LLM sends tool call
└─► AssistantMessage with toolCall block
2. prepareToolCall()
├─► Find tool by name
├─► Validate arguments
└─► beforeToolCall() hook
3. executePreparedToolCall()
└─► tool.execute() with onUpdate callback
4. finalizeExecutedToolCall()
└─► afterToolCall() hook
5. Emit events
├─► tool_execution_start
├─► tool_execution_update (streaming)
└─► tool_execution_end
```
### Tool Definition
```typescript
const weatherTool: AgentTool = {
name: "get_weather",
label: "Get Weather",
description: "Get current weather for a city",
parameters: Type.Object({ city: Type.String() }),
execute: async (toolCallId, params, signal, onUpdate) => {
// Check for abort
if (signal?.aborted) throw new Error("Aborted");
// Long operation with streaming
const result = await fetchWeather(params.city);
onUpdate({ content: [{ type: "text", text: "Fetching..." }] });
return {
content: [{ type: "text", text: result }],
details: { city: params.city, temp: result.temp },
usage: { input: 0, output: 0, ... }
};
}
};
```
---
## Phase 6: Session Persistence (30 minutes)
### Entry Types
| Type | Purpose |
|------|---------|
| `message` | User/assistant/toolResult |
| `model_change` | Model switch |
| `thinking_level_change` | Reasoning level |
| `active_tools_change` | Tools change |
| `compaction` | History summary |
| `branch_summary` | Branch point |
| `custom` | App data (not visible to model) |
| `custom_message` | Custom message |
| `label` | User label |
| `leaf` | Current head |
### Context Building
```typescript
// 1. Get path from leaf to root
const pathEntries = await session.getBranch();
// 2. Apply transforms (compaction)
const contextEntries = defaultContextEntryTransform(pathEntries);
// 3. Project entries to messages
const messages = contextEntries.flatMap(sessionEntryToContextMessages);
// 4. Derive state (model, thinking level, active tools)
const state = deriveSessionContextState(pathEntries);
// 5. Return context
return { ...state, messages };
```
---
## Summary
### What to Remember
1. **Two layers**: Agent (core) + AgentHarness (high-level)
2. **Messages everywhere**: Input, output, tools, events
3. **Hooks for customization**: Transform messages, block tools, override results
4. **Session = Tree**: Persistent conversation history with branching
5. **Events for UI**: All state changes emitted as events
6. **Tool lifecycle**: Prepare → Execute → Finalize → Emit
### Next Steps
1. **Read the detailed docs**:
- `01-ARCHITECTURE-OVERVIEW.md` - Big picture
- `02-AGENT-LOOP-DETAILED.md` - Core loop
- `03-HOOK-SYSTEM.md` - Hooks reference
- `04-SESSION-ARCHITECTURE.md` - Session system
- `05-TOOL-EXECUTION.md` - Tool system
- `06-AGENTHARNESS-REFERENCE.md` - API reference
- `07-DATA-FLOW-STATE.md` - Data flow
- `08-LEARNING-PATH.md` - Study guide
- `09-DIAGRAMS.md` - Visual diagrams
2. **Design your Julia implementation**:
- Data types
- Core agent loop
- Hook system
- Session persistence
- Tool execution
3. **Start coding**:
- Implement basic types
- Implement core loop
- Add hooks
- Add session
- Add harness
### Common Patterns
**Context window management**:
```typescript
transformContext: async (messages) => {
if (estimateTokens(messages) > MAX_TOKENS) {
return pruneOldMessages(messages);
}
return messages;
}
```
**Tool permission checks**:
```typescript
beforeToolCall: async ({ toolCall }) => {
if (toolCall.name === "bash" && !await canExecute()) {
return { block: true, reason: "Permission denied" };
}
return undefined;
}
```
**Streaming updates**:
```typescript
execute: async (id, params, signal, onUpdate) => {
for await (const chunk of process()) {
onUpdate({ content: [{ type: "text", text: `Progress: ${chunk}%` }] });
}
return finalResult;
}
```
---
## Quick Reference
### Agent Core (agent.ts, agent-loop.ts)
| Function | Purpose |
|----------|---------|
| `runAgentLoop()` | Start new conversation |
| `runAgentLoopContinue()` | Continue existing |
| `runLoop()` | Main iteration |
| `streamAssistantResponse()` | Stream LLM |
| `executeToolCalls()` | Execute tools |
### AgentHarness API
| Method | Purpose |
|--------|---------|
| `prompt()` | Run conversation |
| `steer()` | Interrupt agent |
| `followUp()` | Queue message |
| `compact()` | Compress context |
| `navigateTree()` | Branch conversation |
### Hook Types
| Hook | Purpose |
|------|---------|
| `convertToLlm` | Convert messages |
| `beforeToolCall` | Block tools |
| `afterToolCall` | Override results |
| `shouldStopAfterTurn` | Request stop |
### Entry Types
| Type | Purpose |
|------|---------|
| `message` | Conversation messages |
| `compaction` | History summary |
| `branch_summary` | Branch point |
---
**You now have the foundation to reimplement the agent in Julia!**
Start with data types and the core loop, then add hooks, session, and harness layers incrementally.
@@ -0,0 +1,596 @@
# Pi Agent Architecture - Complete Summary
## Quick Reference for Julia Reimplementation
---
## 1. Core Architecture (Top-Down)
```
┌─────────────────────────────────────────────────────────────────────┐
│ APPLICATION LAYER │
│ • Agent (Low-level) │
│ • AgentHarness (High-level) │
└─────────────────────────────────────────────────────────────────────┘
┌────────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Agent Core │ │ Session System │ │ Tool Execution │
│ • Async loop │ │ • Tree storage │ │ • Prepare │
│ • Event │ │ • Branching │ │ • Execute │
│ • Message │ │ • Compaction │ │ • Finalize │
│ • Hooks │ │ • Context │ │ • Streaming │
└───────────────┘ └──────────────────┘ └──────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────┐
│ LLM PROVIDER LAYER │
│ • StreamFn (streaming interface) │
│ • Models (LLM catalog) │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 2. Key Components
### Agent Core
**Files**: `src/agent.ts`, `src/agent-loop.ts`, `src/types.ts`
**Responsibilities**:
- State management (messages, tools, isStreaming, pendingToolCalls)
- Event streaming (agent_start, turn_start, message_start, etc.)
- Queue management (steering, follow-up)
- Hook execution (beforeToolCall, afterToolCall, etc.)
**Key Types**:
```typescript
type AgentMessage = Message | CustomAgentMessages
type AgentEvent =
| { type: "agent_start" }
| { type: "agent_end"; messages: AgentMessage[] }
| { type: "turn_start" }
| { type: "turn_end"; message: AgentMessage; toolResults: ToolResultMessage[] }
| { type: "message_start"; message: AgentMessage }
| { type: "message_update"; message: AgentMessage }
| { type: "message_end"; message: AgentMessage }
| { type: "tool_execution_start"; ... }
| { type: "tool_execution_end"; ... }
interface AgentContext {
systemPrompt: string
messages: AgentMessage[]
tools?: AgentTool<any>[]
}
```
### AgentHarness
**Files**: `src/harness/agent-harness.ts`
**Responsibilities**:
- Session persistence (JSONL/Memory)
- Branching (create conversation paths)
- Compaction (summarize history)
- Tool context binding
- Hook system (before_agent_start, tool_call, tool_result, etc.)
- Queue management (steer, followUp, nextTurn)
**Key Types**:
```typescript
interface AgentHarnessEvent<TSkill, TPromptTemplate> =
| { type: "agent_start" } // From core
| { type: "before_agent_start" } // Harness-specific
| { type: "tool_call"; ... }
| { type: "tool_result"; ... }
| { type: "session_before_compact"; ... }
| { type: "session_before_tree"; ... }
// ... more harness events
interface SessionContext {
systemPrompt: string
messages: AgentMessage[]
thinkingLevel: ThinkingLevel
model: { provider: string; modelId: string } | null
activeToolNames: string[] | null
}
```
### Session System
**Files**: `src/harness/session/`
**Responsibilities**:
- Conversation persistence as tree
- Context building from tree
- Branching and forking
- Compaction
- Entry types (message, model_change, compaction, branch_summary, etc.)
**Key Types**:
```typescript
interface SessionTreeEntry {
id: string
parentId: string | null
timestamp: string
type: string // "message", "compaction", "branch_summary", etc.
}
interface CompactionEntry extends SessionTreeEntry {
type: "compaction"
summary: string
firstKeptEntryId?: string
tokensBefore: number
retainedTail?: AgentMessage[]
}
```
### Tool System
**Files**: `src/harness/tools/`
**Responsibilities**:
- Tool definition and execution
- Sequential vs parallel execution
- Streaming updates
- Error handling
- Before/after hooks
**Key Types**:
```typescript
interface AgentTool<TParameters, TDetails> extends Tool<TParameters> {
label: string
execute(
toolCallId: string,
params: Static<TParameters>,
signal?: AbortSignal,
onUpdate?: AgentToolUpdateCallback<TDetails>
): Promise<AgentToolResult<TDetails>>
}
interface AgentToolResult<T> {
content: (TextContent | ImageContent)[]
details: T
usage?: Usage
addedToolNames?: string[]
terminate?: boolean
}
```
---
## 3. Message Flow
```
User Input
├─► Agent.prompt("Hello")
│ └─► normalizePromptInput() → AgentMessage[]
└─► runWithLifecycle()
├─► isStreaming = true
└─► runAgentLoop()
├─► agent_start
├─► turn_start
├─► message_start/end (user)
├─► streamAssistantResponse()
│ ├─► transformContext() [optional]
│ ├─► convertToLlm()
│ └─► streamFn() → LLM
├─► executeToolCalls()
│ ├─► prepareToolCall()
│ │ ├─► Find tool
│ │ ├─► Validate args
│ │ └─► beforeToolCall() [hook]
│ │
│ ├─► executePreparedToolCall()
│ │ └─► tool.execute() with onUpdate
│ │
│ └─► finalizeExecutedToolCall()
│ └─► afterToolCall() [hook]
└─► turn_end
├─► prepareNextTurn() [hook]
├─► shouldStopAfterTurn() [hook]
├─► Drain steering queue
└─► Drain follow-up queue
┌─► Continue? → Repeat
└─► Stop? → agent_end
```
---
## 4. Hook System
| Hook | Layer | When | Can Block? | Use Case |
|------|-------|------|------------|----------|
| `convertToLlm` | Agent | Before LLM | No | Filter messages |
| `transformContext` | Agent | Before LLM | Yes | Prune context |
| `beforeToolCall` | Agent | Before tool | Yes | Permission checks |
| `afterToolCall` | Agent | After tool | Yes | Override results |
| `shouldStopAfterTurn` | Agent | After turn | Yes | Request early stop |
| `prepareNextTurn` | Agent | Before next | Yes | Update config |
| `getSteeringMessages` | Agent | After turn | Yes | Interrupt agent |
| `getFollowUpMessages` | Agent | When stop | Yes | Queue messages |
**Harness Hooks**:
- `before_agent_start` - Modify system prompt
- `context` - Transform context
- `tool_call` - Log/before tool
- `tool_result` - Log/after tool
- `session_before_compact` - Customize compaction
- `session_before_tree` - Customize branching
- `before_provider_request` - Modify stream options
- `before_provider_payload` - Modify LLM payload
---
## 5. Session Tree
```
root (parentId: null)
├─ message [id: 1] ← User prompt
│ └─ message [id: 2] ← Assistant
│ └─ tool_result [id: 3]
│ └─ message [id: 4]
│ └─ compaction [id: 5]
│ ├─ summary: "..."
│ ├─ firstKeptEntryId: msg6.id
│ ├─ tokensBefore: 10000
│ ├─ retainedTail: [msg6, msg7]
│ └─ msg6 [id: 6] ← Retained
│ └─ ... (rest of retained)
│ └─ leaf [id: 8] ← Current head
└─ branch_summary [id: 9] ← Branch point
└─ message [id: 10] ← New branch
└─ leaf [id: 11] ← New head
```
**Key Operations**:
- `getBranch()` → Get entries from leaf to root
- `buildContext()` → Project entries to messages
- `fork()` → Create branch at entry
- `compact()` → Summarize history
---
## 6. Tool Execution Flow
```
1. LLM sends tool call
└─► AssistantMessage with toolCall block
2. prepareToolCall()
├─► Find tool
├─► prepareArguments() [optional]
├─► validateToolArguments()
└─► beforeToolCall() [hook]
├─► block: true → Error
└─► block: undefined → Continue
3. executePreparedToolCall()
└─► tool.execute(toolCallId, params, signal, onUpdate)
├─► onUpdate(partialResult) [streaming]
└─► Return: { content, details, ... }
4. finalizeExecutedToolCall()
└─► afterToolCall() [hook]
├─► Override: content, details, isError, usage, terminate
└─► Use executed result
5. Emit events
├─► tool_execution_start
├─► tool_execution_update [streaming]
└─► tool_execution_end
└─► createToolResultMessage()
└─► Emit: message_start/end (toolResult)
```
---
## 7. Data Types
### Messages
```typescript
interface Message {
role: "user" | "assistant" | "toolResult"
content: (TextContent | ImageContent)[]
api?: string
provider?: string
model?: string
usage?: Usage
stopReason?: StopReason
errorMessage?: string
timestamp: number
}
interface TextContent { type: "text"; text: string }
interface ImageContent { type: "image"; mediaType: string; data: string }
```
### Events
```typescript
type AgentEvent =
| { type: "agent_start" }
| { type: "agent_end"; messages: AgentMessage[] }
| { type: "turn_start" }
| { type: "turn_end"; message: AgentMessage; toolResults: ToolResultMessage[] }
| { type: "message_start"; message: AgentMessage }
| { type: "message_update"; message: AgentMessage; assistantMessageEvent: AssistantMessageEvent }
| { type: "message_end"; message: AgentMessage }
| { type: "tool_execution_start"; toolCallId: string; toolName: string; args: any }
| { type: "tool_execution_update"; toolCallId: string; toolName: string; args: any; partialResult: any }
| { type: "tool_execution_end"; toolCallId: string; toolName: string; result: any; isError: boolean }
```
### Tools
```typescript
interface AgentTool<TParameters, TDetails> extends Tool<TParameters> {
label: string
prepareArguments?: (args: unknown) => Static<TParameters>
execute(
toolCallId: string,
params: Static<TParameters>,
signal?: AbortSignal,
onUpdate?: AgentToolUpdateCallback<TDetails>
): Promise<AgentToolResult<TDetails>>
}
interface AgentToolResult<T> {
content: (TextContent | ImageContent)[]
details: T
usage?: Usage
addedToolNames?: string[]
terminate?: boolean
}
```
---
## 8. State Management
### Agent State
```typescript
interface AgentState {
systemPrompt: string
model: Model<any>
thinkingLevel: ThinkingLevel
tools: AgentTool<any>[]
messages: AgentMessage[]
isStreaming: boolean
streamingMessage?: AgentMessage
pendingToolCalls: Set<string>
errorMessage?: string
}
```
### State Mutations
| Event | State Change |
|-------|-------------|
| `message_start` | `streamingMessage = message` |
| `message_update` | `streamingMessage = message` |
| `message_end` | `messages.push(message)`, `streamingMessage = undefined` |
| `tool_execution_start` | `pendingToolCalls.add(toolCallId)` |
| `tool_execution_end` | `pendingToolCalls.delete(toolCallId)` |
| `turn_end` | `errorMessage = message.errorMessage` (if error) |
| `agent_end` | `streamingMessage = undefined` |
---
## 9. Queue System
### Steering Queue
**Purpose**: Interrupt agent while working
**Mode**: `"all"` or `"one-at-a-time"`
**Flow**: After turn ends → Drain → Inject into context → Next LLM call
### Follow-up Queue
**Purpose**: Queue messages for after agent stops
**Mode**: `"all"` or `"one-at-a-time"`
**Flow**: When agent would stop → Drain → Set as pending → Continue loop
---
## 10. Entry Types
| Type | Purpose |
|------|---------|
| `message` | User/assistant/toolResult messages |
| `model_change` | Model switch (`setModel()`) |
| `thinking_level_change` | Reasoning level (`setThinkingLevel()`) |
| `active_tools_change` | Tools change (`setActiveTools()`) |
| `compaction` | History summary (`compact()`) |
| `branch_summary` | Branch point (branching) |
| `custom` | App data (not visible to model) |
| `custom_message` | Custom message |
| `label` | User-assigned label |
| `leaf` | Current session head |
---
## 11. Common Patterns
### Context Window Management
```typescript
transformContext: async (messages, signal) => {
if (estimateTokens(messages) > MAX_TOKENS) {
return pruneOldestMessages(messages, Math.floor(MAX_TOKENS * 0.3))
}
return messages
}
```
### Tool Permission Checks
```typescript
beforeToolCall: async ({ toolCall, args }, signal) => {
if (toolCall.name === "bash" && signal?.aborted) {
return { block: true, reason: "Operation aborted" }
}
if (toolCall.name === "bash" && !await canExecute(args)) {
return { block: true, reason: "Permission denied" }
}
return undefined
}
```
### Streaming Updates
```typescript
execute: async (id, params, signal, onUpdate) => {
for await (const item of longProcess()) {
if (signal?.aborted) throw new Error("Aborted")
onUpdate({
content: [{ type: "text", text: `Progress: ${item}%` }],
details: { progress: item }
})
}
return finalResult
}
```
### Early Termination
```typescript
shouldStopAfterTurn: async ({ message, toolResults }) => {
// Check if model indicates completion
if (message.content.some(c => c.text?.includes("TASK_COMPLETE"))) {
return true
}
// Stop if all tool calls set terminate
return toolResults.every(r => r.terminate)
}
```
---
## 12. Learning Path
1. **Start with types** - Understand AgentMessage, AgentEvent, AgentTool
2. **Study agent-loop** - See how messages flow through the loop
3. **Read hooks** - Understand customization points
4. **Explore session** - See persistence and tree structure
5. **Study tools** - Understand tool execution
6. **Read harness** - See high-level API
7. **Design in Julia** - Implement step by step
---
## 13. Implementation Checklist
### Phase 1: Data Types (Julia)
- [ ] AgentMessage equivalent
- [ ] AgentEvent types
- [ ] AgentTool interface
- [ ] AgentContext
### Phase 2: Core Agent
- [ ] Agent class with state
- [ ] Event streaming
- [ ] Message queue (steering, follow-up)
### Phase 3: Agent Loop
- [ ] runAgentLoop()
- [ ] streamAssistantResponse()
- [ ] executeToolCalls()
- [ ] Tool preparation and execution
- [ ] Event emission
### Phase 4: Hooks
- [ ] Hook registration
- [ ] Hook execution
- [ ] Return value handling
### Phase 5: Session
- [ ] SessionTreeEntry types
- [ ] Tree structure
- [ ] Context building
- [ ] Persistence
### Phase 6: AgentHarness
- [ ] High-level API
- [ ] Queue management
- [ ] Branching
- [ ] Compaction
---
## 14. Quick Reference Cards
### Agent Core
| Function | Purpose |
|----------|---------|
| `runAgentLoop()` | Start new conversation |
| `runAgentLoopContinue()` | Continue existing |
| `streamAssistantResponse()` | Stream LLM |
| `executeToolCalls()` | Execute tools |
| `prepareToolCall()` | Prepare tool execution |
| `executePreparedToolCall()` | Execute tool |
| `finalizeExecutedToolCall()` | Finalize tool |
### AgentHarness
| Method | Purpose |
|--------|---------|
| `prompt()` | Run conversation |
| `skill()` | Execute skill |
| `promptFromTemplate()` | Run template |
| `steer()` | Interrupt agent |
| `followUp()` | Queue message |
| `nextTurn()` | Queue next turn |
| `compact()` | Compress context |
| `navigateTree()` | Branch conversation |
| `setModel()` | Change model |
| `setThinkingLevel()` | Change reasoning |
| `setTools()` | Set tools |
| `setActiveTools()` | Set active tools |
### Hooks
| Hook | Layer | Purpose |
|------|-------|---------|
| `convertToLlm` | Agent | Convert messages |
| `transformContext` | Agent | Manipulate context |
| `beforeToolCall` | Agent | Block tools |
| `afterToolCall` | Agent | Override results |
| `shouldStopAfterTurn` | Agent | Request stop |
| `prepareNextTurn` | Agent | Update config |
| `getSteeringMessages` | Agent | Interrupt |
| `getFollowUpMessages` | Agent | Queue messages |
### Session
| Method | Purpose |
|--------|---------|
| `buildContext()` | Get LLM context |
| `appendMessage()` | Add message |
| `fork()` | Create branch |
| `compact()` | Compress history |
---
**You now have a complete reference for reimplementing the Pi Agent in Julia!**
Start with the data types, implement the core loop, add hooks, then build up to the harness and session layers.
+270
View File
@@ -0,0 +1,270 @@
# Pi Agent Learning Resources
This folder contains comprehensive learning materials for understanding the Pi Agent architecture.
---
## 📚 Documentation Files
| File | Description | Time |
|------|-------------|------|
| **00-README.md** | This file | 5 min |
| **01-ARCHITECTURE-OVERVIEW.md** | Top-down architecture overview with diagrams | 30 min |
| **02-AGENT-LOOP-DETAILED.md** | Core agent loop implementation details | 45 min |
| **03-HOOK-SYSTEM.md** | Complete hook system reference | 45 min |
| **04-SESSION-ARCHITECTURE.md** | Session persistence and tree structure | 45 min |
| **05-TOOL-EXECUTION.md** | Tool execution mechanics | 45 min |
| **06-AGENTHARNESS-REFERENCE.md** | High-level API reference | 45 min |
| **07-DATA-FLOW-STATE.md** | Data flow and state management | 45 min |
| **08-LEARNING-PATH.md** | Step-by-step learning guide | 2 hrs |
| **09-DIAGRAMS.md** | Visual diagrams and flowcharts | 30 min |
| **10-QUICK-START.md** | Quick start guide for Julia reimplementation | 30 min |
| **11-COMPLETE-SUMMARY.md** | Complete reference summary | 20 min |
---
## 🎯 Learning Paths
### Path 1: Fast Track (4-5 hours)
**Goal**: Understand enough to start implementing
1. **01-ARCHITECTURE-OVERVIEW.md** - Big picture
2. **10-QUICK-START.md** - Quick start guide
3. **02-AGENT-LOOP-DETAILED.md** - Core loop (skim)
4. **11-COMPLETE-SUMMARY.md** - Reference
**Then**: Start implementing in Julia
### Path 2: Thorough (10-12 hours)
**Goal**: Deep understanding before implementation
1. **01-ARCHITECTURE-OVERVIEW.md** - 30 min
2. **02-AGENT-LOOP-DETAILED.md** - 45 min
3. **03-HOOK-SYSTEM.md** - 45 min
4. **04-SESSION-ARCHITECTURE.md** - 45 min
5. **05-TOOL-EXECUTION.md** - 45 min
6. **06-AGENTHARNESS-REFERENCE.md** - 45 min
7. **07-DATA-FLOW-STATE.md** - 45 min
8. **09-DIAGRAMS.md** - Reference throughout
**Then**: Follow **08-LEARNING-PATH.md** for implementation
### Path 3: Comprehensive (15-20 hours)
**Goal**: Master the entire system
1. **01-ARCHITECTURE-OVERVIEW.md** - 30 min
2. **02-AGENT-LOOP-DETAILED.md** - 2 hrs
3. **03-HOOK-SYSTEM.md** - 2 hrs
4. **04-SESSION-ARCHITECTURE.md** - 2 hrs
5. **05-TOOL-EXECUTION.md** - 2 hrs
6. **06-AGENTHARNESS-REFERENCE.md** - 2 hrs
7. **07-DATA-FLOW-STATE.md** - 2 hrs
8. **08-LEARNING-PATH.md** - Follow implementation guide
9. **Read source files** - `src/agent.ts`, `src/agent-loop.ts`, etc.
---
## 🏗️ Architecture Overview
```
┌───────────────────────────────────────────────────────────────────────────────┐
│ APPLICATION LAYER │
│ ┌──────────────┐ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ Agent │ │ AgentHarness │ │ Your Custom App │ │
│ │ (Core) │ │ (High-Level) │ │ │ │
│ └───────┬──────┘ └────────┬─────────┘ └───────────┬──────────────┘ │
└──────────┼─────────────────────┼──────────────────────────┼──────────────────┘
│ │ │
▼ ▼ ▼
┌───────────────────┐ ┌─────────────────────┐ ┌───────────────────────────┐
│ Agent Core │ │ Session System │ │ Tool System │
│ • agent-loop.ts │ │ • session/ │ │ • tools/ │
│ • agent.ts │ │ • compaction/ │ │ • bash.ts │
│ • types.ts │ │ • session.ts │ │ • read.ts │
└─────────┬─────────┘ └──────────┬──────────┘ │ • write.ts │
│ │ │ • edit.ts │
▼ ▼ └──────────┬──────────────┘
┌───────────────────────────────────────────────────────────┼──────────────────┐
│ AGENT CORE LAYER │ │
│ • Async iteration │ │
│ • Event streaming │ │
│ • Hook execution │ │
│ • Tool execution │ │
└────────────────────────────────────────────────────────────┴──────────────────┘
```
---
## 🔑 Key Concepts
### 1. Agent Core
- **Low-level** async iteration
- **Message-based** communication
- **Event-driven** state changes
- **Hook system** for customization
### 2. AgentHarness
- **High-level** API
- **Session persistence** (tree structure)
- **Branching** support
- **Context compaction**
- **Tool context binding**
### 3. Hooks
- **Before/after** tool execution
- **Message transformation**
- **Context manipulation**
- **Queue draining**
### 4. Session Tree
- **Persistent** conversation history
- **Branchable** conversation paths
- **Context building** from tree
- **Compaction** for efficiency
---
## 🎓 How to Use This Guide
### For Top-Down Learning
1. Start with **01-ARCHITECTURE-OVERVIEW.md**
2. Study **09-DIAGRAMS.md** for visual understanding
3. Read **02-AGENT-LOOP-DETAILED.md** for core implementation
4. Explore **03-HOOK-SYSTEM.md** for customization
5. Understand **04-SESSION-ARCHITECTURE.md** for persistence
### For Quick Start
1. Read **10-QUICK-START.md**
2. Use **11-COMPLETE-SUMMARY.md** as reference
3. Implement while referencing other docs
### For Deep Dive
1. Follow the learning path in **08-LEARNING-PATH.md**
2. Read source files alongside documentation
3. Implement incrementally
4. Test each component
---
## 📝 Implementation Checklist
### Phase 1: Data Types (Julia)
- [ ] AgentMessage type
- [ ] AgentEvent types
- [ ] AgentTool interface
- [ ] AgentContext
- [ ] AgentState
### Phase 2: Core Agent
- [ ] Agent class
- [ ] State management
- [ ] Event streaming
- [ ] Queue management
### Phase 3: Agent Loop
- [ ] `runAgentLoop()`
- [ ] `streamAssistantResponse()`
- [ ] `executeToolCalls()`
- [ ] `prepareToolCall()`
- [ ] Event emission
### Phase 4: Hooks
- [ ] Hook registration
- [ ] Hook execution
- [ ] Return value handling
### Phase 5: Session
- [ ] Tree structure
- [ ] Entry types
- [ ] Context building
- [ ] Persistence
### Phase 6: AgentHarness
- [ ] High-level API
- [ ] Queue methods
- [ ] Branching
- [ ] Compaction
---
## 🛠️ Recommended Implementation Order
1. **Data Types** - Define all types in Julia
2. **Core Agent** - Implement Agent class with basic state
3. **Event System** - Implement event streaming
4. **Agent Loop** - Implement the main loop
5. **Tool System** - Implement tool execution
6. **Hooks** - Add hook system
7. **Session** - Implement session persistence
8. **Harness** - Add high-level API
---
## 📚 Related Files
- `packages/agent/src/` - Source files
- `agent.ts` - Agent class
- `agent-loop.ts` - Core loop
- `types.ts` - Type definitions
- `proxy.ts` - Proxy utilities
- `stream-fn.ts` - Default stream function
- `harness/` - Harness implementation
---
## 💡 Tips
### For Julia Implementation
1. **Start simple** - Implement basic types first
2. **Test incrementally** - Test each component
3. **Follow patterns** - Use Julia's type system
4. **Use idioms** - Follow Julia conventions
5. **Refer to docs** - Use this guide as reference
### Common Patterns
- **Event-driven** - Use Julia's event system
- **Immutable data** - Prefer immutable structures
- **Multiple dispatch** - Leverage Julia's dispatch
- **Async/await** - Use Julia's async for streaming
---
## 🎯 Success Criteria
After learning, you should be able to:
✅ Explain the two-layer architecture
✅ Trace a message through the system
✅ Identify when each hook is called
✅ Explain how session persistence works
✅ Describe the tool execution flow
✅ Implement a custom tool
✅ Create a conversation branch
✅ Compress conversation history
---
## 📞 Getting Help
- Read the documentation files
- Check the diagrams for visual understanding
- Follow the learning path for structured learning
- Refer to the complete summary for reference
---
**Happy Learning! 🚀**
Start with **01-ARCHITECTURE-OVERVIEW.md** and **09-DIAGRAMS.md** for the big picture.