ππ Agent
Chapter 11 / 21

AgentSession Orchestration

The harness pi actually uses

Event subscription + session persistence + model management in one
No simulation scenario available for this chapter

Deep Dive

~7 min read·7003 chars

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

FileLinesResponsibility
packages/coding-agent/src/core/agent-session.ts3327Central orchestration: events, session, model, compaction
packages/coding-agent/src/core/agent-session-services.ts~400Service injection: tools, extensions, telemetry
packages/coding-agent/src/core/agent-session-runtime.ts~300Runtime state: abort, idle timer
packages/coding-agent/src/core/sdk.ts398createAgentSession* 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 test runs bash directly, saves model tokens
  • compactAndRetry: 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

  1. 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.

  2. 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.

  3. 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

DirectionChapterRelationship
PrerequisiteCh01 Agent LoopAgentSession wraps Agent, drives agentLoop
PrerequisiteCh04 Session TreeJSONL format for event writes
PrerequisiteCh10 CLI ModesAll four modes create via createAgentSession
Follow-upCh03 CompactionIdle/overflow triggers compaction
Follow-upCh13 ExtensionsExtension hooks mount on AgentSession
Follow-upCh16 Interactive TUITUI consumes AgentSession events

Check Your Understanding

  1. If AgentSession inherited Agent (instead of subscribing), what impact would that have on testability?
  2. Why does !bash passthrough bypass the model? What impact does this have on token consumption and response speed?
  3. 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.