surface silent runtime failures as error events

AgentRuntime.trigger() wrapped its body in try/finally with no outer
catch. An inner catch around the streamAgent for-await only handled
AbortError and rethrew everything else. Call sites fire-and-forget
trigger (runs.ts:26,60,72), so any thrown error became an unhandled
promise rejection. The finally still ran and published
run-processing-end, but nothing told the renderer why — the chat
showed the spinner, then an empty assistant bubble.

Provider misconfig, invalid API keys, unknown model ids, streamText
setup throws, runsRepo.fetch or loadAgent failing, and provider
auth/rate-limit rejections on the first chunk all hit this path on a
first message. All invisible.

Add a top-level catch that formats the error to a string and emits a
{type: "error"} RunEvent via the existing runsRepo/bus path. The
renderer already renders those as a chat bubble plus toast
(App.tsx:2069) — no UI work needed.

No changes to the abort path: user-initiated stops still flow through
the existing inner catch and the signal.aborted branch that emits
run-stopped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ramnique Singh 2026-04-21 14:36:00 +05:30
parent 32b6b2f1c0
commit c81d3cb27b

View file

@ -194,6 +194,19 @@ export class AgentRuntime implements IAgentRuntime {
await this.runsRepo.appendEvents(runId, [stoppedEvent]);
await this.bus.publish(stoppedEvent);
}
} catch (error) {
console.error(`Run ${runId} failed:`, error);
const message = error instanceof Error
? (error.stack || error.message || error.name)
: typeof error === "string" ? error : JSON.stringify(error);
const errorEvent: z.infer<typeof RunEvent> = {
runId,
type: "error",
error: message,
subflow: [],
};
await this.runsRepo.appendEvents(runId, [errorEvent]);
await this.bus.publish(errorEvent);
} finally {
this.abortRegistry.cleanup(runId);
await this.runsLock.release(runId);