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

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 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

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):

  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
// 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.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

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

// 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.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

// 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.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 3Breaking 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

  "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_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.