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>
844 lines
27 KiB
Markdown
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.
|