11 KiB
11 KiB
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
// 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
// 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 treefork()→ Create branch at pointcompact()→ 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
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
// 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
- Two layers: Agent (core) + AgentHarness (high-level)
- Messages everywhere: Input, output, tools, events
- Hooks for customization: Transform messages, block tools, override results
- Session = Tree: Persistent conversation history with branching
- Events for UI: All state changes emitted as events
- Tool lifecycle: Prepare → Execute → Finalize → Emit
Next Steps
-
Read the detailed docs:
01-ARCHITECTURE-OVERVIEW.md- Big picture02-AGENT-LOOP-DETAILED.md- Core loop03-HOOK-SYSTEM.md- Hooks reference04-SESSION-ARCHITECTURE.md- Session system05-TOOL-EXECUTION.md- Tool system06-AGENTHARNESS-REFERENCE.md- API reference07-DATA-FLOW-STATE.md- Data flow08-LEARNING-PATH.md- Study guide09-DIAGRAMS.md- Visual diagrams
-
Design your Julia implementation:
- Data types
- Core agent loop
- Hook system
- Session persistence
- Tool execution
-
Start coding:
- Implement basic types
- Implement core loop
- Add hooks
- Add session
- Add harness
Common Patterns
Context window management:
transformContext: async (messages) => {
if (estimateTokens(messages) > MAX_TOKENS) {
return pruneOldMessages(messages);
}
return messages;
}
Tool permission checks:
beforeToolCall: async ({ toolCall }) => {
if (toolCall.name === "bash" && !await canExecute()) {
return { block: true, reason: "Permission denied" };
}
return undefined;
}
Streaming updates:
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.