singularity-forge/docs/dev/ADR-010-pi-clean-seam-architecture.md

13 KiB

ADR-010: Pi Clean Seam Architecture

Status: Proposed Date: 2026-04-14 Deciders: Tom Boucher PRD: PRD-pi-clean-seam-refactor.md


Context

SF vendors four packages from pi-mono (an open-source coding agent framework) by copying their source directly into /packages/:

Package Role Current version
@sf/pi-agent-core Core agent loop and types 0.57.1
@sf/pi-ai Multi-provider LLM API 0.57.1
@sf/pi-tui Terminal UI framework 0.57.1
@sf/pi-coding-agent Coding agent, tools, extension system 2.74.0

Vendoring was chosen over npm dependencies to allow SF to modify the upstream packages freely. However, over time, SF has written substantial original logic directly inside pi-coding-agent — approximately 79 files including:

  • agent-session.ts (98KB) — the primary SF session orchestrator
  • compaction/ — context window management
  • modes/interactive/, modes/rpc/, modes/print/ — all three run modes
  • cli/ — CLI argument parsing and utilities
  • sdk.ts — the createAgentSession() factory

This SF-authored code is mixed in with upstream pi code inside the same package. The pi packages are currently 10 versions behind upstream (0.57.1 vs 0.67.2), with a breaking API change from v0.65.0 (session_switch/session_fork removal) unresolved. The primary obstacle to applying updates is that there is no reliable way to distinguish SF files from pi files without reading them individually.

Why not move to npm dependencies now?

Pi-mono does publish to npm as @mariozechner/pi-*. Moving to npm dependencies would eliminate vendoring entirely, but it is blocked by:

  1. @sf/native bindings are imported directly inside the vendored pi-tui and pi-coding-agent source — the upstream npm packages do not have these imports
  2. ~50 direct source modification commits to the vendored packages since March 2026 would need to be evaluated individually
  3. The upstream extension API (~25 events) is a subset of SF's extension system (~50+ events) — the delta would need to be re-architected before the move

Moving to npm is a valid Phase 2. This ADR covers Phase 1: establishing a clean seam without changing the vendoring approach.


Decision

Introduce two new workspace packages that own all SF-authored code currently living inside pi-coding-agent. The vendored pi packages become close-to-upstream source copies. SF code depends on pi; pi code does not depend on SF.

New package structure

packages/
  pi-agent-core/          # vendored upstream — no SF modifications
  pi-ai/                  # vendored upstream — no SF modifications
  pi-tui/                 # vendored upstream — no SF modifications
  pi-coding-agent/        # vendored upstream + extension system (pi-typed, stays here)
  sf-agent-core/         # NEW — SF session orchestration layer
  sf-agent-modes/        # NEW — SF run modes and CLI layer

Dependency graph

sf-run (binary)
  └── @sf/agent-modes
        ├── @sf/agent-core
        │     ├── @sf/pi-coding-agent
        │     ├── @sf/pi-agent-core
        │     └── @sf/pi-ai
        └── @sf/pi-coding-agent
              ├── @sf/pi-agent-core
              ├── @sf/pi-ai
              └── @sf/pi-tui

Arrows point in one direction only. No cycles. The vendored pi packages have no knowledge of @sf/agent-core or @sf/agent-modes.


Package Specifications

@sf/agent-core (packages/sf-agent-core/)

Purpose: SF's session orchestration layer. Owns the AgentSession class, compaction, bash execution, system prompt construction, and the createAgentSession() factory that wires everything together.

Public API surface (exported from index.ts):

// Primary factory — the entry point for everything above this layer
export { createAgentSession, CreateAgentSessionOptions, CreateAgentSessionResult } from './sdk.js'

// Session class and types
export { AgentSession, AgentSessionEvent } from './agent-session.js'

// Supporting types consumed by modes and extensions
export { CompactionOrchestrator } from './compaction/index.js'
export { BashExecutor } from './bash-executor.js'
export { SystemPromptBuilder } from './system-prompt.js'
export { LifecycleHooks } from './lifecycle-hooks.js'
export { ArtifactManager } from './artifact-manager.js'
export { BlobStore } from './blob-store.js'

