Pi Agent Harness

repository·main·Indexed 12 days ago

https://github.com/earendil-works/pi

A self-extensible coding agent project featuring a unified multi-provider LLM API (@earendil-works/pi-ai) and a robust agent runtime (@earendil-works/pi-agent-core). It provides tools for state management, tool calling with TypeBox schema validation, steering and follow-up message queues, and support for streaming LLM responses including reasoning and tool-call events.

Tokens
246K
Snippets
604
Records
851
Agent score
96%

What's inside Pi

  1. Overview of the Pi Agent Harness

    main

    Pi is an agent harness project featuring a self-extensible coding agent. It is composed of several core packages designed for agentic workflows, LLM abstraction, and terminal interfaces.

    Key components include:

    • @earendil-works/pi-coding-agent: An interactive CLI for a coding agent.
    • @earendil-works/pi-agent-core: The runtime engine providing tool calling and state management.
    • @earendil-works/pi-ai: A unified API for interacting with multiple LLM providers (e.g., OpenAI, Anthropic, Google).
    • @earendil-works/pi-tui: A terminal UI library featuring differential rendering.
    • @earendil-works/pi-telemetry: Vendor-neutral telemetry contracts and schemas.
  2. Understand the event nesting hierarchy

    main

    The AgentHarness events follow a hierarchical structure representing the lifecycle of a run. Understanding this nesting helps in building complex observers or UI components.

    Hierarchy Model:

    • run_start
      • message_start / message_end / entry_added (Prompt/Queue consumption)
      • turn_start
        • message_start / message_update* / message_end (Assistant stream)
        • entry_added (Response commitment)
        • tool_start / tool_update* / tool_end (Tool execution)
        • message_start / message_end (Tool results)
        • entry_added (Result commitment)
      • turn_end
      • compaction_start ... entry_added ... compaction_end (Automatic checkpointing)
    • run_end
  3. Detect and Handle Context Overflows

    main

    Overflow detection is a heuristic used to identify when a request or response has exceeded the model's context window. The harness uses three sources of information, ranked by reliability:

    1. Adapter-reported: The most reliable method. A provider adapter calculates usage.input + usage.cacheRead > contextWindow and sets stopReason: "error" with a specific context-limit message.
    2. Error-message matching: A fallback that matches provider-returned HTTP error messages against known context-limit patterns.
    3. length below intendedOutputLimit: A harness-side heuristic. If a response is truncated (indicated by length) but the output is below the intendedOutputLimit, it is treated as an overflow.

    Handling Behavior:

    • An overflow-classified response is normalized to error and dropped from the immediate context to prevent infinite loops.
    • First Overflow: Triggers compaction to compress history.
    • Second Overflow: Triggers failure_drain to terminate the run.
    • Tool Calls during Overflow: An overflow classification never produces a tool plan. However, if a genuine length truncation occurs while tool calls are present, the harness executes the plan but appends an isError: true result for each call explaining that truncation may have corrupted the arguments.
  4. Understand the Assistant Generation Lifecycle

    main

    The AgentHarness manages the lifecycle of assistant responses through a state machine that transitions between several key states. When an assistant is triggered (e.g., via a need_assistant checkpoint), it moves through a sequence of transactions involving response entries (R) and usage data (U).

    Key transitions include:

    • ready: The assistant is prepared to make a request.
    • effect_pending: A request has been sent and the harness is awaiting a response or tool calls.
    • tools: The assistant has provided tool calls that need execution.
    • retry_wait: A retryable error occurred, and the harness is waiting before the next attempt.
    • compaction: An overflow (context limit) occurred, and the harness is compressing the history to make room.
    • failure_drain: A terminal error or unrecoverable overflow occurred, and the harness is cleaning up.
    • deferred: The response was suspended (e.g., for polling or manual intervention).
    • checkpoint: The assistant has finished its turn, potentially allowing the run to complete.

    Crucial Invariant: There is never a durable "response without usage" or "response and usage without a decision." All response (R) and usage (U) data are minted together and land in storage as a single atomic unit upon settlement.

  5. Understand subagent output and display modes

    main

    The subagent tool provides different levels of visibility into the execution process:

    Collapsed View (Default)

    Shows a high-level summary:

    • Status icon (✓/✗/⏳) and agent name.
    • The last 5-10 items (tool calls and text).
    • Usage statistics: 3 turns ↑input ↓output RcacheRead WcacheWrite $cost ctx:contextTokens model.

    Expanded View (Ctrl+O)

    Provides full detail:

    • The complete task text.
    • All tool calls with their formatted arguments.
    • The final output rendered as Markdown.
    • Per-task usage statistics (for parallel or chained modes).

    Parallel Mode Streaming

    When running tasks in parallel, the UI shows live status updates (e.g., 2/3 done, 1 running). Each completed task's final output is returned to the parent model, capped at 50 KB per task. If a task fails, failure diagnostics from stderr or error messages are returned.

  6. Theme JSON format and requirements

    main

    A theme is a JSON object. It must include a unique name (no / allowed) and define all 51 required color tokens.

    Structure:

    • $schema: (Optional) URL for editor auto-completion and validation.
    • name: (Required) Unique identifier.
    • vars: (Optional) A dictionary of reusable color values.
    • colors: (Required) A dictionary containing all 51 required tokens.
    • export: (Optional) Controls colors for /export HTML output (e.g., pageBg, cardBg, infoBg).

    Optional Tokens & Fallbacks:

    • thinkingMax $\rightarrow$ thinkingXhigh
    • scrollbarThumb $\rightarrow$ selectedBg
    • searchMatchBg $\rightarrow$ selectedBg
    • searchMatchText $\rightarrow$ text

    Search Match Behavior: Other search matches use searchMatchText on searchMatchBg with an underline. The current match reverses this foreground/background pair and uses bold text.

    {
      "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
      "name": "my-theme",
      "vars": {
        "blue": "#0066cc",
        "gray": 242
      },
      "colors": {
        "accent": "blue",
        "muted": "gray",
        "text": "",
        ...
      }
    }
  7. Configure Context and System Prompt Files

    main

    Pi uses markdown files to load project-specific instructions and system prompts. These files are layered from global to local scopes.

    Context Files (AGENTS.md or CLAUDE.md)

    Pi loads these files to understand project conventions, safety rules, and preferences. They are loaded in this order:

    1. ~/.pi/agent/AGENTS.md (Global)
    2. Parent directories (walking up from the current working directory)
    3. The current directory

    Overrides: If a directory contains AGENTS.override.md, it is loaded instead of AGENTS.md or CLAUDE.md in that specific directory.

    Disabling: Use the --no-context-files or -nc flag to prevent loading.

    System Prompt Files

    To replace the default system prompt, use:

    • .pi/SYSTEM.md for project-specific prompts.
    • ~/.pi/agent/SYSTEM.md for global prompts.

    To append to the default prompt instead of replacing it, use APPEND_SYSTEM.md in either location.

  8. Understand the AgentHarness storage backends

    main

    The AgentHarness implementation supports three storage backends for managing agent sessions. All backends are designed to record the session's storageVersion and pass a common conformance suite.

    • Memory: A high-performance, in-memory backend. It holds only the live state (entries, registers, usage, and tree structure) and does not maintain a log. It is used for transient sessions.
    • JSONL: A file-based backend where the file acts as a replay recipe. Instead of storing the current state, it appends committed writes as JSON objects or arrays. To reconstruct the state, the JSONL lines are replayed into Memory maps. This backend is append-only and requires periodic Snapshot compaction to remove dead bytes (superseded register values).
    • SQLite: A robust, database-per-session backend. It uses relational tables to store entries, registers, and usage ledgers. It provides strong atomicity and is designed for single-writer-per-session concurrency using a writer_lease mechanism.
  9. Enable reasoning/thinking in models

    main

    Many models support reasoning capabilities. You can check support via the model.reasoning property.

    Simplified Interface: Use completeSimple or streamSimple with the reasoning option. Valid levels are 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max'. Note that xhigh and max are model-specific; use getSupportedThinkingLevels(model) to check availability.

    Provider-Specific Options: For full control, use complete or stream and narrow the model type using hasApi() to access provider-specific configuration:

    • OpenAI: reasoningEffort and reasoningSummary.
    • Anthropic: thinkingEnabled and thinkingBudgetTokens.
    • Google Gemini: thinking: { enabled, budgetTokens }.
    import { hasApi } from '@earendil-works/pi-ai';
    
    // Example: Anthropic Thinking
    const anthropicModel = models.getModel('anthropic', 'claude-sonnet-4-5')!;
    if (hasApi(anthropicModel, 'anthropic-messages')) {
      await models.complete(anthropicModel, context, {
        thinkingEnabled: true,
        thinkingBudgetTokens: 8192
      });
    }
  10. Manage session leases with acquireSession() and modes

    main

    Sessions in pi-client are managed via SessionLease objects. You cannot construct leases directly; you must use the client to acquire them.

    Acquisition Modes

    • Exclusive Mode ({ mode: "exclusive" }): Used for lifecycle or mutation coordination. Only one exclusive lease can exist at a time. If an exclusive lease is active, acquireSession({ mode: "exclusive" }) will fail with PiSessionOwnershipError.
    • Shared Mode ({ mode: "shared" }): Used when multiple low-level consumers intentionally share the session. Shared acquisition fails if an exclusive lease currently exists.
    • createSession(): A convenience method that returns an exclusive lease for a newly created session.
    • attachSession(): A convenience method for shared acquisition of an existing session.

    Releasing Leases

    Leases implement AsyncDisposable. You can release a lease using:

    • dispose(): Releases the lease and attempts cleanup. If cleanup fails, it reports a protocol error but relinquishes local ownership.
    • detach(): Releases the lease. If detach() fails, the lease becomes active again for retry.

    Error States

    • PiSessionOwnershipError: Thrown when attempting to acquire an exclusive lease while one is already held.
    • PiDisconnectedError: Thrown when commands are attempted while the client is disconnected.
    • PiSessionDetachedError: Thrown when commands are attempted while a lease is releasing, released, or invalidated.
  11. Understand Providers and Models in @earendil-works/pi-ai

    main

    In @earendil-works/pi-ai, the core abstraction is the relationship between Providers and Models:

    • Provider: The runtime unit that owns a model catalog, handles authentication (API keys, OAuth), and manages stream behavior. Providers use specific API implementations (wire protocols) like anthropic-messages, openai-responses, or openai-completions.
    • Models: A collection that holds multiple providers and routes requests to the specific provider that owns the requested model.

    When querying models, the system uses the provider's identity to determine which API protocol to use for the request.

  12. Understand the AgentHarness lifecycle and durability

    main

    The AgentHarness manages agent operations through a series of atomic transactions (TX[...]) to ensure durability and crash recovery. When a user interacts with a lane, the harness follows a specific sequence of states:

    1. Acceptance: Validates the request, runs before_run hooks, and commits the user message and initial operation state.
    2. Intent: Commits the intent to make a provider request, minting IDs for the expected response and usage rows before the request is sent.
    3. The Request: The provider streams the response. Note: This window is not durable. If the process crashes here, the harness knows a request was sent but the outcome is uncertain.
    4. Settlement: Commits the response entry, usage, and the next state (e.g., identifying pending tool calls).
    5. Tool Calls: Follow an intent $\rightarrow$ effect $\rightarrow$ settlement pattern.
    6. Termination: Once the model stops, a terminal transaction cleans up registers and records the result in lane.lastResult.

    If a crash occurs, the harness uses the last committed transaction to resume. For example, if a tool is marked with replay: "never" and the process crashes during execution, the harness will not re-run the tool on restart; instead, it appends a synthetic "interrupted" error result to maintain conversation coherence.

    // Example of starting a conversation in a lane
    harness.createLane("slack:1719432.0021", at: "0195c8d1-4a2e-7b31-…")
    lane.prompt("what changed in auth last week?")