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>
This commit is contained in:
paseo-bot[bot]
2026-10-05 12:03:18 +02:00
committed by GitHub
parent da9fae172d
commit 8ced059b4d
4 changed files with 77 additions and 5 deletions
@@ -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 });
@@ -172,6 +172,21 @@ interface OmpPersistenceMetadata {
bridgedTools: Map<string, OmpBridgedToolIdentity>;
}
/** 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<string, boolean>();
private activePromptError: string | null = null;
private readonly pendingPromptResults = new Map<string, OmpPromptResult>();
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;
@@ -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(),
@@ -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<unknown> {
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,