Files
MosaicIQ/docs/pi-sdk-migration-plan.md
francy51 0624026af3 Migrate to Pi Coding Agent SDK for multi-provider LLM support
Replace the custom Pi API fetch client with the @earendil-works/pi-coding-agent
SDK, enabling support for multiple LLM providers (Anthropic, OpenAI, DeepSeek,
Google Gemini, xAI/ZAI) while maintaining backward compatibility.

Key changes:
- Add piSdk.ts with SDK session management and provider auto-detection
- Refactor client.ts to delegate to SDK adapter, keeping public API surface
- Update documentation to reflect multi-provider environment variables
- Add RPC contracts for LLM model selection and provider configuration
- Update agent runner to support provider-specific tools and parameters

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 00:17:26 -04:00

844 lines
27 KiB
Markdown

# Pi SDK Migration Plan
## Overview
Replace the hand-rolled `packages/shared/src/llm/client.ts` HTTP client (which called the Pi/Inflection AI API at `api.pi.ai`) with the **Pi Coding Agent SDK** (`@earendil-works/pi-coding-agent`). This removes the dependency on the Pi/Inflection AI cloud service entirely and lets MosaicIQ use any LLM provider. Benefits:
- Type-safe LLM interaction via `AgentSession`
- Built-in streaming, retry, and compaction
- Custom tools with direct access to our SQLite database
- **Multi-provider support** (Anthropic, OpenAI, DeepSeek, Gemini, etc.)
- Structured output with proper schemas
- Session management (persisted or in-memory)
> **Note:** "Pi" in "Pi Coding Agent SDK" is the name of the coding agent framework (the tool we use to build agents). It is NOT the same as the Pi/Inflection AI chatbot API (`api.pi.ai`). The SDK supports many LLM providers — we are removing the Inflection AI dependency entirely.
The migration is structured as **7 phases**, each independently shippable. No phase should break existing functionality.
---
## Phase 1: Install SDK & Create Adapter Layer
**Goal:** Install the package and build a thin adapter that wraps the SDK, keeping the existing `llm/` API surface working.
### Files to create/modify
| Action | File | Description |
|--------|------|-------------|
| Install | `packages/shared/package.json` | Add `@earendil-works/pi-coding-agent` dependency |
| Create | `packages/shared/src/llm/piSdk.ts` | SDK initialization, session factory, auth setup |
| Modify | `packages/shared/src/llm/client.ts` | Replace raw `fetch` calls with SDK-backed implementation |
### `packages/shared/src/llm/piSdk.ts` — SDK singleton
```typescript
import {
AuthStorage,
createAgentSession,
ModelRegistry,
SessionManager,
SettingsManager,
DefaultResourceLoader,
} from "@earendil-works/pi-coding-agent";
// Lazy-initialized SDK state
let authStorage: AuthStorage;
let modelRegistry: ModelRegistry;
export function getAuthStorage(): AuthStorage {
if (!authStorage) {
authStorage = AuthStorage.create();
}
return authStorage;
}
export function getModelRegistry(): ModelRegistry {
if (!modelRegistry) {
modelRegistry = ModelRegistry.create(getAuthStorage());
}
return modelRegistry;
}
export async function createSession(options?: {
systemPrompt?: string;
customTools?: any[];
onEvent?: (event: any) => void;
}) {
const session = await createAgentSession({
sessionManager: SessionManager.inMemory(),
authStorage: getAuthStorage(),
modelRegistry: getModelRegistry(),
settingsManager: SettingsManager.inMemory({
compaction: { enabled: false }, // We manage context ourselves
retry: { enabled: true, maxRetries: 3 },
}),
// No built-in tools for research agents — only custom tools
tools: [],
customTools: options?.customTools ?? [],
});
if (options?.onEvent) {
session.subscribe(options.onEvent);
}
return session;
}
export function isConfigured(): boolean {
// Check if any supported provider has an API key configured
return !!(
process.env.ANTHROPIC_API_KEY ||
process.env.OPENAI_API_KEY ||
process.env.DEEPSEEK_API_KEY ||
process.env.GOOGLE_API_KEY
);
}
```
### `packages/shared/src/llm/client.ts` — Updated to use SDK
```typescript
// Keep the same exported API surface (streamResponse, complete, etc.)
// but delegate to SDK sessions internally
export async function complete(
prompt: string,
options: CompletionOptions = {}
): Promise<string> {
const session = await createSession({
systemPrompt: options.systemPrompt,
});
let result = "";
session.subscribe((event) => {
if (event.type === "message_update" &&
event.assistantMessageEvent.type === "text_delta") {
result += event.assistantMessageEvent.delta;
}
});
await session.prompt(prompt);
return result;
}
export async function streamResponse(
prompt: string,
options: StreamOptions & CompletionOptions = {}
): Promise<string> {
const session = await createSession();
let fullResponse = "";
session.subscribe((event) => {
if (event.type === "message_update" &&
event.assistantMessageEvent.type === "text_delta") {
const delta = event.assistantMessageEvent.delta;
fullResponse += delta;
options.onProgress?.(delta);
}
});
await session.prompt(prompt);
return fullResponse;
}
```
### Acceptance criteria
- [ ] `pnpm install` succeeds with new SDK dependency
- [ ] Existing `agent.chat`, `agent.start`, `validation.run` RPC methods still work
- [ ] `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` are picked up from env
- [ ] No references to `PI_API_KEY` or `api.pi.ai` remain
- [ ] Streaming still works end-to-end (RPC → event emitter → renderer)
---
## Phase 2: Custom Tools for Database Access
**Goal:** Build Pi SDK custom tools that give the LLM direct access to MosaicIQ's SQLite data. This replaces the "build context → embed in prompt" pattern with tool-based access.
### Files to create
| Action | File | Description |
|--------|------|-------------|
| Create | `packages/shared/src/tools/index.ts` | Tool barrel export |
| Create | `packages/shared/src/tools/companyTools.ts` | Company, filings, earnings tools |
| Create | `packages/shared/src/tools/modelTools.ts` | Financial model read/write tools |
| Create | `packages/shared/src/tools/memoTools.ts` | Memo read/write tools |
| Create | `packages/shared/src/tools/validationTools.ts` | Validation status tools |
### `packages/shared/src/tools/companyTools.ts` — Example
```typescript
import { Type } from "typebox";
import { defineTool } from "@earendil-works/pi-coding-agent";
import type { Db } from "../db/database.js";
import {
getCompany,
listFilings,
getEarningsSchedule,
} from "../db/queries.js";
export function createCompanyTools(db: Db) {
const getCompanyInfo = defineTool({
name: "get_company_info",
label: "Get Company Info",
description: "Get company details including name, ticker, sector, and price",
parameters: Type.Object({
companyId: Type.String({ description: "Company ID or ticker" }),
}),
execute: async (_, params) => {
const company = getCompany(db, params.companyId);
if (!company) {
return {
content: [{ type: "text", text: `Company "${params.companyId}" not found.` }],
isError: true,
details: {},
};
}
return {
content: [{ type: "text", text: JSON.stringify(company, null, 2) }],
details: {},
};
},
});
const getFilings = defineTool({
name: "get_filings",
label: "Get SEC Filings",
description: "List SEC filings for a company (10-K, 10-Q, 8-K, etc.)",
parameters: Type.Object({
companyId: Type.String({ description: "Company ID" }),
limit: Type.Optional(Type.Number({ description: "Max filings to return", default: 20 })),
}),
execute: async (_, params) => {
const filings = listFilings(db, params.companyId, params.limit ?? 20);
return {
content: [{ type: "text", text: JSON.stringify(filings, null, 2) }],
details: {},
};
},
});
const getEarnings = defineTool({
name: "get_earnings",
label: "Get Earnings Schedule",
description: "Get upcoming and past earnings dates and results",
parameters: Type.Object({
companyId: Type.String({ description: "Company ID" }),
}),
execute: async (_, params) => {
const schedule = getEarningsSchedule(db, params.companyId);
return {
content: [{ type: "text", text: JSON.stringify(schedule, null, 2) }],
details: {},
};
},
});
return [getCompanyInfo, getFilings, getEarnings];
}
```
### `packages/shared/src/tools/modelTools.ts`
```typescript
export function createModelTools(db: Db) {
const getModelData = defineTool({
name: "get_financial_model",
label: "Get Financial Model",
description: "Get the financial model for a company (income, balance, operating tabs)",
parameters: Type.Object({
companyId: Type.String(),
tab: Type.Union([Type.Literal("income"), Type.Literal("balance"), Type.Literal("operating")]),
}),
execute: async (_, params) => {
const model = getModel(db, params.companyId, params.tab);
return {
content: [{ type: "text", text: JSON.stringify(model, null, 2) }],
details: {},
};
},
});
const updateModelCell = defineTool({
name: "update_model_cell",
label: "Update Model Cell",
description: "Update a single cell in the financial model",
parameters: Type.Object({
companyId: Type.String(),
tab: Type.String(),
row: Type.Number(),
col: Type.Number(),
value: Type.String(),
}),
execute: async (_, params) => {
const result = updateModelCell(db, params.companyId, params.tab, params.row, params.col, params.value);
return {
content: [{ type: "text", text: `Updated cell [${params.row}, ${params.col}] = ${params.value}` }],
details: result,
};
},
});
return [getModelData, updateModelCell];
}
```
### `packages/shared/src/tools/memoTools.ts`
```typescript
export function createMemoTools(db: Db) {
const getMemo = defineTool({
name: "get_memo",
label: "Get Investment Memo",
description: "Get the full investment memo with all sections, annotations, and review status",
parameters: Type.Object({
companyId: Type.String(),
}),
execute: async (_, params) => {
const memo = getMemo(db, params.companyId);
return {
content: [{ type: "text", text: JSON.stringify(memo, null, 2) }],
details: {},
};
},
});
const updateMemoSection = defineTool({
name: "update_memo_section",
label: "Update Memo Section",
description: "Update a specific section of the investment memo",
parameters: Type.Object({
companyId: Type.String(),
sectionId: Type.String(),
content: Type.String({ description: "New section content (markdown)" }),
}),
execute: async (_, params) => {
const section = updateMemoSection(db, params.companyId, params.sectionId, { content: params.content });
return {
content: [{ type: "text", text: `Updated section "${params.sectionId}"` }],
details: { section },
};
},
});
return [getMemo, updateMemoSection];
}
```
### `packages/shared/src/tools/index.ts`
```typescript
import type { Db } from "../db/database.js";
import { createCompanyTools } from "./companyTools.js";
import { createModelTools } from "./modelTools.js";
import { createMemoTools } from "./memoTools.js";
import { createValidationTools } from "./validationTools.js";
export function createAllTools(db: Db) {
return [
...createCompanyTools(db),
...createModelTools(db),
...createMemoTools(db),
...createValidationTools(db),
];
}
export { createCompanyTools, createModelTools, createMemoTools, createValidationTools };
```
### Acceptance criteria
- [ ] All tools have TypeBox schemas (parameters validated by SDK)
- [ ] Each tool has a clear description for the LLM
- [ ] Tools can access the DB (test with a simple session.prompt())
- [ ] Error cases return `isError: true`
---
## Phase 3: Migrate Agent Runner to SDK Sessions
**Goal:** Rewrite `runner.ts` and `validationAgents.ts` to use SDK sessions with custom tools instead of the "build context → embed in prompt" pattern.
### Files to modify
| Action | File | Description |
|--------|------|-------------|
| Modify | `packages/shared/src/agents/runner.ts` | Use `createSession()` with custom tools |
| Modify | `packages/shared/src/agents/validationAgents.ts` | Use SDK sessions |
| Modify | `packages/shared/src/rpc/agentRpc.ts` | Pass streaming events through |
### New `runner.ts` approach
**Before (current):**
1. Build huge context object from DB
2. Interpolate context into a giant prompt string
3. Send prompt to LLM
4. Parse JSON from response
**After (SDK):**
1. Create SDK session with company-specific custom tools
2. Send a focused prompt (the agent's instructions + user request)
3. LLM calls tools as needed to get data
4. LLM returns structured response
```typescript
// New runner.ts (simplified)
export async function executeAgent(
db: Db,
agentId: string,
companyId: string,
options: AgentRunOptions = {}
): Promise<AgentRunResult> {
const tools = createAllTools(db);
const metadata = getAgentMetadata(agentId);
const systemPrompt = getAgentSystemPrompt(agentId); // Just the role, not the data
const session = await createSession({
systemPrompt,
customTools: tools,
onEvent: (event) => {
// Forward SDK events to our EventEmitter
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
emitAgentStreaming(runId, agentId, companyId, event.assistantMessageEvent.delta);
}
if (event.type === "tool_execution_start") {
emitAgentProgress(runId, agentId, companyId, 50, `Running tool: ${event.toolName}`);
}
},
});
// Focused prompt — the LLM will use tools to gather data
const prompt = `Analyze company ${companyId} using your available tools. ${options.userMessage ?? ""}`;
await session.prompt(prompt);
// Get the final assistant message
const messages = session.agent.state.messages;
const lastAssistant = messages.filter(m => m.role === "assistant").pop();
// ... store results, emit events, etc.
}
```
### Key insight: Agent prompts change
The current `prompts.ts` embeds all data in the prompt. After migration, prompts become **instructions only** — the LLM fetches data via tools:
```typescript
// BEFORE (current): Giant prompt with all data baked in
const prompt = `You are the SEC Filings Agent for ${ctx.company.name}.
Available Filings:
${ctx.filings.map(f => `- ${f.formType}: ${f.title}`).join("\n")}
...hundreds of lines of embedded data...
`;
// AFTER: Focused system prompt, tools provide data
const systemPrompt = `You are the SEC Filings Agent.
Use the get_filings and get_company_info tools to gather data.
Extract segment revenue, identify risk factors, and note accounting changes.
Cite specific sections when making claims.
Respond with structured JSON.`;
// The LLM calls tools as needed during generation
```
### Acceptance criteria
- [ ] `agent.start` RPC works end-to-end with SDK
- [ ] `agent.chat` RPC works with SDK (streaming response)
- [ ] Tool calls appear in agent progress events
- [ ] Validation agents (sv, qa, rt) still return `ValidationResult`
- [ ] Pipeline executor still works
---
## Phase 4: Excel/File Custom Tools
**Goal:** Add custom tools for reading and writing Excel, CSV, and PDF files. The Pi SDK's built-in `read` tool only handles text and images.
### Files to create
| Action | File | Description |
|--------|------|-------------|
| Create | `packages/shared/src/tools/fileTools.ts` | Excel read/write, CSV, PDF tools |
| Modify | `packages/shared/src/tools/index.ts` | Export new tools |
### `packages/shared/src/tools/fileTools.ts`
```typescript
import ExcelJS from "exceljs";
import { Type } from "typebox";
import { defineTool } from "@earendil-works/pi-coding-agent";
export function createFileTools() {
const readExcel = defineTool({
name: "read_excel",
label: "Read Excel File",
description: "Read an Excel (.xlsx) file and return sheet data as JSON. Supports sheet selection and row ranges.",
parameters: Type.Object({
path: Type.String({ description: "Path to .xlsx file" }),
sheetName: Type.Optional(Type.String({ description: "Sheet name (default: first sheet)" })),
maxRows: Type.Optional(Type.Number({ description: "Max rows to return (default: 100)" })),
}),
execute: async (_, params) => {
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.readFile(params.path);
const sheet = params.sheetName
? workbook.getWorksheet(params.sheetName)
: workbook.worksheets[0];
if (!sheet) {
return {
content: [{ type: "text", text: `Sheet not found: ${params.sheetName ?? "(first sheet)"}` }],
isError: true,
details: {},
};
}
const rows: unknown[] = [];
const headers: string[] = [];
const limit = params.maxRows ?? 100;
sheet.eachRow((row, rowNumber) => {
if (rowNumber === 1) {
row.eachCell((cell) => headers.push(String(cell.value ?? "")));
} else if (rowNumber <= limit + 1) {
const values: Record<string, unknown> = {};
row.eachCell((cell, colNumber) => {
values[headers[colNumber - 1] ?? `col_${colNumber}`] = cell.value;
});
rows.push(values);
}
});
return {
content: [{
type: "text",
text: JSON.stringify({ sheetName: sheet.name, headers, rowCount: rows.length, rows }, null, 2),
}],
details: {},
};
},
});
const writeExcel = defineTool({
name: "write_excel",
label: "Write Excel File",
description: "Write structured data to a new Excel (.xlsx) file",
parameters: Type.Object({
path: Type.String({ description: "Output file path" }),
sheetName: Type.String({ description: "Sheet name", default: "Sheet1" }),
data: Type.Array(Type.Record(Type.String(), Type.Unknown()), {
description: "Array of row objects",
}),
}),
execute: async (_, params) => {
const workbook = new ExcelJS.Workbook();
const sheet = workbook.addWorksheet(params.sheetName);
if (params.data.length > 0) {
const headers = Object.keys(params.data[0]);
sheet.addRow(headers);
for (const row of params.data) {
sheet.addRow(headers.map(h => row[h] ?? ""));
}
}
await workbook.xlsx.writeFile(params.path);
return {
content: [{ type: "text", text: `Wrote ${params.data.length} rows to ${params.path}` }],
details: {},
};
},
});
const readCsv = defineTool({
name: "read_csv",
label: "Read CSV File",
description: "Parse a CSV file and return rows as JSON",
parameters: Type.Object({
path: Type.String({ description: "Path to CSV file" }),
delimiter: Type.Optional(Type.String({ description: "Delimiter (default: comma)", default: "," })),
maxRows: Type.Optional(Type.Number({ description: "Max rows (default: 500)" })),
}),
execute: async (_, params) => {
const fs = await import("fs/promises");
const content = await fs.readFile(params.path, "utf-8");
const delimiter = params.delimiter ?? ",";
const lines = content.trim().split("\n");
const headers = lines[0].split(delimiter).map(h => h.trim().replace(/^"|"$/g, ""));
const maxRows = params.maxRows ?? 500;
const rows = lines.slice(1, maxRows + 1).map(line => {
const values = line.split(delimiter).map(v => v.trim().replace(/^"|"$/g, ""));
const obj: Record<string, string> = {};
headers.forEach((h, i) => { obj[h] = values[i] ?? ""; });
return obj;
});
return {
content: [{ type: "text", text: JSON.stringify({ headers, rowCount: rows.length, rows }, null, 2) }],
details: {},
};
},
});
return [readExcel, writeExcel, readCsv];
}
```
### Acceptance criteria
- [ ] `read_excel` reads `.xlsx` files and returns JSON
- [ ] `write_excel` creates `.xlsx` files from structured data
- [ ] `read_csv` parses CSV files
- [ ] Tools are available in agent sessions
- [ ] Export pipeline can be triggered via agent (export agent can write Excel)
---
## Phase 5: Multi-Model Support & Model Configuration
**Goal:** Expose the SDK's multi-model capabilities through the existing RPC layer. Let users switch between providers/models.
### Files to modify
| Action | File | Description |
|--------|------|-------------|
| Modify | `packages/shared/src/llm/piSdk.ts` | Expose `ModelRegistry`, model switching |
| Modify | `packages/shared/src/rpc/modelRpc.ts` | Add model listing/switching RPC methods |
| Modify | `packages/contracts/src/rpc.ts` | Add new RPC methods for model management |
| Modify | `packages/shared/src/rpc/settingsRpc.ts` | Persist model preferences |
### New RPC methods
```typescript
// Add to contracts/rpc.ts
"model.list": undefined;
"model.set": { provider: string; modelId: string };
"model.cycle": undefined;
```
### Settings integration
```typescript
// Store model preference in settings
interface ServerSettings {
// ...existing...
llm: {
provider: string;
modelId: string;
thinkingLevel: "off" | "minimal" | "low" | "medium" | "high";
};
}
```
### Acceptance criteria
- [ ] `model.list` returns available models with valid API keys
- [ ] `model.set` switches the active model for all agents
- [ ] Model preference persists across restarts
- [ ] Thinking level is configurable
- [ ] Works with Anthropic, OpenAI, and other providers
---
## Phase 6: Improved Streaming & Event Bridge
**Goal:** Connect SDK's event system to the existing EventEmitter → IPC → React pipeline properly. Add abort support.
### Files to modify
| Action | File | Description |
|--------|------|-------------|
| Modify | `packages/shared/src/agents/eventEmitter.ts` | Add abort event types |
| Modify | `packages/shared/src/agents/runner.ts` | Forward SDK events to our EventEmitter |
| Modify | `apps/desktop/src/main.ts` | Handle new event types |
| Modify | `apps/web/src/hooks/useServerEvents.ts` | Handle tool_execution events |
| Modify | `packages/contracts/src/rpc.ts` | Add tool execution event types |
### New event types to bridge
```typescript
// SDK events → MosaicIQ events
"tool_execution_start" "agent.progress" (with tool name)
"tool_execution_update" "agent.streaming" (tool output)
"tool_execution_end" "agent.progress" (tool complete)
"message_update" "agent.streaming" (text delta)
"agent_start" "agent.started"
"agent_end" "agent.completed"
```
### Abort support
```typescript
// In runner.ts
const controller = new AbortController();
// Store for cancellation
activeSessions.set(runId, { session, controller });
// In agent.pause RPC handler
const active = activeSessions.get(runId);
if (active) {
await active.session.abort();
}
```
### Acceptance criteria
- [ ] Tool calls appear in real-time in the UI
- [ ] Text streaming works without character loss
- [ ] Agent can be aborted mid-run
- [ ] Error events are surfaced to the UI
- [ ] Queue (steer/follow-up) works for chat
---
## Phase 7: System Prompts & Agent Specialization
**Goal:** Refactor agent prompts from "data-embedded" to "instruction-only" now that tools provide data. Add proper system prompt management.
### Files to modify
| Action | File | Description |
|--------|------|-------------|
| Modify | `packages/shared/src/llm/prompts.ts` | Rewrite all agent prompts as instructions |
| Modify | `packages/shared/src/llm/context.ts` | Simplify (tools handle data now) |
| Create | `packages/shared/src/llm/systemPrompts.ts` | Centralized system prompt management |
### Prompt refactoring example
```typescript
// BEFORE: Data embedded (current)
const AGENT_PROMPTS = {
sf: (ctx) => `You are the SEC Filings Agent for ${ctx.company.name} (${ctx.company.ticker}).
Available Filings:
${ctx.filings.map(f => `- ${f.formType}: ${f.title} (${f.filedDate})`).join("\n")}
... 50+ lines of data ...`,
// AFTER: Instruction-only with tool access
const SYSTEM_PROMPTS = {
sf: `You are the SEC Filings Agent for MosaicIQ equity research.
Your role: Analyze SEC filings and extract structured financial information.
TOOLS AVAILABLE:
- get_company_info: Get company details
- get_filings: List SEC filings
- get_financial_model: Get financial model data
WORKFLOW:
1. Use get_company_info to identify the company
2. Use get_filings to retrieve recent filings
3. Analyze the filings for:
- Segment revenue data from 10-K
- Key risk factors
- Changes in accounting policies
4. Cite specific sections when making claims
OUTPUT:
Respond with structured JSON containing:
- extractedData: Key findings
- sources: Filing references with sections
- assumptions: Any assumptions made
- confidence: "high" | "medium" | "low"
RULES:
- Always cite sources
- Flag uncertainty
- Never fabricate filing content`,
};
```
### Acceptance criteria
- [ ] All 15 agent prompts rewritten as instruction-only
- [ ] Agents successfully use tools to fetch data instead of relying on embedded context
- [ ] Responses maintain same quality and structure
- [ ] `context.ts` simplified (no more giant context builders)
---
## Migration Strategy
### Order of execution
1. **Phase 1** → Non-breaking. SDK installed alongside existing client.
2. **Phase 2** → Additive. New tools, nothing removed.
3. **Phase 3****Breaking change point.** Runner switches to SDK.
- Before merging: run full manual test of all agent RPCs
4. **Phase 4** → Additive. File tools enhance export capabilities.
5. **Phase 5** → Additive. New RPC methods, existing ones unchanged.
6. **Phase 6** → Enhancement. Better streaming, abort support.
7. **Phase 7** → Refactor. Prompts rewritten after everything works.
### What to delete after migration
After Phase 3 is verified:
- **Delete `client.ts` entirely** — remove `PI_API_URL`, `PI_API_KEY` references, raw `fetch` calls
- Remove `streamStructuredResponse` / `completeStructured` (the SDK handles structured output better via tool responses)
- Simplify `context.ts` (no more `buildAgentContext` with all data)
- Remove `PI_API_KEY` from `agent.md`, `CLAUDE.md`, and all documentation
- Update error messages in `runner.ts` and `agentRpc.ts` (currently say "Set PI_API_KEY")
After Phase 7:
- Remove `AgentContext` type from `prompts.ts` (no longer needed)
- Remove `buildAgentContext`, `buildChatContext`, `buildMinimalContext` from `context.ts`
### Files to delete (eventually)
| File | When | Why |
|------|------|-----|
| `packages/shared/src/llm/client.ts` | Phase 3 complete | Replaced by `piSdk.ts` |
| `packages/shared/src/llm/context.ts` | Phase 7 complete | Tools replace context building |
### Risk mitigation
1. **Keep old client as fallback**: During Phase 1-2, keep the old `client.ts` working. Add a feature flag or env var to switch between old and new.
2. **Test with one agent first**: Migrate `agent.chat` first (simplest), verify end-to-end, then migrate the rest.
3. **Type safety**: The SDK is fully typed. Every tool, event, and session method is typed. Use this.
4. **Electron compatibility**: Test that `@earendil-works/pi-coding-agent` works in Electron's main process (Node.js). It should — it's a Node.js package.
---
## Dependency Changes
### `packages/shared/package.json`
```diff
"dependencies": {
"@mosaiciq/contracts": "workspace:*",
+ "@earendil-works/pi-coding-agent": "^latest",
"better-sqlite3": "^12.10.0",
"exceljs": "^4.4.0",
"pptxgenjs": "^4.0.1"
}
```
Note: The SDK brings in its own dependencies (typebox, etc.). These should not conflict with existing deps.
### Environment variables
```bash
# Supported providers (SDK picks up automatically)
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
DEEPSEEK_API_KEY=...
GOOGLE_API_KEY=...
# Or configure via settings file (Phase 5)
```
> **Removed:** `PI_API_KEY` and the `api.pi.ai` endpoint are no longer used.
---
## Estimated Effort
| Phase | Description | Effort |
|-------|-------------|--------|
| 1 | SDK install + adapter layer | 1-2 hours |
| 2 | Custom DB tools | 2-3 hours |
| 3 | Runner migration (breaking) | 3-4 hours |
| 4 | File tools (Excel, CSV) | 1-2 hours |
| 5 | Multi-model support | 2-3 hours |
| 6 | Streaming & event bridge | 2-3 hours |
| 7 | Prompt refactoring | 2-3 hours |
| **Total** | | **13-20 hours** |
Phase 3 is the critical path. Everything before it is additive; everything after it is polish.