Files migrating in from pi-coding-agent/src/core/:

File Notes
agent-session.ts Core session class — 98KB, primary migration target
sdk.ts createAgentSession() factory
compaction/compaction.ts Context window orchestration
compaction/branch-summarization.ts Summarization on fork
compaction/utils.ts Shared compaction utilities
system-prompt.ts SF system prompt construction
bash-executor.ts Bash runtime with SF integration
fallback-resolver.ts Model fallback strategy
lifecycle-hooks.ts Phase hook system
image-overflow-recovery.ts Context overflow recovery
contextual-tips.ts Help text system
keybindings.ts Keyboard binding manager
artifact-manager.ts Blob artifact storage
blob-store.ts External binary data management
export-html/ Session HTML export

Key dependency note: agent-session.ts imports pi types directly (Agent, AgentEvent, AgentMessage, AgentState, AgentTool, ThinkingLevel from @sf/pi-agent-core; Model, Message from @sf/pi-ai). This is intentional — SF's session layer is pi-typed, not abstracting over pi. This makes the seam a clear seam, not an abstraction.


@sf/agent-modes (packages/sf-agent-modes/)

Purpose: SF's run-mode and CLI layer. Assembles the agent session (from @sf/agent-core) with a specific interface: interactive TUI, headless RPC server, or print output. Contains the main() entry point logic invoked by the sf binary.

Public API surface (exported from index.ts):

export { runInteractiveMode } from './modes/interactive/index.js'
export { runRpcMode, RpcMode } from './modes/rpc/index.js'
export { runPrintMode } from './modes/print/index.js'
export { RpcClient } from './modes/rpc/rpc-client.js'
export { parseArgs, SfArgs } from './cli/args.js'
export { main } from './main.js'

Files migrating in from pi-coding-agent/src/:

Directory/File Notes
modes/interactive/ Full TUI interactive mode (~30 component files)
modes/rpc/ RPC server, client, JSON protocol, remote terminal
modes/print/ Print/headless mode
modes/shared/ Shared mode utilities and UI context setup
cli/args.ts CLI argument parsing
cli/config-selector.ts Config directory selection
cli/session-picker.ts Session picker UI
cli/list-models.ts Model listing
cli/file-processor.ts File input processing
main.ts Entry point logic

pi-coding-agent (what remains)

After the migration, pi-coding-agent contains:

  • Upstream tools (src/core/tools/) — bash, read, edit, write, find, grep, ls, hashline tools
  • Upstream agent infrastructure — auth storage, model registry, upstream session manager
  • Extension system (src/core/extensions/) — loader, runner, types, wrapper

The extension system remains here because it is legitimately pi-typed. Extensions subscribe to pi events (session_start, tool_execution_start, model_select, etc.) and receive pi types in their handlers. Moving the extension system out of pi-coding-agent would require re-expressing those types in SF terms, which is the abstraction-layer work explicitly out of scope for this phase.

Required update to extension loader:

src/core/extensions/loader.ts maintains a STATIC_BUNDLED_MODULES map of packages that extensions can import at runtime. After the migration, @sf/agent-core and @sf/agent-modes must be added to this map so that extensions importing those packages continue to resolve correctly in compiled Bun binaries:

// Before (current)
const STATIC_BUNDLED_MODULES = {
  "@sf/pi-agent-core": _bundledPiAgentCore,
  "@sf/pi-ai": _bundledPiAi,
  "@sf/pi-tui": _bundledPiTui,
  "@sf/pi-coding-agent": _bundledPiCodingAgent,
  // ...
}

// After
const STATIC_BUNDLED_MODULES = {
  "@sf/pi-agent-core": _bundledPiAgentCore,
  "@sf/pi-ai": _bundledPiAi,
  "@sf/pi-tui": _bundledPiTui,
  "@sf/pi-coding-agent": _bundledPiCodingAgent,
  "@sf/agent-core": _bundledSfAgentCore,     // NEW
  "@sf/agent-modes": _bundledSfAgentModes,   // NEW
  // ...
}

