diff --git a/packages/provider-muse/src/server/adapter.test.ts b/packages/provider-muse/src/server/adapter.test.ts index 76b46715c6020..bf1241983e893 100644 --- a/packages/provider-muse/src/server/adapter.test.ts +++ b/packages/provider-muse/src/server/adapter.test.ts @@ -1434,6 +1434,48 @@ describe("MuseAdapterV2", () => { }).pipe(Effect.scoped, Effect.provide(testLayer)), ); + it.effect.each(["cancelled", "interrupted", "timedOut", "aborted"] as const)( + "cancels a question Muse settles as %s without an answer", + (outcome) => + Effect.gen(function* () { + const fake = yield* makeFakeMuse(); + const harness = yield* makeHarness(fake); + const { nativeId } = yield* startConversation(harness, fake); + yield* fake.emit("userInput/requested", { + userInputId: "question-1", + turnId: nativeId, + questions: [ + { + id: "approach", + header: "Approach", + question: "Which approach?", + options: [{ label: "Small" }, { label: "Large" }], + selection: { mode: "single" }, + }, + ], + }); + yield* harness.takeEvent( + "runtime_request.updated", + (event) => event.runtimeRequest.kind === "user_input", + ); + yield* fake.emit("userInput/settled", { + userInputId: "question-1", + turnId: nativeId, + answers: [], + clarification: null, + outcome, + }); + const settled = yield* harness.takeEvent("runtime_request.updated"); + assert.strictEqual(settled.runtimeRequest.status, "cancelled"); + assert.isUndefined(settled.runtimeRequest.answers); + const item = yield* harness.takeEvent( + "turn_item.updated", + (event) => event.turnItem.type === "user_input_request", + ); + assert.strictEqual(item.turnItem.status, "cancelled"); + }).pipe(Effect.scoped, Effect.provide(testLayer)), + ); + it.effect("steers only the active native turn and waits for confirmed interruption", () => Effect.gen(function* () { const fake = yield* makeFakeMuse(); diff --git a/packages/provider-muse/src/server/adapter.ts b/packages/provider-muse/src/server/adapter.ts index dbf27ebea95a7..db924e2879f9c 100644 --- a/packages/provider-muse/src/server/adapter.ts +++ b/packages/provider-muse/src/server/adapter.ts @@ -284,6 +284,12 @@ const INFORMATIONAL_NOTIFICATIONS = new Set([ "usage/changed", ]); const TOOLS_WITH_NATIVE_ROWS = new Set(["write_todos", "request_user_input", "workflow"]); +const UNANSWERED_QUESTION_OUTCOMES = new Set([ + "cancelled", + "interrupted", + "timedOut", + "aborted", +]); // Reminder children and other runtime items can start while the model is still retrying. const MODEL_OUTPUT_KINDS = new Set(["agentMessage", "reasoning", "toolCall"]); const responseAnswerSchema = Schema.Union([Schema.String, Schema.Array(Schema.String)]); @@ -1373,7 +1379,12 @@ export const makeMuseAdapterV2 = Effect.fn("makeMuseAdapterV2")(function* ( ? museApprovalDecision({ decision: params.decision, scope: "once" }) : undefined; if (!entry.response && native) entry.response = { decision: native }; - yield* resolvePending(entry, "resolved"); + yield* resolvePending( + entry, + method === "userInput/settled" && UNANSWERED_QUESTION_OUTCOMES.has(params.outcome) + ? "cancelled" + : "resolved", + ); } if (![...pending.values()].some((candidate) => candidate.shown)) yield* updateSession(active ? "running" : "ready");