Feature: Pi Coding Agent Provider Integration
Summary
Add @mariozechner/pi-coding-agent (v0.65.2) as a third AI provider alongside Claude and Codex. Pi is a coding agent harness (like Claude Code SDK and Codex SDK) that wraps 15+ LLM providers — adding it unlocks Google Gemini, Mistral, Groq, xAI, OpenRouter, and any OpenAI-compatible endpoint through a single integration. The core challenge is bridging Pi's callback-based subscribe() event system into Archon's AsyncGenerator<MessageChunk> contract.
User Story
As a user of Archon
I want to use Pi as an AI provider in workflows and conversations
So that I can access models from Google, Mistral, Groq, xAI, OpenRouter, and other providers without individual integrations
Problem Statement
Archon currently supports only two AI providers: Claude (via @anthropic-ai/claude-agent-sdk) and Codex (via @openai/codex-sdk). Users wanting to use other models (Gemini, Mistral, Grok, local models) have no path. Pi coding agent wraps 15+ providers with built-in coding tools (read, write, edit, bash) and a TypeScript SDK suitable for embedding.
Solution Statement
Implement PiProvider as a new IAgentProvider that:
- Creates a
pi-coding-agent AgentSession per sendQuery call
- Bridges Pi's callback-based
subscribe() events to an AsyncGenerator<MessageChunk> using an async queue
- Maps Pi's event types (
text_delta, tool_execution_start/end, thinking_delta, agent_end, turn_end) to Archon's MessageChunk variants
- Uses
SessionManager.inMemory() (no file persistence — session resume not supported in v1)
- Propagates
cwd, model selection, system prompt, and abort signal
Metadata
| Field |
Value |
| Type |
NEW_CAPABILITY |
| Complexity |
HIGH |
| Systems Affected |
@archon/core (clients, config), @archon/workflows (schemas, validation, executor, dag-executor, deps), @archon/server (config schemas), @archon/web (provider selects) |
| Dependencies |
@mariozechner/pi-coding-agent@^0.65.2, @mariozechner/pi-ai@^0.65.2 |
| Estimated Tasks |
14 |
UX Design
Before State
╔═══════════════════════════════════════════════════════════════╗
║ BEFORE STATE ║
╠═══════════════════════════════════════════════════════════════╣
║ ║
║ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ ║
║ │ Workflow │ ──► │ provider: │ ──► │ Claude Code │ ║
║ │ YAML │ │ claude|codex │ │ or Codex SDK │ ║
║ └──────────┘ └──────────────┘ └──────────────┘ ║
║ ║
║ Only 2 providers: Claude (Anthropic) and Codex (OpenAI) ║
║ Want Gemini/Mistral/Groq? Out of luck. ║
║ ║
╚═══════════════════════════════════════════════════════════════╝
After State
╔═══════════════════════════════════════════════════════════════╗
║ AFTER STATE ║
╠═══════════════════════════════════════════════════════════════╣
║ ║
║ ┌──────────┐ ┌────────────────────┐ ║
║ │ Workflow │ ──► │ provider: │ ║
║ │ YAML │ │ claude|codex|pi │ ║
║ └──────────┘ └────────┬───────────┘ ║
║ │ ║
║ ┌───────────────┼───────────────┐ ║
║ ▼ ▼ ▼ ║
║ ┌────────────┐ ┌────────────┐ ┌────────────┐ ║
║ │ Claude SDK │ │ Codex SDK │ │ Pi Agent │ ║
║ │ (Anthropic)│ │ (OpenAI) │ │ (15+ LLMs) │ ║
║ └────────────┘ └────────────┘ └─────┬──────┘ ║
║ │ ║
║ ┌──────────────────┼────────────┐ ║
║ ▼ ▼ ▼ ▼ ▼ ║
║ Gemini Mistral Groq xAI ... ║
║ ║
╚═══════════════════════════════════════════════════════════════╝
Interaction Changes
| Location |
Before |
After |
User Impact |
Workflow YAML provider: |
claude | codex |
claude | codex | pi |
Can use Pi in workflows |
| Web UI BuilderToolbar |
2 provider options |
3 provider options |
Can select Pi in builder |
| Web UI NodeInspector |
2 provider options |
3 provider options |
Can set Pi per-node |
.archon/config.yaml |
assistant: claude | codex |
assistant: claude | codex | pi |
Can set Pi as default |
DEFAULT_AI_ASSISTANT env |
claude | codex |
claude | codex | pi |
Env-level Pi default |
Mandatory Reading
CRITICAL: Implementation agent MUST read these files before starting any task:
| Priority |
File |
Lines |
Why Read This |
| P0 |
packages/core/src/providers/codex.ts |
all |
Primary pattern to MIRROR — simpler than Claude, same harness model |
| P0 |
packages/core/src/types/index.ts |
188-386 |
MessageChunk, TokenUsage, IAgentProvider, AgentRequestOptions — the contract |
| P0 |
packages/core/src/providers/factory.ts |
all |
Registration point — add case 'pi' |
| P1 |
packages/workflows/src/deps.ts |
224-262 |
AgentProviderFactory type union + WorkflowConfig.assistants shape |
| P1 |
packages/workflows/src/model-validation.ts |
all |
isClaudeModel, isModelCompatible — add isPiModel |
| P1 |
packages/workflows/src/dag-executor.ts |
346-608 |
resolveNodeProviderAndModel — add Pi branch |
| P1 |
packages/workflows/src/executor.ts |
278-312 |
Workflow-level provider resolution — widen type |
| P1 |
packages/core/src/config/config-types.ts |
all |
Config type definitions — add Pi everywhere |
| P2 |
packages/core/src/providers/codex.test.ts |
all |
Test pattern to FOLLOW |
| P2 |
packages/workflows/src/schemas/workflow.ts |
29-43 |
workflowBaseSchema.provider enum |
| P2 |
packages/workflows/src/schemas/dag-node.ts |
113-139 |
dagNodeBaseSchema.provider enum |
External Documentation:
| Source |
Section |
Why Needed |
| Pi SDK docs |
createAgentSession, subscribe, tools |
Core embedding API |
| Pi agent-core types |
AgentEvent, AgentTool |
Event types to map |
| Pi AI types |
AssistantMessageEvent, Model, KnownProvider, Usage |
Model and streaming types |
Patterns to Mirror
LAZY_LOGGER:
// SOURCE: packages/core/src/providers/codex.ts:17-21
let cachedLog: ReturnType<typeof createLogger> | undefined;
function getLog(): ReturnType<typeof createLogger> {
if (!cachedLog) cachedLog = createLogger('provider.codex');
return cachedLog;
}
ERROR_CLASSIFICATION:
// SOURCE: packages/core/src/providers/codex.ts:112-121
function classifyCodexError(
errorMessage: string,
stderrOutput: string
): 'rate_limit' | 'auth' | 'model_access' | 'crash' | 'unknown' {
const combined = `${errorMessage} ${stderrOutput}`.toLowerCase();
if (RATE_LIMIT_PATTERNS.some(p => combined.includes(p))) return 'rate_limit';
if (AUTH_PATTERNS.some(p => combined.includes(p))) return 'auth';
if (MODEL_ACCESS_PATTERNS.some(p => combined.includes(p))) return 'model_access';
if (SUBPROCESS_CRASH_PATTERNS.some(p => combined.includes(p))) return 'crash';
return 'unknown';
}
RETRY_LOOP:
// SOURCE: packages/core/src/providers/codex.ts (same pattern as claude.ts)
const MAX_SUBPROCESS_RETRIES = 3;
const RETRY_BASE_DELAY_MS = 2000;
for (let attempt = 0; attempt <= MAX_SUBPROCESS_RETRIES; attempt++) {
try {
// ... execute ...
return;
} catch (error) {
// exponential backoff: RETRY_BASE_DELAY_MS * 2^attempt
}
}
FACTORY_CASE:
// SOURCE: packages/core/src/providers/factory.ts:26-37
case 'codex':
getLog().debug({ provider: 'codex' }, 'provider_selected');
return new CodexProvider();
PROVIDER_ENUM (Zod):
// SOURCE: packages/workflows/src/schemas/workflow.ts:32
provider: z.enum(['claude', 'codex']).optional(),
MODEL_VALIDATION:
// SOURCE: packages/workflows/src/model-validation.ts:1-16
export function isClaudeModel(model: string): boolean { ... }
export function isModelCompatible(provider: 'claude' | 'codex', model?: string): boolean { ... }
CONFIG_ASSISTANTS_SHAPE:
// SOURCE: packages/core/src/config/config-types.ts:196-199
assistants: {
claude: ClaudeProviderDefaults;
codex: ProviderDefaults;
};
DAG_PROVIDER_OPTIONS:
// SOURCE: packages/workflows/src/dag-executor.ts:476-485
if (provider === 'codex') {
options = {
model,
modelReasoningEffort: config.assistants.codex.modelReasoningEffort,
webSearchMode: config.assistants.codex.webSearchMode,
additionalDirectories: config.assistants.codex.additionalDirectories,
};
}
TEST_MOCK_PATTERN:
// SOURCE: packages/core/src/providers/codex.test.ts
mock.module('@openai/codex-sdk', () => ({ Codex: MockCodex }));
import { CodexProvider } from './codex';
Files to Change
| File |
Action |
Justification |
packages/core/src/providers/pi.ts |
CREATE |
PiProvider class implementing IAgentProvider |
packages/core/src/providers/factory.ts |
UPDATE |
Add case 'pi' to switch |
packages/core/src/providers/index.ts |
UPDATE |
Export PiProvider |
packages/core/src/config/config-types.ts |
UPDATE |
Add 'pi' to all provider unions + PiProviderDefaults |
packages/core/src/config/config-loader.ts |
UPDATE |
Add 'pi' to env var check (line 216) + defaults |
packages/workflows/src/model-validation.ts |
UPDATE |
Add isPiModel(), widen isModelCompatible() |
packages/workflows/src/schemas/workflow.ts |
UPDATE |
z.enum(['claude', 'codex', 'pi']) |
packages/workflows/src/schemas/dag-node.ts |
UPDATE |
z.enum(['claude', 'codex', 'pi']) |
packages/workflows/src/deps.ts |
UPDATE |
Widen AgentProviderFactory, add pi to WorkflowConfig.assistants |
packages/workflows/src/executor.ts |
UPDATE |
Widen resolvedProvider type, add Pi model inference |
packages/workflows/src/dag-executor.ts |
UPDATE |
Add Pi branch in resolveNodeProviderAndModel + buildLoopNodeOptions |
packages/workflows/src/loader.ts |
UPDATE |
Add 'pi' to provider literal check (line 272) |
packages/server/src/routes/schemas/config.schemas.ts |
UPDATE |
Add 'pi' to Zod enums and assistants object |
packages/web/src/components/workflows/BuilderToolbar.tsx |
UPDATE |
Add Pi option to provider select |
packages/web/src/components/workflows/NodeInspector.tsx |
UPDATE |
Add Pi option to provider select |
packages/core/src/providers/pi.test.ts |
CREATE |
Unit tests for PiProvider |
packages/core/src/providers/factory.test.ts |
UPDATE |
Add Pi test case, update error string assertion |
NOT Building (Scope Limits)
- Session resume — Pi sessions are file-based (.jsonl). Resume support requires persisting file paths and reopening sessions. Deferred to v2 —
resumeSessionId will be ignored, each turn starts fresh.
- Pi-specific config options — Pi supports thinkingLevel, custom tools, extensions. V1 only exposes
model in config (assistants.pi.model). Advanced options deferred.
- OAuth providers — Pi supports OAuth-based model access (Claude Pro, ChatGPT Plus). Not wiring this in v1 — API keys only.
- Structured output — Pi may support structured output via model-level features. Not mapped to
outputFormat in v1.
- Token cost tracking — Pi's
Usage type includes cost breakdown. Not surfacing in v1 beyond basic input/output token counts.
Step-by-Step Tasks
Task 1: Install Pi dependencies
- ACTION: Add
@mariozechner/pi-coding-agent and @mariozechner/pi-ai to packages/core/package.json
- IMPLEMENT:
bun add @mariozechner/pi-coding-agent@^0.65.2 @mariozechner/pi-ai@^0.65.2 --cwd packages/core
- GOTCHA: Pi pulls
@anthropic-ai/sdk and openai as transitive deps — check for version conflicts with existing deps. Bun workspaces should deduplicate compatible ranges.
- VALIDATE:
bun install succeeds, bun run type-check still passes
Task 2: Define provider type alias
- ACTION: Create a shared
ProviderType literal union to avoid repeating 'claude' | 'codex' | 'pi' in 20+ places
- IMPLEMENT: In
packages/core/src/types/index.ts, add: export type ProviderType = 'claude' | 'codex' | 'pi';
- UPDATE: Refactor
config-types.ts, factory.ts, conversations.ts to use ProviderType where the literal union is currently hardcoded
- NOTE:
@archon/workflows cannot import from @archon/core (circular dep). The deps.ts type union must be updated independently (or use string with runtime validation).
- VALIDATE:
bun run type-check
Task 3: CREATE packages/core/src/providers/pi.ts
-
ACTION: Implement PiProvider class implementing IAgentProvider
-
IMPLEMENT: The core of the integration. Key design decisions:
Callback-to-AsyncGenerator bridge:
// Create an async queue that bridges subscribe() callbacks to yield
interface QueueItem {
value: MessageChunk;
done: false;
} | { done: true }
function createAsyncQueue<T>(): {
push: (item: T) => void;
finish: () => void;
error: (err: Error) => void;
[Symbol.asyncIterator]: () => AsyncIterator<T>;
}
Session lifecycle per sendQuery call:
- Create
AuthStorage, set API key from env vars based on resolved provider
- Get model via
getModel(piProvider, piModelId) — requires mapping Archon's flat model string to Pi's (provider, modelId) tuple
- Create
DefaultResourceLoader with systemPromptOverride if options.systemPrompt is set
- Call
createAgentSession({ cwd, model, tools: createCodingTools(cwd), sessionManager: SessionManager.inMemory(), ... })
- Subscribe to events, map to
MessageChunk, push to async queue
- Call
session.prompt(prompt)
- On
agent_end, push { type: 'result' } with token usage from accumulated turn_end events, then finish the queue
- Yield from the async queue
Event mapping:
| Pi Event |
MessageChunk |
message_update + text_delta |
{ type: 'assistant', content: delta } |
message_update + thinking_delta |
{ type: 'thinking', content: delta } |
tool_execution_start |
{ type: 'tool', toolName, toolInput: args } |
tool_execution_end |
{ type: 'tool_result', toolName, toolOutput } |
message_update + error |
{ type: 'system', content: errorMessage } |
agent_end |
{ type: 'result', tokens } |
Model string mapping:
Pi uses (provider, modelId) tuples (e.g., ('anthropic', 'claude-opus-4-5')). Archon uses flat strings. The PiProvider needs a parser:
- Format:
"pi:<provider>/<model>" e.g., "pi:anthropic/claude-opus-4-5", "pi:google/gemini-2.5-pro", "pi:openai/gpt-5.1"
- If no prefix, treat as a Pi model alias (future: add common aliases)
- Store mapping logic in a
parsePiModel(modelString: string): { provider: KnownProvider, modelId: string } helper
Abort handling:
Wire options.abortSignal → session.abort(). Subscribe to the signal's abort event.
Error classification:
Define classifyPiError() following the same pattern as Codex. Pi throws plain Error from prompt() for auth issues and missing models. LLM failures arrive as events with stopReason: 'error'.
-
MIRROR: packages/core/src/providers/codex.ts — same class structure, lazy logger, retry loop, error classification
-
IMPORTS: createAgentSession, createCodingTools, SessionManager, AuthStorage, ModelRegistry, DefaultResourceLoader from @mariozechner/pi-coding-agent; getModel from @mariozechner/pi-ai
-
GOTCHA: session.prompt() returns Promise<void> and does NOT await agent completion — completion is signaled via the agent_end event through subscribe(). The async generator must not return until agent_end fires.
-
GOTCHA: session.dispose() must be called in a finally block to clean up resources.
-
GOTCHA: Pi's @anthropic-ai/sdk transitive dep may conflict with the project's @anthropic-ai/claude-agent-sdk. Verify at runtime.
-
VALIDATE: bun run type-check
Task 4: UPDATE packages/core/src/providers/factory.ts
- ACTION: Add
case 'pi' to the switch
- IMPLEMENT:
case 'pi':
getLog().debug({ provider: 'pi' }, 'provider_selected');
return new PiProvider();
Update error message: "Supported types: 'claude', 'codex', 'pi'"
- MIRROR:
factory.ts:28-33
- VALIDATE:
bun run type-check
Task 5: UPDATE packages/core/src/providers/index.ts
- ACTION: Export PiProvider
- IMPLEMENT: Add
export { PiProvider } from './pi';
- MIRROR:
index.ts:11-12
- VALIDATE:
bun run type-check
Task 6: UPDATE packages/core/src/config/config-types.ts
- ACTION: Add Pi to all provider unions and assistants objects
- IMPLEMENT:
- Create
PiProviderDefaults interface: { model?: string; } (minimal for v1)
GlobalConfig.defaultAssistant: 'claude' | 'codex' | 'pi' (line 41)
GlobalConfig.assistants: add pi?: PiProviderDefaults (line 48)
RepoConfig.assistant: 'claude' | 'codex' | 'pi' (line 98)
RepoConfig.assistants: add pi?: PiProviderDefaults (line 103)
MergedConfig.assistant: 'claude' | 'codex' | 'pi' (line 195)
MergedConfig.assistants: add pi: PiProviderDefaults (line 196-199)
SafeConfig.assistant: 'claude' | 'codex' | 'pi' (line 251)
SafeConfig.assistants: add pi: Pick<PiProviderDefaults, 'model'> (line 253)
- VALIDATE:
bun run type-check
Task 7: UPDATE packages/core/src/config/config-loader.ts
- ACTION: Add
'pi' to env var validation and config defaults
- IMPLEMENT:
- Line 216:
if (envAssistant === 'claude' || envAssistant === 'codex' || envAssistant === 'pi')
- Add
pi: {} to the defaults merge in getDefaults() under assistants
- Add
pi: {} to the merged config construction
- VALIDATE:
bun run type-check
Task 8: UPDATE packages/workflows/src/model-validation.ts
- ACTION: Add
isPiModel() function and widen isModelCompatible()
- IMPLEMENT:
export function isPiModel(model: string): boolean {
return model.startsWith('pi:');
}
export function isModelCompatible(
provider: 'claude' | 'codex' | 'pi',
model?: string
): boolean {
if (!model) return true;
if (provider === 'claude') return isClaudeModel(model);
if (provider === 'pi') return isPiModel(model) || (!isClaudeModel(model));
// Codex: reject Claude aliases and Pi-prefixed models
return !isClaudeModel(model) && !isPiModel(model);
}
Pi accepts pi:provider/model strings AND generic model strings (not Claude aliases). Codex rejects Pi-prefixed models.
- VALIDATE:
bun run type-check
Task 9: UPDATE Zod schemas (workflow + dag-node)
- ACTION: Add
'pi' to provider enums in both schemas
- IMPLEMENT:
packages/workflows/src/schemas/workflow.ts:32: z.enum(['claude', 'codex', 'pi'])
packages/workflows/src/schemas/dag-node.ts:119: z.enum(['claude', 'codex', 'pi'])
- VALIDATE:
bun run type-check
Task 10: UPDATE packages/workflows/src/deps.ts
- ACTION: Widen
AgentProviderFactory type and add pi to WorkflowConfig.assistants
- IMPLEMENT:
- Line 224:
export type AgentProviderFactory = (provider: 'claude' | 'codex' | 'pi') => IWorkflowAgentProvider;
- Lines 249-261: Add to
WorkflowConfig.assistants:
- Line 236:
assistant: 'claude' | 'codex' | 'pi';
- GOTCHA: The compile-time assertion in
store-adapter.ts:19 (const assertConfigCompat: WorkflowConfig = {} as MergedConfig) will fail until BOTH MergedConfig and WorkflowConfig have the pi field. Do Tasks 6 and 10 together.
- VALIDATE:
bun run type-check
Task 11: UPDATE workflow executor + dag-executor + loader
- ACTION: Add Pi to provider resolution logic
- IMPLEMENT:
executor.ts:281: Widen type to 'claude' | 'codex' | 'pi'. Add model inference:
} else if (workflow.model && isPiModel(workflow.model)) {
resolvedProvider = 'pi';
providerSource = 'inferred from workflow model';
}
Insert BEFORE the else if (workflow.model) codex fallback (line 289).
dag-executor.ts:350-608: Widen all 'claude' | 'codex' types. Add Pi inference in resolveNodeProviderAndModel:
} else if (node.model && isPiModel(node.model)) {
provider = 'pi';
}
Add Pi options branch after the codex branch (line 476):
else if (provider === 'pi') {
options = { model };
}
Add Pi warnings for Claude-only fields (same pattern as Codex warnings at lines 387-472).
loader.ts:272: Add || raw.provider === 'pi'
dag-executor.ts buildLoopNodeOptions: Add Pi branch (same pattern).
- Update ALL
'claude' | 'codex' type annotations in parameters and variables across both files.
- GOTCHA: There are ~15 locations where
'claude' | 'codex' is used as a type annotation in dag-executor.ts. Use search to find all of them.
- VALIDATE:
bun run type-check
Task 12: UPDATE server config schemas + API generated types
- ACTION: Add Pi to server-side Zod schemas
- IMPLEMENT:
packages/server/src/routes/schemas/config.schemas.ts:10: z.enum(['claude', 'codex', 'pi'])
- Same file line 37:
z.enum(['claude', 'codex', 'pi'])
- Add
pi to safeConfigSchema.assistants:
pi: z.object({ model: z.string().optional() }),
- Add
pi to updateAssistantConfigBodySchema:
pi: z.object({ model: z.string() }).optional(),
- POST-STEP: Regenerate frontend types:
bun run dev:server then bun --filter @archon/web generate:types
- VALIDATE:
bun run type-check
Task 13: UPDATE Web UI provider selects
- ACTION: Add Pi option to BuilderToolbar and NodeInspector
- IMPLEMENT:
packages/web/src/components/workflows/BuilderToolbar.tsx:14: Update type to 'claude' | 'codex' | 'pi' | undefined
- Line 21: Same type update for
onProviderChange
- Line 161: Update cast to include
'pi'
- After line 167: Add
<option value="pi">Pi</option>
packages/web/src/components/workflows/NodeInspector.tsx:324: Update cast to include 'pi'
- After line 331: Add
<option value="pi">Pi</option>
- VALIDATE:
bun run type-check && bun run lint
Task 14: CREATE tests + UPDATE factory test
- ACTION: Create
pi.test.ts and update factory.test.ts
- IMPLEMENT:
- Create
packages/core/src/providers/pi.test.ts:
- Mock
@mariozechner/pi-coding-agent and @mariozechner/pi-ai with mock.module()
- Mock
createAgentSession to return a controllable session object
- Mock
subscribe to call the callback with test events
- Mock
prompt to resolve immediately
- Test cases:
- Yields assistant text from text_delta events
- Yields thinking from thinking_delta events
- Yields tool from tool_execution_start
- Yields tool_result from tool_execution_end
- Yields result with token usage from agent_end
- Handles abort signal (calls session.abort())
- Retries on transient errors
- Throws on auth errors (no retry)
- Calls session.dispose() in finally
- Update
packages/core/src/providers/factory.test.ts:
- Add mock for pi module
- Add test:
'creates PiProvider for pi type'
- Update error message assertion:
"Supported types: 'claude', 'codex', 'pi'"
- Ensure
pi.test.ts runs in the same bun test src/providers/ batch (no new test split needed since Pi mocks don't conflict with Claude/Codex mocks — they mock different module paths)
- VALIDATE:
bun run test
Testing Strategy
Unit Tests to Write
| Test File |
Test Cases |
Validates |
packages/core/src/providers/pi.test.ts |
9 cases: text, thinking, tool, tool_result, result, abort, retry, auth error, dispose |
PiProvider event mapping and lifecycle |
packages/core/src/providers/factory.test.ts |
+2 cases: pi creation, updated error msg |
Factory registration |
Edge Cases Checklist
Validation Commands
Level 1: STATIC_ANALYSIS
bun run type-check && bun run lint
EXPECT: Exit 0, no errors or warnings
Level 2: UNIT_TESTS
EXPECT: All tests pass across all packages
Level 3: FULL_SUITE
EXPECT: Type-check + lint + format + tests all pass
Level 4: MANUAL_VALIDATION
- Start dev server:
bun run dev:server
- Open Web UI workflow builder — verify Pi appears in provider dropdowns
- Create a test workflow YAML with
provider: pi
- Validate the workflow via API:
POST /api/workflows/validate
- Verify
GET /api/config returns Pi in the assistants object
- (If Pi API key available) Run a simple workflow with Pi provider end-to-end
Acceptance Criteria
Completion Checklist
Risks and Mitigations
| Risk |
Likelihood |
Impact |
Mitigation |
Pi's @anthropic-ai/sdk transitive dep conflicts with existing @anthropic-ai/claude-agent-sdk |
MEDIUM |
HIGH |
Bun workspace dedup should handle it; if not, check if Pi supports tree-shaking or selective provider loading |
| Pi SDK API changes (pre-1.0) |
MEDIUM |
MEDIUM |
Pin to ^0.65.2; monitor releases. The createAgentSession + subscribe API has been stable since 0.50+ |
| Callback→AsyncGenerator bridge has edge cases (backpressure, error propagation) |
LOW |
MEDIUM |
Use proven pattern with Promise-based queue; test thoroughly with mock events |
| Large dependency footprint (Pi pulls in all provider SDKs) |
HIGH |
LOW |
Accept for now — tree-shaking or conditional imports could be optimized later. Pi's @mariozechner/pi-ai imports all providers unconditionally |
Pi's bash tool runs with detached: true (can orphan processes) |
LOW |
MEDIUM |
Archon already manages this via worktree isolation; Pi sessions are short-lived per sendQuery call |
Notes
Model String Convention
The pi:<provider>/<model> convention is chosen to:
- Avoid ambiguity with Claude and Codex model strings
- Map cleanly to Pi's
getModel(provider, modelId) API
- Be self-documenting in workflow YAML:
model: pi:google/gemini-2.5-pro
Session Resume (Future v2)
Pi supports file-based session persistence. A future version could:
- Store session file paths (not just IDs) in the sessions table
- Use
SessionManager.open(filePath) to resume
- This would give full conversation continuity across turns
Pi's Multi-Provider Value
The key strategic value of Pi integration is that it's a "provider multiplier" — one integration gives access to:
- Google Gemini (1M context, multimodal)
- Mistral models (fast, good for coding)
- Groq/Cerebras (ultra-fast inference)
- xAI Grok
- OpenRouter (hundreds of models)
- Any OpenAI-compatible endpoint (Ollama, vLLM for local models)
- OAuth-based access to Claude Pro, ChatGPT Plus, GitHub Copilot (no API keys)
Feature: Pi Coding Agent Provider Integration
Summary
Add
@mariozechner/pi-coding-agent(v0.65.2) as a third AI provider alongside Claude and Codex. Pi is a coding agent harness (like Claude Code SDK and Codex SDK) that wraps 15+ LLM providers — adding it unlocks Google Gemini, Mistral, Groq, xAI, OpenRouter, and any OpenAI-compatible endpoint through a single integration. The core challenge is bridging Pi's callback-basedsubscribe()event system into Archon'sAsyncGenerator<MessageChunk>contract.User Story
As a user of Archon
I want to use Pi as an AI provider in workflows and conversations
So that I can access models from Google, Mistral, Groq, xAI, OpenRouter, and other providers without individual integrations
Problem Statement
Archon currently supports only two AI providers: Claude (via
@anthropic-ai/claude-agent-sdk) and Codex (via@openai/codex-sdk). Users wanting to use other models (Gemini, Mistral, Grok, local models) have no path. Pi coding agent wraps 15+ providers with built-in coding tools (read, write, edit, bash) and a TypeScript SDK suitable for embedding.Solution Statement
Implement
PiProvideras a newIAgentProviderthat:pi-coding-agentAgentSessionpersendQuerycallsubscribe()events to anAsyncGenerator<MessageChunk>using an async queuetext_delta,tool_execution_start/end,thinking_delta,agent_end,turn_end) to Archon'sMessageChunkvariantsSessionManager.inMemory()(no file persistence — session resume not supported in v1)cwd, model selection, system prompt, and abort signalMetadata
@mariozechner/pi-coding-agent@^0.65.2,@mariozechner/pi-ai@^0.65.2UX Design
Before State
After State
Interaction Changes
provider:claude | codexclaude | codex | pi.archon/config.yamlassistant: claude | codexassistant: claude | codex | piDEFAULT_AI_ASSISTANTenvclaude | codexclaude | codex | piMandatory Reading
CRITICAL: Implementation agent MUST read these files before starting any task:
packages/core/src/providers/codex.tspackages/core/src/types/index.tsMessageChunk,TokenUsage,IAgentProvider,AgentRequestOptions— the contractpackages/core/src/providers/factory.tscase 'pi'packages/workflows/src/deps.tsAgentProviderFactorytype union +WorkflowConfig.assistantsshapepackages/workflows/src/model-validation.tsisClaudeModel,isModelCompatible— addisPiModelpackages/workflows/src/dag-executor.tsresolveNodeProviderAndModel— add Pi branchpackages/workflows/src/executor.tspackages/core/src/config/config-types.tspackages/core/src/providers/codex.test.tspackages/workflows/src/schemas/workflow.tsworkflowBaseSchema.providerenumpackages/workflows/src/schemas/dag-node.tsdagNodeBaseSchema.providerenumExternal Documentation:
Patterns to Mirror
LAZY_LOGGER:
ERROR_CLASSIFICATION:
RETRY_LOOP:
FACTORY_CASE:
PROVIDER_ENUM (Zod):
MODEL_VALIDATION:
CONFIG_ASSISTANTS_SHAPE:
DAG_PROVIDER_OPTIONS:
TEST_MOCK_PATTERN:
Files to Change
packages/core/src/providers/pi.tspackages/core/src/providers/factory.tscase 'pi'to switchpackages/core/src/providers/index.tspackages/core/src/config/config-types.ts'pi'to all provider unions + PiProviderDefaultspackages/core/src/config/config-loader.ts'pi'to env var check (line 216) + defaultspackages/workflows/src/model-validation.tsisPiModel(), widenisModelCompatible()packages/workflows/src/schemas/workflow.tsz.enum(['claude', 'codex', 'pi'])packages/workflows/src/schemas/dag-node.tsz.enum(['claude', 'codex', 'pi'])packages/workflows/src/deps.tsAgentProviderFactory, addpitoWorkflowConfig.assistantspackages/workflows/src/executor.tsresolvedProvidertype, add Pi model inferencepackages/workflows/src/dag-executor.tsresolveNodeProviderAndModel+buildLoopNodeOptionspackages/workflows/src/loader.ts'pi'to provider literal check (line 272)packages/server/src/routes/schemas/config.schemas.ts'pi'to Zod enums and assistants objectpackages/web/src/components/workflows/BuilderToolbar.tsxpackages/web/src/components/workflows/NodeInspector.tsxpackages/core/src/providers/pi.test.tspackages/core/src/providers/factory.test.tsNOT Building (Scope Limits)
resumeSessionIdwill be ignored, each turn starts fresh.modelin config (assistants.pi.model). Advanced options deferred.outputFormatin v1.Usagetype includescostbreakdown. Not surfacing in v1 beyond basicinput/outputtoken counts.Step-by-Step Tasks
Task 1: Install Pi dependencies
@mariozechner/pi-coding-agentand@mariozechner/pi-aitopackages/core/package.jsonbun add @mariozechner/pi-coding-agent@^0.65.2 @mariozechner/pi-ai@^0.65.2 --cwd packages/core@anthropic-ai/sdkandopenaias transitive deps — check for version conflicts with existing deps. Bun workspaces should deduplicate compatible ranges.bun installsucceeds,bun run type-checkstill passesTask 2: Define provider type alias
ProviderTypeliteral union to avoid repeating'claude' | 'codex' | 'pi'in 20+ placespackages/core/src/types/index.ts, add:export type ProviderType = 'claude' | 'codex' | 'pi';config-types.ts,factory.ts,conversations.tsto useProviderTypewhere the literal union is currently hardcoded@archon/workflowscannot import from@archon/core(circular dep). Thedeps.tstype union must be updated independently (or usestringwith runtime validation).bun run type-checkTask 3: CREATE
packages/core/src/providers/pi.tsACTION: Implement
PiProviderclass implementingIAgentProviderIMPLEMENT: The core of the integration. Key design decisions:
Callback-to-AsyncGenerator bridge:
Session lifecycle per sendQuery call:
AuthStorage, set API key from env vars based on resolved providergetModel(piProvider, piModelId)— requires mapping Archon's flat model string to Pi's(provider, modelId)tupleDefaultResourceLoaderwithsystemPromptOverrideifoptions.systemPromptis setcreateAgentSession({ cwd, model, tools: createCodingTools(cwd), sessionManager: SessionManager.inMemory(), ... })MessageChunk, push to async queuesession.prompt(prompt)agent_end, push{ type: 'result' }with token usage from accumulatedturn_endevents, then finish the queueEvent mapping:
message_update+text_delta{ type: 'assistant', content: delta }message_update+thinking_delta{ type: 'thinking', content: delta }tool_execution_start{ type: 'tool', toolName, toolInput: args }tool_execution_end{ type: 'tool_result', toolName, toolOutput }message_update+error{ type: 'system', content: errorMessage }agent_end{ type: 'result', tokens }Model string mapping:
Pi uses
(provider, modelId)tuples (e.g.,('anthropic', 'claude-opus-4-5')). Archon uses flat strings. The PiProvider needs a parser:"pi:<provider>/<model>"e.g.,"pi:anthropic/claude-opus-4-5","pi:google/gemini-2.5-pro","pi:openai/gpt-5.1"parsePiModel(modelString: string): { provider: KnownProvider, modelId: string }helperAbort handling:
Wire
options.abortSignal→session.abort(). Subscribe to the signal'sabortevent.Error classification:
Define
classifyPiError()following the same pattern as Codex. Pi throws plainErrorfromprompt()for auth issues and missing models. LLM failures arrive as events withstopReason: 'error'.MIRROR:
packages/core/src/providers/codex.ts— same class structure, lazy logger, retry loop, error classificationIMPORTS:
createAgentSession,createCodingTools,SessionManager,AuthStorage,ModelRegistry,DefaultResourceLoaderfrom@mariozechner/pi-coding-agent;getModelfrom@mariozechner/pi-aiGOTCHA:
session.prompt()returnsPromise<void>and does NOT await agent completion — completion is signaled via theagent_endevent throughsubscribe(). The async generator must not return untilagent_endfires.GOTCHA:
session.dispose()must be called in a finally block to clean up resources.GOTCHA: Pi's
@anthropic-ai/sdktransitive dep may conflict with the project's@anthropic-ai/claude-agent-sdk. Verify at runtime.VALIDATE:
bun run type-checkTask 4: UPDATE
packages/core/src/providers/factory.tscase 'pi'to the switch"Supported types: 'claude', 'codex', 'pi'"factory.ts:28-33bun run type-checkTask 5: UPDATE
packages/core/src/providers/index.tsexport { PiProvider } from './pi';index.ts:11-12bun run type-checkTask 6: UPDATE
packages/core/src/config/config-types.tsPiProviderDefaultsinterface:{ model?: string; }(minimal for v1)GlobalConfig.defaultAssistant:'claude' | 'codex' | 'pi'(line 41)GlobalConfig.assistants: addpi?: PiProviderDefaults(line 48)RepoConfig.assistant:'claude' | 'codex' | 'pi'(line 98)RepoConfig.assistants: addpi?: PiProviderDefaults(line 103)MergedConfig.assistant:'claude' | 'codex' | 'pi'(line 195)MergedConfig.assistants: addpi: PiProviderDefaults(line 196-199)SafeConfig.assistant:'claude' | 'codex' | 'pi'(line 251)SafeConfig.assistants: addpi: Pick<PiProviderDefaults, 'model'>(line 253)bun run type-checkTask 7: UPDATE
packages/core/src/config/config-loader.ts'pi'to env var validation and config defaultsif (envAssistant === 'claude' || envAssistant === 'codex' || envAssistant === 'pi')pi: {}to the defaults merge ingetDefaults()underassistantspi: {}to the merged config constructionbun run type-checkTask 8: UPDATE
packages/workflows/src/model-validation.tsisPiModel()function and widenisModelCompatible()pi:provider/modelstrings AND generic model strings (not Claude aliases). Codex rejects Pi-prefixed models.bun run type-checkTask 9: UPDATE Zod schemas (workflow + dag-node)
'pi'to provider enums in both schemaspackages/workflows/src/schemas/workflow.ts:32:z.enum(['claude', 'codex', 'pi'])packages/workflows/src/schemas/dag-node.ts:119:z.enum(['claude', 'codex', 'pi'])bun run type-checkTask 10: UPDATE
packages/workflows/src/deps.tsAgentProviderFactorytype and addpitoWorkflowConfig.assistantsexport type AgentProviderFactory = (provider: 'claude' | 'codex' | 'pi') => IWorkflowAgentProvider;WorkflowConfig.assistants:assistant: 'claude' | 'codex' | 'pi';store-adapter.ts:19(const assertConfigCompat: WorkflowConfig = {} as MergedConfig) will fail until BOTHMergedConfigandWorkflowConfighave thepifield. Do Tasks 6 and 10 together.bun run type-checkTask 11: UPDATE workflow executor + dag-executor + loader
executor.ts:281: Widen type to'claude' | 'codex' | 'pi'. Add model inference:else if (workflow.model)codex fallback (line 289).dag-executor.ts:350-608: Widen all'claude' | 'codex'types. Add Pi inference inresolveNodeProviderAndModel:loader.ts:272: Add|| raw.provider === 'pi'dag-executor.tsbuildLoopNodeOptions: Add Pi branch (same pattern).'claude' | 'codex'type annotations in parameters and variables across both files.'claude' | 'codex'is used as a type annotation in dag-executor.ts. Use search to find all of them.bun run type-checkTask 12: UPDATE server config schemas + API generated types
packages/server/src/routes/schemas/config.schemas.ts:10:z.enum(['claude', 'codex', 'pi'])z.enum(['claude', 'codex', 'pi'])pitosafeConfigSchema.assistants:pitoupdateAssistantConfigBodySchema:bun run dev:serverthenbun --filter @archon/web generate:typesbun run type-checkTask 13: UPDATE Web UI provider selects
packages/web/src/components/workflows/BuilderToolbar.tsx:14: Update type to'claude' | 'codex' | 'pi' | undefinedonProviderChange'pi'<option value="pi">Pi</option>packages/web/src/components/workflows/NodeInspector.tsx:324: Update cast to include'pi'<option value="pi">Pi</option>bun run type-check && bun run lintTask 14: CREATE tests + UPDATE factory test
pi.test.tsand updatefactory.test.tspackages/core/src/providers/pi.test.ts:@mariozechner/pi-coding-agentand@mariozechner/pi-aiwithmock.module()createAgentSessionto return a controllable session objectsubscribeto call the callback with test eventspromptto resolve immediatelypackages/core/src/providers/factory.test.ts:'creates PiProvider for pi type'"Supported types: 'claude', 'codex', 'pi'"pi.test.tsruns in the samebun test src/providers/batch (no new test split needed since Pi mocks don't conflict with Claude/Codex mocks — they mock different module paths)bun run testTesting Strategy
Unit Tests to Write
packages/core/src/providers/pi.test.tspackages/core/src/providers/factory.test.tsEdge Cases Checklist
pi:google/gemini-2.5-provs baregemini-2.5-provs invalid formatisError: trueassistantchunk (not buffer)pi:anthropic/opusshould be valid for Pi, invalid for Claude/CodexValidation Commands
Level 1: STATIC_ANALYSIS
bun run type-check && bun run lintEXPECT: Exit 0, no errors or warnings
Level 2: UNIT_TESTS
bun run testEXPECT: All tests pass across all packages
Level 3: FULL_SUITE
EXPECT: Type-check + lint + format + tests all pass
Level 4: MANUAL_VALIDATION
bun run dev:serverprovider: piPOST /api/workflows/validateGET /api/configreturns Pi in the assistants objectAcceptance Criteria
PiProviderimplementsIAgentProviderand yields correctMessageChunkvariantsprovider: piworks in workflow YAML and is validated by Zod schemas.archon/config.yamlacceptsassistant: piandassistants.pi.modelDEFAULT_AI_ASSISTANT=pienv var workspi:google/gemini-2.5-provalid for Pi, rejected for Claude/Codexbun run validatesucceedsCompletion Checklist
Risks and Mitigations
@anthropic-ai/sdktransitive dep conflicts with existing@anthropic-ai/claude-agent-sdk^0.65.2; monitor releases. ThecreateAgentSession+subscribeAPI has been stable since 0.50+@mariozechner/pi-aiimports all providers unconditionallydetached: true(can orphan processes)sendQuerycallNotes
Model String Convention
The
pi:<provider>/<model>convention is chosen to:getModel(provider, modelId)APImodel: pi:google/gemini-2.5-proSession Resume (Future v2)
Pi supports file-based session persistence. A future version could:
SessionManager.open(filePath)to resumePi's Multi-Provider Value
The key strategic value of Pi integration is that it's a "provider multiplier" — one integration gives access to: