AgentSession Orchestration
The harness pi actually uses
“Event subscription + session persistence + model management in one”
Deep Dive
Event subscription + session persistence + model management in one
Learning Objectives
After this chapter you will be able to:
- Explain why AgentSession is the 3,327-line "central orchestration layer"
- Describe the "subscribe equals persist" auto-write mechanism
- Explain how the unified prompt() entry integrates compaction, bash passthrough, and overflow retry
- Understand how fork/clone implements session branching via JSONL
Key Source Files
| File | Lines | Responsibility |
|---|---|---|
packages/coding-agent/src/core/agent-session.ts | 3327 | Central orchestration: events, session, model, compaction |
packages/coding-agent/src/core/agent-session-services.ts | ~400 | Service injection: tools, extensions, telemetry |
packages/coding-agent/src/core/agent-session-runtime.ts | ~300 | Runtime state: abort, idle timer |
packages/coding-agent/src/core/sdk.ts | 398 | createAgentSession* factory functions |
Design Motivation: Why an Orchestration Layer?
Problem: The agent loop (→ Chapter 1) only handles "call model → execute tools → write back." But production also needs:
- Every event written to session files (persistence)
- Auto-compaction on context overflow (→ Chapter 3)
- Runtime model/thinking-level switching
- Session forking and resumption
- A unified mount point for extension hooks
If stuffed into the agent loop: The loop becomes a 5,000-line god object — untestable, irreplaceable.
pi's solution: Keep the agent loop minimal (792 lines); all "services around the loop" are orchestrated by AgentSession.
Analogy: The agent loop is the engine; AgentSession is the whole car — the engine just turns; the car handles fuel, electronics, dashboard, airbags.
Subscribe → Auto-Persist
class AgentSession {
constructor(agent: Agent, sessionManager: SessionManager) {
// The core one-liner: subscribe to all events
agent.subscribe((event) => this.onAgentEvent(event))
}
private onAgentEvent(event: AgentEvent) {
switch (event.type) {
case "text_delta":
this.currentMessage.content += event.text
break
case "tool_use":
this.currentMessage.toolCalls.push(event)
break
case "agent_end":
// Write to JSONL (→ Chapter 4 Session Tree)
this.sessionManager.append(this.currentMessage)
break
}
}
}
Key design decisions:
- Subscription installed once at construction — impossible to "forget to write the session"
- All modes (interactive/print/rpc/json) share this single path
- Write target is JSONL files (→ Chapter 4), append-only, crash-safe
prompt(): The Unified Entry
async prompt(opts: PromptOptions): Promise<void> {
// ① Refresh idle compaction timer
this.idleCompactionTimer.refresh()
// ② Bash passthrough: ! prefix executes directly, bypasses model
if (opts.text.startsWith("!")) {
await this.runBashDirect(opts.text.slice(1))
return
}
// ③ Drive the agent loop
try {
await this.agent.prompt(opts.text)
} catch (err) {
// ④ Overflow → compact then retry the same prompt
if (isContextOverflow(err)) {
await this.compactAndRetry(opts)
return
}
throw err
}
}
Line-by-line highlights:
idleCompactionTimer: Auto-compacts when user is idle ("while you're thinking, let me tidy up context")!prefix: Power-user shortcut —!npm testruns bash directly, saves model tokenscompactAndRetry: Context overflow isn't an error, it's a signal — compact and retry transparently
Model / Thinking / Branch Management
// Runtime model switch
setModel(model: Model) {
this.agent.state.model = model
this.sessionManager.append({ kind: "model_change", to: model.id })
}
// Runtime thinking level switch
setThinkingLevel(level: ThinkingLevel) {
this.agent.state.thinkingLevel = level
this.sessionManager.append({ kind: "thinking_level_change", to: level })
}
// Session fork (→ Chapter 4 Session Tree)
fork(): AgentSession {
const branch = this.sessionManager.fork() // JSONL in-place branch
return new AgentSession(this.agent.clone(), branch)
}
Why write model_change to the session?
- Session resumption needs to know "from which point the model changed"
- Replay (→ Chapter 21 Evals) requires exact reproduction of model config
- Audit: who switched to what model and when
SDK Factory: sdk.ts
// sdk.ts provides programmatic creation
export function createAgentSession(opts: SessionOptions): AgentSession {
const agent = new Agent(buildState(opts))
const session = new AgentSession(agent, new SessionManager(opts.path))
installTools(session, opts.tools) // register tools
installExtensions(session, opts.exts) // load extensions
installTelemetry(session, opts.tele) // attach telemetry
return session
}
Service assembly order matters: Tools before extensions (extensions may override tools), extensions before telemetry (telemetry must observe extension behavior).
Engineering Insights
-
3,327 lines (~111KB): The largest single file in coding-agent. It's large because it's "glue" — binding agent loop, session, compaction, model, extensions, tools together. Large ≠ complex — mostly switch/case dispatch and error handling.
-
Subscribe, not inherit: AgentSession doesn't extend Agent; it subscribes. This lets Agent be independently tested, mocked, replaced. A textbook case of composition over inheritance.
-
Idle compaction timing: Triggers when user is thinking (30s no input) — doesn't consume interaction time. Waiting for overflow would cause noticeable stutter.
Cross-Chapter Links
| Direction | Chapter | Relationship |
|---|---|---|
| Prerequisite | Ch01 Agent Loop | AgentSession wraps Agent, drives agentLoop |
| Prerequisite | Ch04 Session Tree | JSONL format for event writes |
| Prerequisite | Ch10 CLI Modes | All four modes create via createAgentSession |
| Follow-up | Ch03 Compaction | Idle/overflow triggers compaction |
| Follow-up | Ch13 Extensions | Extension hooks mount on AgentSession |
| Follow-up | Ch16 Interactive TUI | TUI consumes AgentSession events |
Check Your Understanding
- If AgentSession inherited Agent (instead of subscribing), what impact would that have on testability?
- Why does
!bashpassthrough bypass the model? What impact does this have on token consumption and response speed? - Why is writing model_change to the session necessary for "session replay"?
Takeaways
- 3,327-line central orchestration — agent loop is the engine, AgentSession is the car
- Subscribe equals persist — installed at construction, impossible to miss
- prompt() unified entry: idle compaction + bash passthrough + overflow retry
- Composition over inheritance: subscribe, not extends
This chapter is based on the main-branch source of earendil-works/pi.