From 8ced059b4da95a1d4931e991ded21d7597045f5f Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:03:18 +0200 Subject: [PATCH] fix(omp): fail the turn when OMP rejects a prompt before its agent runs (#6110) OMP reports a prompt it rejects before running the agent, such as one with no API key for the selected model, as a prompt_result with agentInvoked false, status error, and the error message. Paseo read only agentInvoked and ended the turn as completed, so the agent went idle with no reply and no error. The turn now fails with OMP's message. Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../server/agent/providers/omp/agent.test.ts | 18 +++++++++ .../src/server/agent/providers/omp/agent.ts | 37 ++++++++++++++++--- .../server/agent/providers/omp/rpc-types.ts | 2 + .../providers/omp/test-utils/omp-harness.ts | 25 +++++++++++++ 4 files changed, 77 insertions(+), 5 deletions(-) diff --git a/packages/server/src/server/agent/providers/omp/agent.test.ts b/packages/server/src/server/agent/providers/omp/agent.test.ts index f06bb62f57..11eaa63fb7 100644 --- a/packages/server/src/server/agent/providers/omp/agent.test.ts +++ b/packages/server/src/server/agent/providers/omp/agent.test.ts @@ -927,6 +927,24 @@ describe("OMP agent client and session", () => { expect(omp.completedTurnCount()).toBe(1); }); + test.each(["result after ack", "result before ack"] as const)( + "fails a prompt OMP rejects before its agent runs (%s)", + async (order) => { + const omp = new OmpHarness(); + await omp.start(); + + await expect( + omp.runPromptRejectedBeforeAgentRuns( + "Reply with ok.", + "No API key found for anthropic.", + order, + ), + ).rejects.toThrow("No API key found for anthropic."); + expect(omp.turnFailures()).toEqual(["No API key found for anthropic."]); + expect(omp.completedTurnCount()).toBe(0); + }, + ); + test("completes a no-turn notify with one notification and no assistant text", async () => { const scheduler = new ManualNoTurnScheduler(); const omp = new OmpHarness({ noTurnScheduler: scheduler }); diff --git a/packages/server/src/server/agent/providers/omp/agent.ts b/packages/server/src/server/agent/providers/omp/agent.ts index 4c97014fe8..b4e7b40bee 100644 --- a/packages/server/src/server/agent/providers/omp/agent.ts +++ b/packages/server/src/server/agent/providers/omp/agent.ts @@ -172,6 +172,21 @@ interface OmpPersistenceMetadata { bridgedTools: Map; } +/** OMP's outcome for one prompt, correlated by the prompt's request id. */ +interface OmpPromptResult { + agentInvoked: boolean; + /** Why OMP rejected the prompt before its agent ran, such as a missing API key. */ + error: string | null; +} + +function promptResultError(result: { + status?: string; + error?: { message: string }; +}): string | null { + if (result.status !== "error") return null; + return result.error?.message || "OMP rejected the prompt"; +} + interface StartTurnResult { turnId: string; } @@ -695,7 +710,8 @@ export class OmpAgentSession implements AgentSession { private readonly pendingNoTurnOutputs: Array<{ turnId: string; message: string }> = []; private activePromptRequestId: string | null = null; private activePromptAgentInvoked: boolean | null = null; - private readonly pendingPromptResults = new Map(); + private activePromptError: string | null = null; + private readonly pendingPromptResults = new Map(); private pendingNoTurnCompletionAbort: AbortController | null = null; private lastKnownThinkingOptionId: string | null; private outOfBandCompactionEmit: ((event: AgentStreamEvent) => void) | null = null; @@ -865,12 +881,13 @@ export class OmpAgentSession implements AgentSession { if (ack.requestId) { this.pendingPromptResults.delete(ack.requestId); } - this.activePromptAgentInvoked = correlatedResult ?? ack.agentInvoked ?? null; - if (correlatedResult === false) { + this.activePromptAgentInvoked = correlatedResult?.agentInvoked ?? ack.agentInvoked ?? null; + this.activePromptError = correlatedResult?.error ?? null; + if (correlatedResult?.agentInvoked === false) { this.scheduleNoTurnPromptCompletion(turnId); return; } - if (correlatedResult !== true && ack.agentInvoked === false) { + if (correlatedResult?.agentInvoked !== true && ack.agentInvoked === false) { await this.completeNoTurnPrompt(turnId); return; } @@ -1309,7 +1326,14 @@ export class OmpAgentSession implements AgentSession { ) { return; } + const error = this.activePromptError; this.emitBufferedNoTurnOutputs(turnId); + if (error) { + this.usagePoller.stopTurn(); + this.resetActiveTurn({ terminalizeWork: true }); + this.emit({ type: "turn_failed", provider: this.provider, turnId, error }); + return; + } this.completeTurn(turnId, []); } @@ -1318,6 +1342,7 @@ export class OmpAgentSession implements AgentSession { this.activeNoTurnPromptText = null; this.activePromptRequestId = null; this.activePromptAgentInvoked = null; + this.activePromptError = null; this.pendingNoTurnOutputs.splice(0, this.pendingNoTurnOutputs.length); } @@ -1715,15 +1740,17 @@ export class OmpAgentSession implements AgentSession { ? event.agentInvoked : undefined; if (requestId && agentInvoked !== undefined) { + const result = { agentInvoked, error: promptResultError(event) }; if (requestId === this.activePromptRequestId && this.activeTurnId) { this.activePromptAgentInvoked = agentInvoked; + this.activePromptError = result.error; if (agentInvoked === false) { this.scheduleNoTurnPromptCompletion(this.activeTurnId); } else { this.cancelNoTurnPromptCompletion(); } } else if (this.activePromptRequestId === null) { - this.pendingPromptResults.set(requestId, agentInvoked); + this.pendingPromptResults.set(requestId, result); } } return; diff --git a/packages/server/src/server/agent/providers/omp/rpc-types.ts b/packages/server/src/server/agent/providers/omp/rpc-types.ts index 4c69f2fbeb..42972b6d1e 100644 --- a/packages/server/src/server/agent/providers/omp/rpc-types.ts +++ b/packages/server/src/server/agent/providers/omp/rpc-types.ts @@ -480,6 +480,8 @@ export const OmpRuntimeEventSchema = z.discriminatedUnion("type", [ type: z.literal("prompt_result"), id: z.string().optional(), agentInvoked: z.boolean().optional(), + status: z.string().optional(), + error: z.object({ message: z.string() }).passthrough().optional(), }) .passthrough(), z.object({ type: z.literal("process_exit"), error: z.string() }).passthrough(), diff --git a/packages/server/src/server/agent/providers/omp/test-utils/omp-harness.ts b/packages/server/src/server/agent/providers/omp/test-utils/omp-harness.ts index f7f0039257..233a10984e 100644 --- a/packages/server/src/server/agent/providers/omp/test-utils/omp-harness.ts +++ b/packages/server/src/server/agent/providers/omp/test-utils/omp-harness.ts @@ -329,6 +329,31 @@ export class OmpHarness { return { completed: () => isCompleted, completion }; } + async runPromptRejectedBeforeAgentRuns( + input: string, + error: string, + order: "result after ack" | "result before ack", + ): Promise { + const session = this.requireSession(); + const runtime = this.omp.latestSession(); + runtime.promptAck = { requestId: "prompt-rejected" }; + const rejection = { + type: "prompt_result", + id: "prompt-rejected", + agentInvoked: false, + status: "error", + sessionSettled: true, + error: { message: error, retryable: false }, + } as const; + const promptStarted = runtime.nextPrompt(); + const run = session.run(input); + if (order === "result before ack") runtime.emit(rejection); + await promptStarted; + await waitForImmediate(); + if (order === "result after ack") runtime.emit(rejection); + return await run; + } + async runPromptAfterCorrelatedTrueResult( input: string, output: string,