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 <noreply@anthropic.com>
This commit is contained in:
Jiang Bohan 2026-02-06 22:23:44 +08:00
parent dacf8894e9
commit 9fe6b920c4
3 changed files with 29 additions and 11 deletions

View file

@ -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);

View file

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

View file

@ -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<AgentRunResult> {
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;
}