refactor(core): clean up callModel readability (#38706)

This commit is contained in:
Kit Langton 2026-07-24 11:17:20 -04:00 committed by GitHub
commit 6e4a972bb9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -35,6 +35,33 @@ type CallOutcome = Data.TaggedEnum<{
}> }>
const CallOutcome = Data.taggedEnum<CallOutcome>() const CallOutcome = Data.taggedEnum<CallOutcome>()
// Declining an interactive prompt halts the drain instead of becoming model-facing tool output.
const isUserDeclined = (cause: Cause.Cause<unknown>) =>
cause.reasons.some(
(reason) =>
Cause.isDieReason(reason) &&
(reason.defect instanceof PermissionV2.DeclinedError || reason.defect instanceof QuestionTool.CancelledError),
)
/**
* Classifies how the owned tool fibers ended. Interrupts and interactive declines abort
* the step; a defect from a tool implementation becomes a failed tool call the model can
* read; a typed infrastructure failure must fail the assistant and then the drain.
*/
const classifyToolExits = (settled: Exit.Exit<Array<Exit.Exit<void, ToolOutputStore.Error>>, never>) => {
const causes =
settled._tag === "Failure"
? [settled.cause]
: settled.value.flatMap((exit) => (exit._tag === "Failure" ? [exit.cause] : []))
const failure = causes.find((cause) => !Cause.hasInterrupts(cause) && !isUserDeclined(cause))
return {
interrupted: causes.some(Cause.hasInterrupts),
declined: causes.some(isUserDeclined),
failure,
infraError: failure === undefined ? undefined : Option.getOrUndefined(Cause.findErrorOption(failure)),
}
}
const layer = Layer.effect( const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
@ -52,43 +79,87 @@ const layer = Layer.effect(
// re-fire a redundant LLM call; `SessionTitle` itself is idempotent based on durable history. // re-fire a redundant LLM call; `SessionTitle` itself is idempotent based on durable history.
const titleStarted = new Set<SessionSchema.ID>() const titleStarted = new Set<SessionSchema.ID>()
const forkTitle = yield* FiberSet.makeRuntime<never, void, never>() const forkTitle = yield* FiberSet.makeRuntime<never, void, never>()
const getSession = Effect.fn("SessionRunner.getSession")(function* (sessionID: SessionSchema.ID) { /**
const session = yield* store.get(sessionID) * Drains eligible manual compaction and user input until the Session becomes idle.
if (!session) return yield* Effect.die(new Error(`Session not found: ${sessionID}`)) * Execution lifecycle is published per busy period by SessionExecution, not here.
return session */
const drain = Effect.fn("SessionRunner.drain")(function* (input: {
readonly sessionID: SessionSchema.ID
readonly force: boolean
}) {
if (!input.force && !(yield* SessionPending.has(db, input.sessionID, "any"))) return
yield* settleStaleToolCalls(input.sessionID)
yield* runPendingCompaction(input.sessionID)
if (!input.force && !(yield* SessionPending.has(db, input.sessionID, "input"))) return
do {
yield* runSteps(input.sessionID)
} while (yield* SessionPending.has(db, input.sessionID, "input"))
}) })
/** Fires title generation once per process after the first step makes a user message visible. */
const startTitleOnce = Effect.fnUntraced(function* (sessionID: SessionSchema.ID) { /**
if (titleStarted.has(sessionID)) return * Runs logical steps until no tool result or newly admitted steer requires another
titleStarted.add(sessionID) * model call. Queued inputs remain pending until the current model work reaches idle.
forkTitle(title.generateForFirstPrompt(yield* getSession(sessionID)).pipe(Effect.ignore)) */
}) const runSteps = Effect.fn("SessionRunner.runSteps")(function* (sessionID: SessionSchema.ID) {
/** Closes stale tool calls left active by an earlier interrupted drain. */ // Fresh work may promote queued input; later steps absorb steers only.
const settleStaleToolCalls = Effect.fn("SessionRunner.settleStaleToolCalls")(function* ( let promotable: SessionPending.Promotable = "input"
sessionID: SessionSchema.ID, let step = 1
) { while (true) {
for (const message of yield* store.context(sessionID)) { const result = yield* runStep(sessionID, promotable, step)
if (message.type !== "assistant") continue yield* startTitleOnce(sessionID)
for (const tool of message.content) { yield* runPendingCompaction(sessionID)
if (tool.type !== "tool" || (tool.state.status !== "streaming" && tool.state.status !== "running")) continue if (!result.needsContinuation && !(yield* SessionPending.has(db, sessionID, "steer"))) return
yield* events.publish(SessionEvent.Tool.Failed, { promotable = "steer"
sessionID, step = result.step + 1
assistantMessageID: message.id,
callID: tool.id,
error: { type: "aborted", message: `Tool execution interrupted: ${tool.name}` },
executed: tool.executed === true,
})
}
} }
}) })
// Declining an interactive prompt halts the drain instead of becoming model-facing tool output. /** Completes one logical model step, transparently retrying or rebuilding after compaction. */
const isUserDeclined = (cause: Cause.Cause<unknown>) => const runStep = Effect.fnUntraced(function* (
cause.reasons.some( sessionID: SessionSchema.ID,
(reason) => promotable: SessionPending.Promotable,
Cause.isDieReason(reason) && step: number,
(reason.defect instanceof PermissionV2.DeclinedError || reason.defect instanceof QuestionTool.CancelledError), ) {
) const retry = yield* Schedule.toStepWithSleep(SessionRunnerRetry.schedule(events, sessionID))
/**
* Consumes one retry allowance: sleeps the scheduled backoff and reports what the next
* attempt should reuse, or publishes Step.Failed and fails once attempts are exhausted.
* The step loop performs the retry itself on the next iteration.
*/
const waitForRetry = (failure: SessionRunnerRetry.RetryableFailure) =>
retry(failure).pipe(
Effect.as(CallOutcome.Retry({ step: failure.step, assistantMessageID: failure.assistantMessageID })),
Pull.catchDone(() =>
events
.publish(SessionEvent.Step.Failed, {
sessionID,
assistantMessageID: failure.assistantMessageID,
error: failure.error,
})
.pipe(Effect.andThen(Effect.fail(failure.cause))),
),
)
let currentPromotable: SessionPending.Promotable | undefined = promotable
let currentStep = step
let assistantMessageID: SessionMessage.ID | undefined
// Overflow recovery is one-shot: a call after recovery must not recover another overflow.
let recoverOverflow = true
while (true) {
const outcome = yield* callModel(
sessionID,
currentPromotable,
currentStep,
recoverOverflow,
assistantMessageID,
).pipe(Effect.catchTag("SessionRunner.RetryableFailure", waitForRetry))
if (outcome._tag === "Completed") return { needsContinuation: outcome.needsContinuation, step: outcome.step }
if (outcome._tag === "Retry") assistantMessageID = outcome.assistantMessageID
if (outcome._tag === "Restart" && outcome.recoveredOverflow) recoverOverflow = false
// Neither a retry nor a compaction restart re-promotes input.
currentPromotable = undefined
currentStep = outcome.step
}
})
/** /**
* Prepares and runs at most one model call, executes its local tools, and durably * Prepares and runs at most one model call, executes its local tools, and durably
@ -105,11 +176,9 @@ const layer = Layer.effect(
// Establish what the model knows before admitting what the user said, so // Establish what the model knows before admitting what the user said, so
// a blocked first step leaves pending inputs untouched. // a blocked first step leaves pending inputs untouched.
yield* InstructionState.prepare(db, events, selected.instructions, selected.session.id) yield* InstructionState.prepare(db, events, selected.instructions, selected.session.id)
let currentStep = step const promoted = promotable ? yield* SessionPending.promote(db, events, selected.session.id, promotable) : 0
if (promotable) { // Promoted input opens a fresh step allowance.
const promoted = yield* SessionPending.promote(db, events, selected.session.id, promotable) const currentStep = promoted > 0 ? 1 : step
if (promoted > 0) currentStep = 1
}
const loaded = yield* context.load(selected) const loaded = yield* context.load(selected)
const { session, agent } = loaded const { session, agent } = loaded
const resolved = loaded.model const resolved = loaded.model
@ -119,7 +188,8 @@ const layer = Layer.effect(
const compactionInput = { session, messages: loaded.messages, model, cost: resolved.cost } const compactionInput = { session, messages: loaded.messages, model, cost: resolved.cost }
if (compaction.required(compactionInput) && !(yield* SessionPending.compaction(db, session.id))) { if (compaction.required(compactionInput) && !(yield* SessionPending.compaction(db, session.id))) {
const compacted = yield* compaction.compact(compactionInput) const compacted = yield* compaction.compact(compactionInput)
if (compacted.status === "completed") return CallOutcome.Restart({ step: currentStep, recoveredOverflow: false }) if (compacted.status === "completed")
return CallOutcome.Restart({ step: currentStep, recoveredOverflow: false })
return yield* new StepFailedError({ error: compacted.error }) return yield* new StepFailedError({ error: compacted.error })
} }
const prepared = yield* modelRequests.prepare({ const prepared = yield* modelRequests.prepare({
@ -236,8 +306,7 @@ const layer = Layer.effect(
recoverOverflow && recoverOverflow &&
!publisher.hasRetryEvidence() && !publisher.hasRetryEvidence() &&
isContextOverflowFailure(overflowFailure ?? streamFailure) && isContextOverflowFailure(overflowFailure ?? streamFailure) &&
(yield* restore(compaction.compact({ session, messages: loaded.messages, model, cost: resolved.cost }))) (yield* restore(compaction.compact(compactionInput))).status === "completed"
.status === "completed"
) )
return CallOutcome.Restart({ step: currentStep, recoveredOverflow: true }) return CallOutcome.Restart({ step: currentStep, recoveredOverflow: true })
@ -267,30 +336,17 @@ const layer = Layer.effect(
const settled = yield* restore( const settled = yield* restore(
Effect.forEach(ownedToolFibers, Fiber.await, { concurrency: "unbounded" }), Effect.forEach(ownedToolFibers, Fiber.await, { concurrency: "unbounded" }),
).pipe(Effect.exit) ).pipe(Effect.exit)
const settledCauses =
settled._tag === "Failure"
? [settled.cause]
: settled.value.flatMap((exit) => (exit._tag === "Failure" ? [exit.cause] : []))
const toolsInterrupted = settledCauses.some(Cause.hasInterrupts)
const userDeclined = settledCauses.some(isUserDeclined)
if (settled._tag === "Failure") yield* FiberSet.clear(toolFibers) if (settled._tag === "Failure") yield* FiberSet.clear(toolFibers)
if (userDeclined || streamInterrupted || toolsInterrupted) { const tools = classifyToolExits(settled)
if (tools.declined || streamInterrupted || tools.interrupted) {
yield* serialized(publisher.failUnsettledTools({ type: "aborted", message: "Tool execution interrupted" })) yield* serialized(publisher.failUnsettledTools({ type: "aborted", message: "Tool execution interrupted" }))
yield* serialized(publisher.failAssistant({ type: "aborted", message: "Step interrupted" })) yield* serialized(publisher.failAssistant({ type: "aborted", message: "Step interrupted" }))
} }
// A settled tool fiber failure is one of two things. A defect from a tool if (tools.failure !== undefined) {
// implementation becomes a failed tool call the model can read, and the step still const error = toSessionError(tools.infraError ?? Cause.squash(tools.failure))
// settles so the model may recover. A typed infrastructure failure (tool output
// could not be persisted) also fails the assistant and then fails the drain.
const settledFailure = settledCauses.find((cause) => !Cause.hasInterrupts(cause) && !isUserDeclined(cause))
const infraError =
settledFailure === undefined ? undefined : Option.getOrUndefined(Cause.findErrorOption(settledFailure))
if (settledFailure !== undefined) {
const failure = infraError ?? Cause.squash(settledFailure)
const error = toSessionError(failure)
yield* serialized(publisher.failUnsettledTools(error)) yield* serialized(publisher.failUnsettledTools(error))
if (infraError !== undefined) yield* serialized(publisher.failAssistant(error)) if (tools.infraError !== undefined) yield* serialized(publisher.failAssistant(error))
} }
// Fail unresolved calls before the terminal step event. Local calls have joined, so // Fail unresolved calls before the terminal step event. Local calls have joined, so
@ -338,63 +394,16 @@ const layer = Layer.effect(
} }
if (stream._tag === "Failure") return yield* Effect.failCause(stream.cause) if (stream._tag === "Failure") return yield* Effect.failCause(stream.cause)
if (userDeclined) return yield* Effect.interrupt if (tools.declined) return yield* Effect.interrupt
if ((toolsInterrupted || infraError !== undefined) && settledFailure) if ((tools.interrupted || tools.infraError !== undefined) && tools.failure)
return yield* Effect.failCause(settledFailure) return yield* Effect.failCause(tools.failure)
if (toolsInterrupted && settled._tag === "Failure") return yield* Effect.failCause(settled.cause) if (tools.interrupted && settled._tag === "Failure") return yield* Effect.failCause(settled.cause)
if (stepFailure) return yield* new StepFailedError({ error: stepFailure }) if (stepFailure) return yield* new StepFailedError({ error: stepFailure })
return CallOutcome.Completed({ needsContinuation, step: currentStep }) return CallOutcome.Completed({ needsContinuation, step: currentStep })
}), }),
) )
}, Effect.scoped) }, Effect.scoped)
/** Completes one logical model step, transparently retrying or rebuilding after compaction. */
const runStep = Effect.fnUntraced(function* (
sessionID: SessionSchema.ID,
promotable: SessionPending.Promotable,
step: number,
) {
const retry = yield* Schedule.toStepWithSleep(SessionRunnerRetry.schedule(events, sessionID))
/**
* Consumes one retry allowance: sleeps the scheduled backoff and reports what the next
* attempt should reuse, or publishes Step.Failed and fails once attempts are exhausted.
* The step loop performs the retry itself on the next iteration.
*/
const waitForRetry = (failure: SessionRunnerRetry.RetryableFailure) =>
retry(failure).pipe(
Effect.as(CallOutcome.Retry({ step: failure.step, assistantMessageID: failure.assistantMessageID })),
Pull.catchDone(() =>
events
.publish(SessionEvent.Step.Failed, {
sessionID,
assistantMessageID: failure.assistantMessageID,
error: failure.error,
})
.pipe(Effect.andThen(Effect.fail(failure.cause))),
),
)
let currentPromotable: SessionPending.Promotable | undefined = promotable
let currentStep = step
let assistantMessageID: SessionMessage.ID | undefined
// Overflow recovery is one-shot: a call after recovery must not recover another overflow.
let recoverOverflow = true
while (true) {
const outcome = yield* callModel(
sessionID,
currentPromotable,
currentStep,
recoverOverflow,
assistantMessageID,
).pipe(Effect.catchTag("SessionRunner.RetryableFailure", waitForRetry))
if (outcome._tag === "Completed") return { needsContinuation: outcome.needsContinuation, step: outcome.step }
if (outcome._tag === "Retry") assistantMessageID = outcome.assistantMessageID
if (outcome._tag === "Restart" && outcome.recoveredOverflow) recoverOverflow = false
// Neither a retry nor a compaction restart re-promotes input.
currentPromotable = undefined
currentStep = outcome.step
}
})
/** Executes a previously admitted manual compaction request, if one is pending. */ /** Executes a previously admitted manual compaction request, if one is pending. */
const runPendingCompaction = Effect.fn("SessionRunner.runPendingCompaction")(function* ( const runPendingCompaction = Effect.fn("SessionRunner.runPendingCompaction")(function* (
sessionID: SessionSchema.ID, sessionID: SessionSchema.ID,
@ -429,39 +438,36 @@ const layer = Layer.effect(
) )
}) })
/** /** Closes stale tool calls left active by an earlier interrupted drain. */
* Runs logical steps until no tool result or newly admitted steer requires another const settleStaleToolCalls = Effect.fn("SessionRunner.settleStaleToolCalls")(function* (
* model call. Queued inputs remain pending until the current model work reaches idle. sessionID: SessionSchema.ID,
*/ ) {
const runSteps = Effect.fn("SessionRunner.runSteps")(function* (sessionID: SessionSchema.ID) { for (const message of yield* store.context(sessionID)) {
// Fresh work may promote queued input; later steps absorb steers only. if (message.type !== "assistant") continue
let promotable: SessionPending.Promotable = "input" for (const tool of message.content) {
let step = 1 if (tool.type !== "tool" || (tool.state.status !== "streaming" && tool.state.status !== "running")) continue
while (true) { yield* events.publish(SessionEvent.Tool.Failed, {
const result = yield* runStep(sessionID, promotable, step) sessionID,
yield* startTitleOnce(sessionID) assistantMessageID: message.id,
yield* runPendingCompaction(sessionID) callID: tool.id,
if (!result.needsContinuation && !(yield* SessionPending.has(db, sessionID, "steer"))) return error: { type: "aborted", message: `Tool execution interrupted: ${tool.name}` },
promotable = "steer" executed: tool.executed === true,
step = result.step + 1 })
}
} }
}) })
/** /** Fires title generation once per process after the first step makes a user message visible. */
* Drains eligible manual compaction and user input until the Session becomes idle. const startTitleOnce = Effect.fnUntraced(function* (sessionID: SessionSchema.ID) {
* Execution lifecycle is published per busy period by SessionExecution, not here. if (titleStarted.has(sessionID)) return
*/ titleStarted.add(sessionID)
const drain = Effect.fn("SessionRunner.drain")(function* (input: { forkTitle(title.generateForFirstPrompt(yield* getSession(sessionID)).pipe(Effect.ignore))
readonly sessionID: SessionSchema.ID })
readonly force: boolean
}) { const getSession = Effect.fn("SessionRunner.getSession")(function* (sessionID: SessionSchema.ID) {
if (!input.force && !(yield* SessionPending.has(db, input.sessionID, "any"))) return const session = yield* store.get(sessionID)
yield* settleStaleToolCalls(input.sessionID) if (!session) return yield* Effect.die(new Error(`Session not found: ${sessionID}`))
yield* runPendingCompaction(input.sessionID) return session
if (!input.force && !(yield* SessionPending.has(db, input.sessionID, "input"))) return
do {
yield* runSteps(input.sessionID)
} while (yield* SessionPending.has(db, input.sessionID, "input"))
}) })
return Service.of({ drain }) return Service.of({ drain })