From 5deef980dd7a1ceba4d4e53bef737033fc3a3a40 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 3 Jul 2026 16:55:18 -0500 Subject: [PATCH] refactor(core): handle form cancellation in runner --- packages/core/src/form.ts | 2 ++ packages/core/src/session/runner/llm.ts | 14 ++++++-------- packages/core/src/tool/question.ts | 3 +-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/core/src/form.ts b/packages/core/src/form.ts index d49129b3b81..fbb286eba04 100644 --- a/packages/core/src/form.ts +++ b/packages/core/src/form.ts @@ -63,6 +63,8 @@ export class InvalidFormError extends Schema.TaggedErrorClass( message: Schema.String, }) {} +export class CancelledError extends Schema.TaggedErrorClass()("Form.CancelledError", {}) {} + export type CreateInput = | (Omit & { readonly id?: ID }) | (Omit & { readonly id?: ID }) diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index 0aeaa0e0525..bd9fdcf9c9a 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -16,7 +16,7 @@ import { Config } from "../../config" import { Database } from "../../database/database" import { EventV2 } from "../../event" import { Location } from "../../location" -import { QuestionV2 } from "../../question" +import { Form } from "../../form" import { SystemContext } from "../../system-context/index" import { SystemContextBuiltIns } from "../../system-context/builtins" import { InstructionContext } from "../../instruction-context" @@ -149,9 +149,8 @@ const layer = Layer.effect( const awaitToolFibers = (fibers: FiberSet.FiberSet) => Effect.raceFirst(FiberSet.join(fibers), FiberSet.awaitEmpty(fibers)) - // Match V1: dismissing a question halts the loop instead of becoming model-facing tool output. - const isQuestionRejected = (cause: Cause.Cause) => - cause.reasons.some((reason) => Cause.isDieReason(reason) && reason.defect instanceof QuestionV2.RejectedError) + const isFormCancelled = (cause: Cause.Cause) => + cause.reasons.some((reason) => Cause.isDieReason(reason) && reason.defect instanceof Form.CancelledError) const loadSystemContext = (agent: AgentV2.Selection, sessionID: SessionSchema.ID) => Effect.all( @@ -341,14 +340,13 @@ const layer = Layer.effect( if (streamInterrupted) yield* FiberSet.clear(toolFibers) const settled = yield* restore(awaitToolFibers(toolFibers)).pipe(Effect.exit) const toolsInterrupted = settled._tag === "Failure" && Cause.hasInterrupts(settled.cause) - const questionDismissed = settled._tag === "Failure" && isQuestionRejected(settled.cause) + const formCancelled = settled._tag === "Failure" && isFormCancelled(settled.cause) - if (questionDismissed || streamInterrupted || toolsInterrupted) { + if (formCancelled || streamInterrupted || toolsInterrupted) { yield* FiberSet.clear(toolFibers) yield* serialized(publisher.failUnsettledTools("Tool execution interrupted")) yield* serialized(publisher.failAssistant("Step interrupted")) - // Match V1: dismissing a question halts the loop like an interruption. - if (questionDismissed) return yield* Effect.interrupt + if (formCancelled) return yield* Effect.interrupt } // A settled tool fiber failure is one of two things. A defect from a tool // implementation becomes a failed tool call the model can read, and the step still diff --git a/packages/core/src/tool/question.ts b/packages/core/src/tool/question.ts index 463997cd264..20be82f06cc 100644 --- a/packages/core/src/tool/question.ts +++ b/packages/core/src/tool/question.ts @@ -94,8 +94,7 @@ export const Plugin = { .pipe(Effect.orDie), ), Effect.flatMap((state) => { - // The runner halts the loop on this exact defect (session/runner/llm.ts). - if (state.status !== "answered") return Effect.die(new QuestionV2.RejectedError()) + if (state.status !== "answered") return Effect.die(new Form.CancelledError()) return Effect.succeed({ answers: input.questions.map((_, index): QuestionV2.Answer => { const value = state.answer[`q${index}`]