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>
27 KiB
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
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
// 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 installsucceeds with new SDK dependency- Existing
agent.chat,agent.start,validation.runRPC methods still work ANTHROPIC_API_KEYorOPENAI_API_KEYare picked up from env- No references to
PI_API_KEYorapi.pi.airemain - 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
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
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
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
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):
- Build huge context object from DB
- Interpolate context into a giant prompt string
- Send prompt to LLM
- Parse JSON from response
After (SDK):
- Create SDK session with company-specific custom tools
- Send a focused prompt (the agent's instructions + user request)
- LLM calls tools as needed to get data
- LLM returns structured response
// 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:
// 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.startRPC works end-to-end with SDKagent.chatRPC 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
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_excelreads.xlsxfiles and returns JSONwrite_excelcreates.xlsxfiles from structured dataread_csvparses 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
// Add to contracts/rpc.ts
"model.list": undefined;
"model.set": { provider: string; modelId: string };
"model.cycle": undefined;
Settings integration
// Store model preference in settings
interface ServerSettings {
// ...existing...
llm: {
provider: string;
modelId: string;
thinkingLevel: "off" | "minimal" | "low" | "medium" | "high";
};
}
Acceptance criteria
model.listreturns available models with valid API keysmodel.setswitches 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
// 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
// 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
// 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.tssimplified (no more giant context builders)
Migration Strategy
Order of execution
- Phase 1 → Non-breaking. SDK installed alongside existing client.
- Phase 2 → Additive. New tools, nothing removed.
- Phase 3 → Breaking change point. Runner switches to SDK.
- Before merging: run full manual test of all agent RPCs
- Phase 4 → Additive. File tools enhance export capabilities.
- Phase 5 → Additive. New RPC methods, existing ones unchanged.
- Phase 6 → Enhancement. Better streaming, abort support.
- Phase 7 → Refactor. Prompts rewritten after everything works.
What to delete after migration
After Phase 3 is verified:
- Delete
client.tsentirely — removePI_API_URL,PI_API_KEYreferences, rawfetchcalls - Remove
streamStructuredResponse/completeStructured(the SDK handles structured output better via tool responses) - Simplify
context.ts(no morebuildAgentContextwith all data) - Remove
PI_API_KEYfromagent.md,CLAUDE.md, and all documentation - Update error messages in
runner.tsandagentRpc.ts(currently say "Set PI_API_KEY")
After Phase 7:
- Remove
AgentContexttype fromprompts.ts(no longer needed) - Remove
buildAgentContext,buildChatContext,buildMinimalContextfromcontext.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
- Keep old client as fallback: During Phase 1-2, keep the old
client.tsworking. Add a feature flag or env var to switch between old and new. - Test with one agent first: Migrate
agent.chatfirst (simplest), verify end-to-end, then migrate the rest. - Type safety: The SDK is fully typed. Every tool, event, and session method is typed. Use this.
- Electron compatibility: Test that
@earendil-works/pi-coding-agentworks in Electron's main process (Node.js). It should — it's a Node.js package.
Dependency Changes
packages/shared/package.json
"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
# 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_KEYand theapi.pi.aiendpoint 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.