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 orchestratorcompaction/— context window managementmodes/interactive/,modes/rpc/,modes/print/— all three run modescli/— CLI argument parsing and utilitiessdk.ts— thecreateAgentSession()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:
@sf/nativebindings are imported directly inside the vendored pi-tui and pi-coding-agent source — the upstream npm packages do not have these imports- ~50 direct source modification commits to the vendored packages since March 2026 would need to be evaluated individually
- 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
- Download the new pi-mono release for the four vendored packages
- Copy the upstream source into
packages/pi-agent-core/,pi-ai/,pi-tui/,pi-coding-agent/- Do not touch
packages/sf-agent-core/orpackages/sf-agent-modes/
- Do not touch
- Run
tsc --noEmit(or the build) across the workspace - Fix type errors in
@sf/agent-coreand@sf/agent-modesonly - 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-coreis 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-corewithout pulling in the TUI layer
Negative
- One-time migration cost: ~79 file moves, import path updates across the codebase, two new
package.jsonfiles, build script update - The virtual module map in
extensions/loader.tsgrows by two entries and requires matching bundle imports at compile time - Maintainers need to understand the new three-layer structure (
pi-coding-agent→agent-core→agent-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:
- Audit — identify all imports of
pi-coding-agentinternal paths (non-index) and document them - Create packages — scaffold
sf-agent-coreandsf-agent-modeswithpackage.jsonand emptyindex.ts - Move files in batches — start with leaf files (no downstream dependents within pi-coding-agent), work toward
agent-session.tslast - Fix imports incrementally — TypeScript will identify broken imports after each batch
- Update extension loader — add new packages to virtual module map
- Update build script — insert new packages in dependency order
- Verify — full build, existing tests pass,
sf --versionworks
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.