From 9fe6b920c4ef3e90a9c795e94f077bef23698dd9 Mon Sep 17 00:00:00 2001 From: Jiang Bohan Date: Fri, 6 Feb 2026 22:23:44 +0800 Subject: [PATCH] fix(agent): always provide getApiKey callback and emit error events PiAgentCore was created with an empty object when no API key was initially configured. This broke dynamic provider switching because setProvider() updated currentApiKey but PiAgentCore had no getApiKey callback to read it. Always provide the callback so it dynamically reads the current key. Also adds AgentErrorEvent to MulticaEvent and emits it from AsyncAgent.write() catch handlers so errors flow through the subscriber mechanism to IPC listeners. Co-Authored-By: Claude Opus 4.6 --- src/agent/async-agent.ts | 3 +++ src/agent/events.ts | 12 +++++++++--- src/agent/runner.ts | 25 +++++++++++++++++-------- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/agent/async-agent.ts b/src/agent/async-agent.ts index c1eb9a5e..c1b30ea4 100644 --- a/src/agent/async-agent.ts +++ b/src/agent/async-agent.ts @@ -51,11 +51,14 @@ export class AsyncAgent { // Normal text is delivered via message_end event; only handle errors here if (result.error) { this.channel.send({ id: uuidv7(), content: `[error] ${result.error}` }); + this.agent.emitError(result.error); } }) .catch((err) => { const message = err instanceof Error ? err.message : String(err); this.channel.send({ id: uuidv7(), content: `[error] ${message}` }); + // Also emit through subscriber mechanism so IPC listeners receive the error + this.agent.emitError(message); }) .finally(() => { this.pendingWrites = Math.max(0, this.pendingWrites - 1); diff --git a/src/agent/events.ts b/src/agent/events.ts index 8eb8b422..3ae35b64 100644 --- a/src/agent/events.ts +++ b/src/agent/events.ts @@ -21,10 +21,16 @@ export type CompactionEndEvent = { type: "compaction_end"; removed: number; kept: number; - tokensRemoved?: number; - tokensKept?: number; + tokensRemoved?: number | undefined; + tokensKept?: number | undefined; reason: "count" | "tokens" | "summary" | "pruning"; }; +/** Emitted when an agent encounters an error during execution */ +export type AgentErrorEvent = { + type: "agent_error"; + message: string; +}; + /** Union of all Multica-specific events */ -export type MulticaEvent = CompactionStartEvent | CompactionEndEvent; +export type MulticaEvent = CompactionStartEvent | CompactionEndEvent | AgentErrorEvent; diff --git a/src/agent/runner.ts b/src/agent/runner.ts index d364f0fa..c6fa4566 100644 --- a/src/agent/runner.ts +++ b/src/agent/runner.ts @@ -1,7 +1,7 @@ import { Agent as PiAgentCore, type AgentEvent, type AgentMessage } from "@mariozechner/pi-agent-core"; import { v7 as uuidv7 } from "uuid"; import type { AgentOptions, AgentRunResult, ReasoningMode } from "./types.js"; -import type { MulticaEvent } from "./events.js"; +import type { MulticaEvent, CompactionEndEvent } from "./events.js"; import { createAgentOutput } from "./cli/output.js"; import { resolveModel, resolveTools, type ResolveToolsOptions } from "./tools.js"; import { @@ -159,11 +159,14 @@ export class Agent { : 0; } - this.agent = new PiAgentCore( - this.currentApiKey - ? { getApiKey: (_provider: string) => this.currentApiKey! } - : {}, - ); + this.agent = new PiAgentCore({ + getApiKey: (_provider: string) => { + if (!this.currentApiKey) { + throw new Error(`No API key configured for provider: ${this.resolvedProvider}`); + } + return this.currentApiKey; + }, + }); // Load Agent Profile (if profileId is specified) // Every Agent should have a Profile for memory, tools config, and other settings @@ -352,6 +355,11 @@ export class Agent { } } + /** Emit an error event through the subscriber mechanism */ + emitError(message: string): void { + this.emitMulticaEvent({ type: "agent_error", message }); + } + async run(prompt: string): Promise { await this.ensureInitialized(); this.output.state.lastAssistantText = ""; @@ -461,14 +469,15 @@ export class Agent { if (result?.kept) { this.agent.replaceMessages(result.kept); } - this.emitMulticaEvent({ + const endEvent: CompactionEndEvent = { type: "compaction_end", removed: result?.removedCount ?? 0, kept: result?.kept.length ?? messages.length, tokensRemoved: result?.tokensRemoved, tokensKept: result?.tokensKept, reason: result?.reason ?? "tokens", - }); + }; + this.emitMulticaEvent(endEvent); } catch (err) { throw err; }