Pi Coding Agent

repository·main·Indexed 41 days ago

https://github.com/disler/the-verifier-agent

A minimal terminal coding harness and multi-agent orchestration system featuring a 'Builder-Verifier' pattern for automated code review. Pi provides a terminal UI with tools for reading, writing, editing, and executing bash commands, supporting a wide range of LLM providers via API keys or subscriptions. It includes features for session branching, context compaction, custom prompt templates, agent skills, and a TypeScript SDK for programmatic integration.

Tokens
86K
Snippets
205
Records
274
Agent score
49%

What's inside Pi

  1. Understand the Session File Format

    main

    Sessions are stored as JSONL (JSON Lines) files. Each line is a single JSON object representing a SessionEntryBase.

    Tree Structure

    Entries form a tree structure using id and parentId fields. This allows for in-place branching (e.g., via /fork) without creating new files.

    • The first entry (the SessionHeader) has no parentId.
    • Subsequent entries point to their parent via parentId.
    • The "leaf" represents the current position in the conversation.

    File Location

    Sessions are located at: ~/.pi/agent/sessions/--<path>--/<timestamp>_<uuid>.jsonl

    Note: <path> is the working directory with / replaced by -.

  2. How Skills work in Pi

    main

    Skills are self-contained capability packages that the agent loads on-demand. They provide specialized workflows, setup instructions, helper scripts, and reference documentation for specific tasks.

    Pi uses a progressive disclosure model:

    1. Startup: Pi scans skill locations and extracts names and descriptions.
    2. Context: Available skill names and descriptions are included in the system prompt in XML format.
    3. On-demand Loading: When a task matches a skill's description, the agent uses the read tool to load the full SKILL.md file.
    4. Execution: The agent follows the instructions in the skill, using relative paths to access associated scripts, references, or assets.

    Note: Models do not always load the full skill automatically. You can force the agent to load a skill by using the /skill:name command or via explicit prompting.

  3. Understand the Compaction and Branch Summarization format

    main

    Both compaction and branch summarization use a standardized Markdown structure to preserve context. This format ensures that when the agent's context is compressed, the most critical information—goals, progress, and decisions—is retained for the LLM.

    Summary Structure

    ## Goal
    [What the user is trying to accomplish]
    
    ## Constraints & Preferences
    - [Requirements mentioned by user]
    
    ## Progress
    ### Done
    - [x] [Completed tasks]
    
    ### In Progress
    - [ ] [Current work]
    
    ### Blocked
    - [Issues, if any]
    
    ## Key Decisions
    - **[Decision]**: [Rationale]
    
    ## Next Steps
    1. [What should happen next]
    
    ## Critical Context
    - [Data needed to continue]
    
    <read-files>
    path/to/file1.ts
    path/to/file2.ts
    </read-files>
    
    <modified-files>
    path/to/changed.ts
    </modified-files>
  4. Compact Session Context

    main

    To prevent exhausting context windows in long sessions, Pi uses compaction to summarize older messages while preserving recent ones.

    Compaction Methods

    • Manual: Use the /compact command. You can provide optional custom instructions: /compact <custom instructions>.
    • Automatic: Enabled by default. It triggers on context overflow (recovers and retries) or when approaching the limit (proactive).

    Note: Compaction is lossy. The full history remains in the JSONL file and can be revisited using the /tree command.

  5. Termux Clipboard and Limitations

    main

    Pi uses termux-clipboard-set and termux-clipboard-get for clipboard operations.

    Important Limitations:

    • No image clipboard: The Termux clipboard API only supports text; ctrl+v image pasting will not work.
    • No native binaries: Some optional native dependencies (like the clipboard module) are skipped during installation on Android ARM64.
  6. How Prompt Templates work in Pi

    main

    Prompt templates are Markdown snippets that expand into full prompts within the editor. You invoke a template by typing /name (where name is the filename without the .md extension). Pi provides autocomplete that displays available templates along with their descriptions and argument hints.

    /review                           # Expands review.md
    /component Button                 # Expands with argument
    /component Button "click handler" # Multiple arguments
  7. Manage Pi package dependencies

    main

    When building Pi packages, you must handle dependencies differently depending on whether they are runtime dependencies or other Pi packages.

    Runtime Dependencies

    Standard third-party dependencies should be listed in the dependencies field of package.json. When a user installs your package via npm or git, Pi automatically runs npm install to resolve these.

    Core Pi Packages (Peer Dependencies)

    If your extension or skill imports core Pi libraries, do not bundle them. Instead, list them in peerDependencies with a "*" range:

    • @mariozechner/pi-ai
    • @mariozechner/pi-agent-core
    • @mariozechner/pi-coding-agent
    • @mariozechner/pi-tui
    • typebox

    Other Pi Packages (Bundled Dependencies)

    If your package depends on another Pi package, you must bundle it. Add the dependency to both dependencies and bundledDependencies, then reference its resources via node_modules/ paths in your pi manifest.

    Example:

    {
      "dependencies": {
        "shitty-extensions": "^1.0.1"
      },
      "bundledDependencies": ["shitty-extensions"],
      "pi": {
        "extensions": ["extensions", "node_modules/shitty-extensions/extensions"],
        "skills": ["skills", "node_modules/shitty-extensions/skills"]
      }
    }
  8. Best Practices for TUI Component Development

    main

    When building custom TUI components for the Pi agent, follow these critical rules:

    1. Theme Access: Always use the theme object provided in the ctx.ui.custom callback. Never import a theme directly.
    2. Type Safety: When using DynamicBorder, always explicitly type the color parameter: (s: string) => theme.fg("accent", s).
    3. Rendering Lifecycle: Always call tui.requestRender() inside your handleInput method after any state changes to ensure the UI updates.
    4. Component Interface: Custom components must return an object implementing the three-method interface: { render, invalidate, handleInput }.
    5. Reuse Components: Prioritize using built-in components like SelectList, SettingsList, and BorderedLoader instead of building from scratch.
  9. Understand the Pi Extension Lifecycle

    main

    Pi follows a specific event lifecycle that extensions can hook into. The lifecycle is divided into several phases:

    1. Startup: session_start followed by resources_discover.
    2. User Input: input event (can intercept/transform), followed by skill/template expansion, then before_agent_start.
    3. Agent Loop: agent_start $\rightarrow$ turn loop (turn_start $\rightarrow$ context $\rightarrow$ before_provider_request $\rightarrow$ after_provider_response $\rightarrow$ LLM response $\rightarrow$ tool_execution $\rightarrow$ turn_end) $\rightarrow$ agent_end.
    4. Session Management: Commands like /new, /resume, /fork, or /compact trigger specific lifecycle events like session_before_switch, session_shutdown, and session_start with specific reasons (new, resume, fork).
    5. Shutdown: Triggered by exit commands or signals, leading to session_shutdown.
  10. Understand selection behavior in the Session Tree

    main

    The behavior of the session leaf pointer depends on the type of node you select in the tree:

    User Message or Custom Message

    1. The leaf is set to the parent of the selected node (or null if it is the root).
    2. The message text is placed in the editor for re-submission.
    3. Submitting the edited text creates a new branch.

    Non-User Message (assistant, compaction, etc.)

    1. The leaf is set to the selected node itself.
    2. The editor remains empty.
    3. You continue the conversation from that point.

    Selecting the Root User Message

    If you select the very first message in the session (which has no parent):

    1. The leaf is reset to null (resulting in an empty conversation).
    2. The message text is placed in the editor.
    3. This effectively restarts the session from scratch.
  11. Implement OAuth/SSO Authentication

    main

    You can add OAuth/SSO authentication flows that integrate with the agent's /login command by providing an oauth object to registerProvider.

    The oauth object requires:

    • name: A display name for the provider.
    • login(callbacks): An async function to handle the authentication flow. It uses OAuthLoginCallbacks to support browser-based redirects (onAuth), device code flows (onDeviceCode), or manual input (onPrompt). It must return OAuthCredentials.
    • refreshToken(credentials): An async function to refresh the session, returning new OAuthCredentials.
    • getApiKey(credentials): Returns the access token string from the credentials.
    • modifyModels(models, credentials) (Optional): Allows you to dynamically change model configurations (like baseUrl) based on the user's authenticated state/region.
    import type { OAuthCredentials, OAuthLoginCallbacks } from "@mariozechner/pi-ai";
    
    pi.registerProvider("corporate-ai", {
      baseUrl: "https://ai.corp.com/v1",
      api: "openai-responses",
      models: [...],
      oauth: {
        name: "Corporate AI (SSO)",
    
        async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
          // Example: Device code flow
          callbacks.onDeviceCode({
            userCode: "ABCD-1234",
            verificationUri: "https://sso.corp.com/device"
          });
    
          const code = await callbacks.onPrompt({ message: "Enter SSO code:" });
          const tokens = await exchangeCodeForTokens(code);
    
          return {
            refresh: tokens.refreshToken,
            access: tokens.accessToken,
            expires: Date.now() + tokens.expiresIn * 1000
          };
        },
    
        async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
          const tokens = await refreshAccessToken(credentials.refresh);
          return {
            refresh: tokens.refreshToken ?? credentials.refresh,
            access: tokens.accessToken,
            expires: Date.now() + tokens.expiresIn * 1000
          };
        },
    
        getApiKey(credentials: OAuthCredentials): string {
          return credentials.access;
        }
      }
    });
    
    // Users authenticate via:
    // /login corporate-ai