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