How Pi Updates Work After This Change

  1. Download the new pi-mono release for the four vendored packages
  2. Copy the upstream source into packages/pi-agent-core/, pi-ai/, pi-tui/, pi-coding-agent/
    • Do not touch packages/sf-agent-core/ or packages/sf-agent-modes/
  3. Run tsc --noEmit (or the build) across the workspace
  4. Fix type errors in @sf/agent-core and @sf/agent-modes only
  5. If upstream changed the extension event API, fix extension system integration in pi-coding-agent/src/core/extensions/

Steps 2-5 are scoped to known files. No archaeology required.


Known Issues to Fix During Migration

Issue Location Fix
Internal-path import of AgentSessionEvent src/web/bridge-service.ts Import from @sf/agent-core public export
clearQueue() not in typed public API AgentSession Add to public interface in @sf/agent-core/index.ts
buildSessionContext() on SessionManager Used by SF code, not publicly exported Evaluate: re-export from @sf/agent-core or remove dependency
Deprecated session_switch, session_fork, session_directory usage 2+ files in pi-coding-agent Migrate to session_start with reason field (required for v0.65.0 compat) — can be done as part of or after clean seam work

Consequences

Positive

  • Pi updates are scoped: type errors from a pi update surface only in the two new SF packages, not scattered across mixed source
  • The module system enforces the boundary: a pi file importing @sf/agent-core is a compiler error, not a convention violation
  • Phase 2 (moving pi packages to npm) becomes a package.json change rather than a file archaeology project
  • Headless/RPC consumers can depend on @sf/agent-core without pulling in the TUI layer

Negative

  • One-time migration cost: ~79 file moves, import path updates across the codebase, two new package.json files, build script update
  • The virtual module map in extensions/loader.ts grows by two entries and requires matching bundle imports at compile time
  • Maintainers need to understand the new three-layer structure (pi-coding-agentagent-coreagent-modes) when debugging

Neutral

  • End-user install experience (npm install -g sf-run@latest) is unchanged
  • Extension authors see no change — the extension API surface remains in @sf/pi-coding-agent
  • SF packages continue to use pi types directly — no new abstraction layer

Alternatives Considered

Single @sf/agent package

Move everything into one package instead of two. Simpler dependency graph but creates a large package where session logic and TUI logic share a build unit. Rejected because headless/RPC use cases would pull in the TUI unnecessarily, and the two concerns have meaningfully different consumers.

Directory convention within pi-coding-agent (no new packages)

Add a src/sf/ subdirectory inside pi-coding-agent to clearly mark SF files without creating new packages. Fastest to implement but the seam is a convention, not enforced by the module system. A future accidental cross-import would not be caught by the compiler. Rejected because the enforcement value of proper packages is worth the modest extra setup.

Move to npm dependencies now (Phase 2 first)

Take @mariozechner/pi-* from npm and skip vendoring entirely. Blocked by @sf/native imports baked into the vendored source, ~50 direct source modification commits, and the upstream extension API gap. Deferred to Phase 2.


Implementation Notes

The migration should proceed in this order to maintain a working build at each step:

  1. Audit — identify all imports of pi-coding-agent internal paths (non-index) and document them
  2. Create packages — scaffold sf-agent-core and sf-agent-modes with package.json and empty index.ts
  3. Move files in batches — start with leaf files (no downstream dependents within pi-coding-agent), work toward agent-session.ts last
  4. Fix imports incrementally — TypeScript will identify broken imports after each batch
  5. Update extension loader — add new packages to virtual module map
  6. Update build script — insert new packages in dependency order
  7. Verify — full build, existing tests pass, sf --version works

The pi update to v0.67.2 (and the deprecated API migration) can be done as a follow-on once the clean seam is in place, since that work will be dramatically simpler with the new structure.