From 1dfa6ce2b4590da383ada9a882352cf7d6a9ebc1 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Mon, 23 Feb 2026 22:06:42 -0800 Subject: [PATCH] fix(pi): inherit default model and api key for embedded agent --- src/backends/piEmbedded.test.ts | 21 +++++++++++++++++++++ src/backends/piEmbedded.ts | 30 +++++++++++++++++++++++++++--- src/daemon/index.ts | 30 ++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 3 deletions(-) diff --git a/src/backends/piEmbedded.test.ts b/src/backends/piEmbedded.test.ts index 6b077dc..3994e7a 100644 --- a/src/backends/piEmbedded.test.ts +++ b/src/backends/piEmbedded.test.ts @@ -100,6 +100,27 @@ describe('PiEmbeddedBackend', () => { } }); + it('surfaces agent state error when no assistant text is produced', async () => { + const mod = createModule(` + export class Agent { + constructor() { + this.state = { messages: [], error: undefined }; + } + async prompt() { + this.state.error = "Missing API key for default provider"; + } + } + `); + + try { + const backend = new PiEmbeddedBackend({ module: mod.moduleUrl, timeoutMs: 2000 }); + await expect(backend.process({ prompt: 'hello', history: [] })) + .rejects.toThrow('Missing API key for default provider'); + } finally { + mod.cleanup(); + } + }); + it('throws when module cannot be loaded', async () => { const backend = new PiEmbeddedBackend({ module: '/definitely/missing/pi-module.mjs', timeoutMs: 2000 }); await expect(backend.process({ prompt: 'hello', history: [] })) diff --git a/src/backends/piEmbedded.ts b/src/backends/piEmbedded.ts index 4626e08..e8aae8b 100644 --- a/src/backends/piEmbedded.ts +++ b/src/backends/piEmbedded.ts @@ -33,6 +33,8 @@ interface PiModelFactoryModuleLike extends PiModuleLike { export interface PiEmbeddedBackendOptions { timeoutMs?: number; model?: string; + defaultModelSpec?: string; + getApiKey?: (provider: string) => string | undefined | Promise; systemPromptMode?: PiSystemPromptMode; module?: string; } @@ -197,12 +199,16 @@ export class PiEmbeddedBackend implements ExternalBackend { readonly name = 'pi_embedded' as const; private readonly timeoutMs: number; private readonly model?: string; + private readonly defaultModelSpec?: string; + private readonly getApiKey?: (provider: string) => string | undefined | Promise; private readonly systemPromptMode: PiSystemPromptMode; private readonly moduleOverride?: string; constructor(options: PiEmbeddedBackendOptions = {}) { this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; this.model = options.model; + this.defaultModelSpec = options.defaultModelSpec; + this.getApiKey = options.getApiKey; this.systemPromptMode = options.systemPromptMode ?? 'hybrid'; this.moduleOverride = options.module; } @@ -294,14 +300,28 @@ export class PiEmbeddedBackend implements ExternalBackend { input: ExternalBackendRequest, moduleName: string, ): Promise { - const agent = new AgentCtor(); + const modelSpec = this.model ?? this.defaultModelSpec; + const model = modelSpec + ? await this.resolvePiModel(modelSpec, moduleName) + : undefined; + + const agentOptions: Record = {}; + if (model) { + agentOptions.initialState = { model }; + } + if (this.getApiKey) { + agentOptions.getApiKey = this.getApiKey; + } + + const agent = Object.keys(agentOptions).length > 0 + ? new AgentCtor(agentOptions) + : new AgentCtor(); if (!agent || typeof agent !== 'object') { throw new Error('Pi Agent constructor returned an invalid object'); } const agentObj = agent as PiSessionLike; - if (this.model) { - const model = await this.resolvePiModel(this.model, moduleName); + if (model) { const setModel = agentObj.setModel; if (typeof setModel === 'function') { await Promise.resolve(setModel.call(agent, model)); @@ -348,6 +368,10 @@ export class PiEmbeddedBackend implements ExternalBackend { } } + const stateError = (state as PiSessionLike).error; + if (typeof stateError === 'string' && stateError.trim().length > 0) { + throw new Error(`Pi Agent runtime produced no assistant text: ${stateError}`); + } throw new Error('Pi Agent runtime produced no assistant text'); } diff --git a/src/daemon/index.ts b/src/daemon/index.ts index 648a8ca..b123561 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -44,11 +44,39 @@ import { type ExternalBackendName, } from '../backends/index.js'; +function collectModelApiKeys(config: Config): Record { + const keys: Record = {}; + const candidates = [ + config.models.default, + config.models.fast, + config.models.complex, + config.models.local, + ...Object.values(config.models.local_providers ?? {}), + ]; + + for (const candidate of candidates) { + if (!candidate?.provider || !candidate.api_key) { + continue; + } + if (candidate.api_key.trim().length === 0) { + continue; + } + keys[candidate.provider] = candidate.api_key; + } + + return keys; +} + +function defaultPiModelSpec(config: Config): string { + return `${config.models.default.provider}:${config.models.default.model}`; +} + export function createConfiguredExternalBackends(config: Config): { externalBackends: Partial>; defaultName?: ExternalBackendName; } { const backends: Partial> = {}; + const modelApiKeys = collectModelApiKeys(config); if (config.backends.claude_code.enabled) { backends.claude_code = new ClaudeCodeBackend( @@ -82,6 +110,8 @@ export function createConfiguredExternalBackends(config: Config): { backends.pi_embedded = new PiEmbeddedBackend({ timeoutMs: config.backends.pi_embedded.timeout_ms, model: config.backends.pi_embedded.model, + defaultModelSpec: defaultPiModelSpec(config), + getApiKey: (provider) => modelApiKeys[provider], systemPromptMode: config.backends.pi_embedded.system_prompt_mode, module: config.backends.pi_embedded.module, });