refactor(core): align runner naming with step vocabulary (#35227)

This commit is contained in:
Kit Langton
2026-07-03 15:41:50 -04:00
committed by GitHub
parent 78e01f528b
commit df0efb490b
14 changed files with 47 additions and 50 deletions
@@ -18,7 +18,7 @@ const decodeApplied = Schema.decodeUnknownOption(SystemContext.Applied)
* Loads or creates the session's durable context checkpoint, narrating any * Loads or creates the session's durable context checkpoint, narrating any
* drift since the model was last told as a chronological update. Completed * drift since the model was last told as a chronological update. Completed
* compaction rebaselines; nothing else rewrites the baseline. Runs before * compaction rebaselines; nothing else rewrites the baseline. Runs before
* input promotion so a blocked first turn leaves pending inputs untouched. * input promotion so a blocked first step leaves pending inputs untouched.
*/ */
export const prepare = Effect.fn("SessionContextCheckpoint.prepare")(function* ( export const prepare = Effect.fn("SessionContextCheckpoint.prepare")(function* (
db: DatabaseService, db: DatabaseService,
+1 -1
View File
@@ -20,7 +20,7 @@ const layer = Layer.effect(
drain: Effect.fnUntraced(function* (sessionID: SessionSchema.ID, force) { drain: Effect.fnUntraced(function* (sessionID: SessionSchema.ID, force) {
const session = yield* store.get(sessionID) const session = yield* store.get(sessionID)
if (!session) return yield* Effect.die(new Error(`Session not found: ${sessionID}`)) if (!session) return yield* Effect.die(new Error(`Session not found: ${sessionID}`))
return yield* SessionRunner.Service.use((runner) => runner.run({ sessionID, force })).pipe( return yield* SessionRunner.Service.use((runner) => runner.drain({ sessionID, force })).pipe(
Effect.provide(locations.get(session.location)), Effect.provide(locations.get(session.location)),
Effect.tapCause((cause) => Effect.tapCause((cause) =>
Cause.hasInterruptsOnly(cause) Cause.hasInterruptsOnly(cause)
+1 -1
View File
@@ -34,7 +34,7 @@ const messageRows = Effect.fnUntraced(function* (
and( and(
eq(SessionMessageTable.session_id, sessionID), eq(SessionMessageTable.session_id, sessionID),
// Keep system updates visible in the gap between a completed compaction // Keep system updates visible in the gap between a completed compaction
// and the next prepared turn's rebaseline, when their content is not yet // and the next prepared step's rebaseline, when their content is not yet
// folded into a new baseline. // folded into a new baseline.
compaction compaction
? or( ? or(
+2 -2
View File
@@ -36,9 +36,9 @@ const layer = Layer.effect(
// absolute paths, but the human-facing description shows paths relative to the project // absolute paths, but the human-facing description shows paths relative to the project
// root so opening a subdirectory still describes paths from the project root. // root so opening a subdirectory still describes paths from the project root.
const root = yield* fs.resolve(location.project.directory) const root = yield* fs.resolve(location.project.directory)
// Same-turn parallel reads settle concurrently, so an in-memory claim guards each // Same-step parallel reads settle concurrently, so an in-memory claim guards each
// Session/path pair before any filesystem work. The durable history check below covers // Session/path pair before any filesystem work. The durable history check below covers
// paths injected in earlier turns after this Location layer was reopened. // paths injected in earlier steps after this Location layer was reopened.
const injected = yield* Ref.make<Map<SessionSchema.ID, Set<string>>>(new Map()) const injected = yield* Ref.make<Map<SessionSchema.ID, Set<string>>>(new Map())
const load = Effect.fn("SessionInstructions.load")(function* (input: { const load = Effect.fn("SessionInstructions.load")(function* (input: {
+1 -1
View File
@@ -19,7 +19,7 @@ export interface Adapter {
export function memory(state: MemoryState): Adapter { export function memory(state: MemoryState): Adapter {
const assistantIndex = (messageID: SessionMessage.ID) => const assistantIndex = (messageID: SessionMessage.ID) =>
state.messages.findLastIndex((message) => message.id === messageID) state.messages.findLastIndex((message) => message.id === messageID)
// A newer turn supersedes stale incomplete rows; never resume an older assistant projection. // A newer step supersedes stale incomplete rows; never resume an older assistant projection.
const latestAssistantIndex = () => state.messages.findLastIndex((message) => message.type === "assistant") const latestAssistantIndex = () => state.messages.findLastIndex((message) => message.type === "assistant")
const activeShellIndex = (callID: string) => const activeShellIndex = (callID: string) =>
state.messages.findLastIndex((message) => message.type === "shell" && message.callID === callID) state.messages.findLastIndex((message) => message.type === "shell" && message.callID === callID)
+3 -2
View File
@@ -168,7 +168,8 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
.get() .get()
.pipe(Effect.orDie) .pipe(Effect.orDie)
: undefined : undefined
if (event.data.from && !boundary) return yield* Effect.die(new Error(`Fork boundary message not found: ${event.data.from}`)) if (event.data.from && !boundary)
return yield* Effect.die(new Error(`Fork boundary message not found: ${event.data.from}`))
const copied = yield* db const copied = yield* db
.select({ seq: SessionMessageTable.seq }) .select({ seq: SessionMessageTable.seq })
.from(SessionMessageTable) .from(SessionMessageTable)
@@ -357,7 +358,7 @@ function run(db: DatabaseService, event: MessageEvent) {
const adapter: SessionMessageUpdater.Adapter = { const adapter: SessionMessageUpdater.Adapter = {
getCurrentAssistant() { getCurrentAssistant() {
return Effect.gen(function* () { return Effect.gen(function* () {
// A newer turn supersedes stale incomplete rows; never resume an older assistant projection. // A newer step supersedes stale incomplete rows; never resume an older assistant projection.
const row = yield* db const row = yield* db
.select() .select()
.from(SessionMessageTable) .from(SessionMessageTable)
+3 -7
View File
@@ -9,16 +9,12 @@ import type { SystemContext } from "../../system-context/index"
import type { ToolOutputStore } from "../../tool-output-store" import type { ToolOutputStore } from "../../tool-output-store"
export type RunError = export type RunError =
| LLMError LLMError | SessionRunnerModel.Error | MessageDecodeError | SystemContext.InitializationBlocked | ToolOutputStore.Error
| SessionRunnerModel.Error
| MessageDecodeError
| SystemContext.InitializationBlocked
| ToolOutputStore.Error
/** Runs one local continuation from already-recorded Session history. */ /** Runs one local continuation from already-recorded Session history. */
export interface Interface { export interface Interface {
/** Drains eligible durable work. Explicit runs perform one provider attempt even when no work is eligible. */ /** Drains eligible durable work. Explicit runs perform one physical attempt even when no work is eligible. */
readonly run: (input: { readonly drain: (input: {
readonly sessionID: SessionSchema.ID readonly sessionID: SessionSchema.ID
readonly force: boolean readonly force: boolean
}) => Effect.Effect<void, RunError> }) => Effect.Effect<void, RunError>
+22 -22
View File
@@ -61,11 +61,11 @@ import { llmClient } from "../../effect/app-node-platform"
* - Runtime context assembly * - Runtime context assembly
* - Track V1 runtime-context parity canonically in `specs/v2/session.md`. * - Track V1 runtime-context parity canonically in `specs/v2/session.md`.
* *
* - One provider turn * - One step
* - [x] Translate every projected V2 Session message variant into canonical * - [x] Translate every projected V2 Session message variant into canonical
* `@opencode-ai/llm` messages. * `@opencode-ai/llm` messages.
* - [ ] Resolve policy-filtered built-in, MCP, plugin, and structured-output tool definitions. * - [ ] Resolve policy-filtered built-in, MCP, plugin, and structured-output tool definitions.
* - [x] Stream exactly one `llm.stream(request)` provider turn. * - [x] Stream exactly one `llm.stream(request)` physical attempt.
* - [x] Persist assistant text and usage events incrementally as they arrive. * - [x] Persist assistant text and usage events incrementally as they arrive.
* - [ ] Persist snapshots, patches, and retry notices incrementally as they arrive. * - [ ] Persist snapshots, patches, and retry notices incrementally as they arrive.
* - [x] Persist reasoning, provider errors, and tool-call events incrementally as they arrive. * - [x] Persist reasoning, provider errors, and tool-call events incrementally as they arrive.
@@ -77,8 +77,8 @@ import { llmClient } from "../../effect/app-node-platform"
* - [x] Start each recorded local call eagerly and await all settlements before continuation. * - [x] Start each recorded local call eagerly and await all settlements before continuation.
* - [ ] Add scoped runtime context, progress updates, attachment normalization, * - [ ] Add scoped runtime context, progress updates, attachment normalization,
* plugins, and cancellation settlement. * plugins, and cancellation settlement.
* - [x] Reload projected history and start the next explicit provider turn after local tool results. * - [x] Reload projected history and start the next explicit step after local tool results.
* - [x] Continue for durable user steering accepted during an active provider turn. * - [x] Continue for durable user steering accepted during an active step.
* - [ ] Continue for compaction or another continuation condition when required. * - [ ] Continue for compaction or another continuation condition when required.
* *
* - Post-run maintenance * - Post-run maintenance
@@ -86,12 +86,12 @@ import { llmClient } from "../../effect/app-node-platform"
* - [ ] Coalesce streamed deltas and add covering projected-history indexes. * - [ ] Coalesce streamed deltas and add covering projected-history indexes.
* - [ ] Update title, summaries, compaction state, and cleanup in bounded background work. * - [ ] Update title, summaries, compaction state, and cleanup in bounded background work.
* *
* Use `llm.stream(request)` for each provider turn. Keep tool execution and continuation here. * Use `llm.stream(request)` for each physical attempt. Keep tool execution and continuation here.
* Durable continuation recovery remains a separate future slice with an explicit retry policy. * Durable continuation recovery remains a separate future slice with an explicit retry policy.
* *
* The current slice loads V2 history, translates it, resolves a model through a core service, and persists one * The current slice loads V2 history, translates it, resolves a model through a core service, and persists one
* provider turn. Registry definitions are advertised, local tool calls are settled durably, and an * step. Registry definitions are advertised, local tool calls are settled durably, and an
* explicit loop starts the next provider turn after local settlement. Configured agent step limits bound the loop. * explicit loop starts the next step after local settlement. Configured agent step limits bound the loop.
*/ */
const layer = Layer.effect( const layer = Layer.effect(
@@ -114,7 +114,7 @@ const layer = Layer.effect(
const db = (yield* Database.Service).db const db = (yield* Database.Service).db
const compaction = yield* SessionCompaction.Service const compaction = yield* SessionCompaction.Service
const title = yield* SessionTitle.Service const title = yield* SessionTitle.Service
// Title generation is a side effect of the first turn; it must not delay turn continuation. // Title generation is a side effect of the first step; it must not delay step continuation.
// Tracked per process so repeated wakes before the second user message arrives don't // Tracked per process so repeated wakes before the second user message arrives don't
// 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 titleAttempted = new Set<SessionSchema.ID>() const titleAttempted = new Set<SessionSchema.ID>()
@@ -166,7 +166,7 @@ const layer = Layer.effect(
{ concurrency: "unbounded" }, { concurrency: "unbounded" },
).pipe(Effect.map(SystemContext.combine)) ).pipe(Effect.map(SystemContext.combine))
const runTurnAttempt = Effect.fn("SessionRunner.runTurnAttempt")(function* ( const attemptStep = Effect.fn("SessionRunner.attemptStep")(function* (
sessionID: SessionSchema.ID, sessionID: SessionSchema.ID,
promotion: SessionInput.Delivery | undefined, promotion: SessionInput.Delivery | undefined,
step: number, step: number,
@@ -177,7 +177,7 @@ const layer = Layer.effect(
return yield* Effect.interrupt return yield* Effect.interrupt
const agent = yield* agents.select(session.agent) const agent = yield* agents.select(session.agent)
// 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 turn leaves pending inputs untouched. // a blocked first step leaves pending inputs untouched.
const checkpoint = yield* SessionContextCheckpoint.prepare( const checkpoint = yield* SessionContextCheckpoint.prepare(
db, db,
events, events,
@@ -231,7 +231,7 @@ const layer = Layer.effect(
snapshot: startSnapshot, snapshot: startSnapshot,
}) })
const publication = Semaphore.makeUnsafe(1) const publication = Semaphore.makeUnsafe(1)
// Durable publishes are serialized so tool fibers and turn settlement never interleave // Durable publishes are serialized so tool fibers and step settlement never interleave
// mid-event. // mid-event.
const serialized = <A, E, R>(effect: Effect.Effect<A, E, R>) => publication.withPermit(effect) const serialized = <A, E, R>(effect: Effect.Effect<A, E, R>) => publication.withPermit(effect)
const publish = (event: LLMEvent, outputPaths: ReadonlyArray<string> = []) => const publish = (event: LLMEvent, outputPaths: ReadonlyArray<string> = []) =>
@@ -282,7 +282,7 @@ const layer = Layer.effect(
Effect.ensuring(serialized(publisher.flush())), Effect.ensuring(serialized(publisher.flush())),
) )
// Captures the end snapshot, diffs it against the turn's start, and durably ends the // Captures the end snapshot, diffs it against the step's start, and durably ends the
// assistant step. // assistant step.
const publishStepEnd = (settlement: NonNullable<ReturnType<typeof publisher.stepSettlement>>) => const publishStepEnd = (settlement: NonNullable<ReturnType<typeof publisher.stepSettlement>>) =>
Effect.gen(function* () { Effect.gen(function* () {
@@ -316,7 +316,7 @@ const layer = Layer.effect(
const streamInterrupted = stream._tag === "Failure" && Cause.hasInterrupts(stream.cause) const streamInterrupted = stream._tag === "Failure" && Cause.hasInterrupts(stream.cause)
// A context overflow before any assistant output is recoverable: compact and // A context overflow before any assistant output is recoverable: compact and
// restart the turn instead of surfacing the provider error. // restart the step instead of surfacing the provider error.
if ( if (
recoverOverflow && recoverOverflow &&
!publisher.hasAssistantStarted() && !publisher.hasAssistantStarted() &&
@@ -325,7 +325,7 @@ const layer = Layer.effect(
) )
return { _tag: "RestartAfterOverflowCompaction", step: currentStep } as const return { _tag: "RestartAfterOverflowCompaction", step: currentStep } as const
// An unrecovered held-back overflow becomes the turn's durable provider error. A // An unrecovered held-back overflow becomes the step's durable provider error. A
// thrown LLM failure fails hosted tool calls and the assistant unless a provider // thrown LLM failure fails hosted tool calls and the assistant unless a provider
// error was already recorded from the stream. // error was already recorded from the stream.
if (overflowFailure) yield* publish(overflowFailure) if (overflowFailure) yield* publish(overflowFailure)
@@ -346,12 +346,12 @@ const layer = Layer.effect(
if (questionDismissed || streamInterrupted || toolsInterrupted) { if (questionDismissed || streamInterrupted || toolsInterrupted) {
yield* FiberSet.clear(toolFibers) yield* FiberSet.clear(toolFibers)
yield* serialized(publisher.failUnsettledTools("Tool execution interrupted")) yield* serialized(publisher.failUnsettledTools("Tool execution interrupted"))
yield* serialized(publisher.failAssistant("Provider turn interrupted")) yield* serialized(publisher.failAssistant("Step interrupted"))
// Match V1: dismissing a question halts the loop like an interruption. // Match V1: dismissing a question halts the loop like an interruption.
if (questionDismissed) return yield* Effect.interrupt if (questionDismissed) return yield* Effect.interrupt
} }
// A settled tool fiber failure is one of two things. A defect from a tool // 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 turn still // implementation becomes a failed tool call the model can read, and the step still
// settles so the model may recover. A typed infrastructure failure (tool output // 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. // could not be persisted) also fails the assistant and then fails the drain.
const settledFailure = settled._tag === "Failure" && !toolsInterrupted ? settled.cause : undefined const settledFailure = settled._tag === "Failure" && !toolsInterrupted ? settled.cause : undefined
@@ -387,7 +387,7 @@ const layer = Layer.effect(
) )
}, Effect.scoped) }, Effect.scoped)
const runTurn = Effect.fnUntraced(function* ( const runStep = Effect.fnUntraced(function* (
sessionID: SessionSchema.ID, sessionID: SessionSchema.ID,
promotion: SessionInput.Delivery | undefined, promotion: SessionInput.Delivery | undefined,
step: number, step: number,
@@ -399,7 +399,7 @@ const layer = Layer.effect(
let currentPromotion = promotion let currentPromotion = promotion
let currentStep = step let currentStep = step
while (true) { while (true) {
const attempt = yield* runTurnAttempt(sessionID, currentPromotion, currentStep, recoverOverflow) const attempt = yield* attemptStep(sessionID, currentPromotion, currentStep, recoverOverflow)
if (attempt._tag === "Completed") return { needsContinuation: attempt.needsContinuation, step: attempt.step } if (attempt._tag === "Completed") return { needsContinuation: attempt.needsContinuation, step: attempt.step }
if (attempt._tag === "RestartAfterOverflowCompaction") recoverOverflow = undefined if (attempt._tag === "RestartAfterOverflowCompaction") recoverOverflow = undefined
yield* Effect.yieldNow yield* Effect.yieldNow
@@ -410,7 +410,7 @@ const layer = Layer.effect(
// ExecutionSettled is published per execution (busy period) by SessionExecution, not per // ExecutionSettled is published per execution (busy period) by SessionExecution, not per
// drain here. // drain here.
const run = Effect.fn("SessionRunner.run")(function* (input: { const drain = Effect.fn("SessionRunner.drain")(function* (input: {
readonly sessionID: SessionSchema.ID readonly sessionID: SessionSchema.ID
readonly force: boolean readonly force: boolean
}) { }) {
@@ -428,8 +428,8 @@ const layer = Layer.effect(
// a provider error suppresses it. Pending steers also continue the loop so // a provider error suppresses it. Pending steers also continue the loop so
// interjections are answered before the session goes idle. // interjections are answered before the session goes idle.
while (needsContinuation) { while (needsContinuation) {
const result = yield* runTurn(input.sessionID, promotion, step) const result = yield* runStep(input.sessionID, promotion, step)
// Steer/queue promotion inside runTurn has already made the pending input a visible // Steer/queue promotion inside runStep has already made the pending input a visible
// user message by this point, so the first-user-message check below is reliable. // user message by this point, so the first-user-message check below is reliable.
if (!titleAttempted.has(input.sessionID)) { if (!titleAttempted.has(input.sessionID)) {
titleAttempted.add(input.sessionID) titleAttempted.add(input.sessionID)
@@ -445,7 +445,7 @@ const layer = Layer.effect(
} }
}) })
return Service.of({ run }) return Service.of({ drain })
}), }),
) )
@@ -50,7 +50,7 @@ const settledOutput = (value: ToolOutput | undefined, result: ToolResultValue):
return { structured: record(settled.structured), content: settled.content } return { structured: record(settled.structured), content: settled.content }
} }
/** Persist one provider turn without executing tools or starting a continuation turn. */ /** Persist one step without executing tools or starting a continuation step. */
export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) => { export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) => {
const tools = new Map< const tools = new Map<
string, string,
+1 -1
View File
@@ -12,7 +12,7 @@ import { Effect, Option, Schema } from "effect"
* The durable `Applied` record tracks what the model was last told, per source: * The durable `Applied` record tracks what the model was last told, per source:
* it is the model's current belief. Interpreters uphold one invariant * it is the model's current belief. Interpreters uphold one invariant
* `reconcile` never rewrites the baseline; it only narrates drift as update * `reconcile` never rewrites the baseline; it only narrates drift as update
* text. Only `rebaseline` (compaction) and `initialize` (first turn) produce * text. Only `rebaseline` (compaction) and `initialize` (first step) produce
* baseline text. * baseline text.
* *
* Returning `unavailable` means observation failed temporarily. It differs from * Returning `unavailable` means observation failed temporarily. It differs from
@@ -354,7 +354,7 @@ Recent work
state: SessionMessage.ToolStateError.make({ state: SessionMessage.ToolStateError.make({
status: "error", status: "error",
input: { query: "Effect" }, input: { query: "Effect" },
error: { type: "unknown", message: "Provider turn interrupted" }, error: { type: "unknown", message: "Step interrupted" },
content: [], content: [],
structured: {}, structured: {},
}), }),
@@ -362,7 +362,7 @@ Recent work
}), }),
], ],
finish: "error", finish: "error",
error: { type: "unknown", message: "Provider turn interrupted" }, error: { type: "unknown", message: "Step interrupted" },
time: { created, completed: created }, time: { created, completed: created },
}), }),
], ],
@@ -386,7 +386,7 @@ Recent work
result: { result: {
type: "error", type: "error",
value: { value: {
error: { type: "unknown", message: "Provider turn interrupted" }, error: { type: "unknown", message: "Step interrupted" },
content: [], content: [],
structured: {}, structured: {},
}, },
@@ -98,7 +98,7 @@ const execution = Layer.effect(
Effect.gen(function* () { Effect.gen(function* () {
const sessionRunner = yield* SessionRunner.Service const sessionRunner = yield* SessionRunner.Service
const coordinator = yield* SessionRunCoordinator.make<SessionV2.ID, SessionRunner.RunError>({ const coordinator = yield* SessionRunCoordinator.make<SessionV2.ID, SessionRunner.RunError>({
drain: (sessionID, force) => sessionRunner.run({ sessionID, force }), drain: (sessionID, force) => sessionRunner.drain({ sessionID, force }),
}) })
return SessionExecution.Service.of({ return SessionExecution.Service.of({
active: coordinator.active, active: coordinator.active,
+6 -6
View File
@@ -260,7 +260,7 @@ const execution = Layer.effect(
Effect.gen(function* () { Effect.gen(function* () {
const sessionRunner = yield* SessionRunner.Service const sessionRunner = yield* SessionRunner.Service
const coordinator = yield* SessionRunCoordinator.make<SessionV2.ID, SessionRunner.RunError>({ const coordinator = yield* SessionRunCoordinator.make<SessionV2.ID, SessionRunner.RunError>({
drain: (sessionID, force) => sessionRunner.run({ sessionID, force }), drain: (sessionID, force) => sessionRunner.drain({ sessionID, force }),
}) })
return SessionExecution.Service.of({ return SessionExecution.Service.of({
active: coordinator.active, active: coordinator.active,
@@ -575,7 +575,7 @@ const verifyPartialFlushOnInterruption = (kind: FragmentKind) =>
) )
const runner = yield* SessionRunner.Service const runner = yield* SessionRunner.Service
const fiber = yield* runner.run({ sessionID, force: true }).pipe(Effect.forkChild) const fiber = yield* runner.drain({ sessionID, force: true }).pipe(Effect.forkChild)
yield* Deferred.await(streamed) yield* Deferred.await(streamed)
yield* Fiber.interrupt(fiber) yield* Fiber.interrupt(fiber)
expect(yield* session.context(sessionID)).toMatchObject([ expect(yield* session.context(sessionID)).toMatchObject([
@@ -583,7 +583,7 @@ const verifyPartialFlushOnInterruption = (kind: FragmentKind) =>
{ {
type: "assistant", type: "assistant",
finish: "error", finish: "error",
error: { type: "unknown", message: "Provider turn interrupted" }, error: { type: "unknown", message: "Step interrupted" },
content: [ content: [
kind === "tool input" kind === "tool input"
? { type: "tool", id: fragmentID(kind, "interrupted"), state: { status: "error" } } ? { type: "tool", id: fragmentID(kind, "interrupted"), state: { status: "error" } }
@@ -2983,7 +2983,7 @@ describe("SessionRunnerLLM", () => {
expect(requests).toHaveLength(1) expect(requests).toHaveLength(1)
expect(yield* session.context(sessionID)).toMatchObject([ expect(yield* session.context(sessionID)).toMatchObject([
{ type: "user", text: "Interrupt provider" }, { type: "user", text: "Interrupt provider" },
{ type: "assistant", finish: "error", error: { type: "unknown", message: "Provider turn interrupted" } }, { type: "assistant", finish: "error", error: { type: "unknown", message: "Step interrupted" } },
]) ])
expect(yield* recordedEventTypes(sessionID)).toContain("step.failed.1") expect(yield* recordedEventTypes(sessionID)).toContain("step.failed.1")
yield* session.interrupt(sessionID) yield* session.interrupt(sessionID)
@@ -3007,7 +3007,7 @@ describe("SessionRunnerLLM", () => {
] ]
const runner = yield* SessionRunner.Service const runner = yield* SessionRunner.Service
const run = yield* runner.run({ sessionID, force: true }).pipe(Effect.forkChild) const run = yield* runner.drain({ sessionID, force: true }).pipe(Effect.forkChild)
yield* Deferred.await(toolExecutionsStarted) yield* Deferred.await(toolExecutionsStarted)
yield* Fiber.interrupt(run) yield* Fiber.interrupt(run)
toolExecutionGate = undefined toolExecutionGate = undefined
@@ -3018,7 +3018,7 @@ describe("SessionRunnerLLM", () => {
{ {
type: "assistant", type: "assistant",
finish: "error", finish: "error",
error: { type: "unknown", message: "Provider turn interrupted" }, error: { type: "unknown", message: "Step interrupted" },
content: [ content: [
{ {
type: "tool", type: "tool",
+1 -1
View File
@@ -42,7 +42,7 @@ Execution routing starts from only the Session ID:
SessionExecution.resume(sessionID) SessionExecution.resume(sessionID)
-> SessionStore.get(sessionID) -> SessionStore.get(sessionID)
-> LocationServiceMap.get(session.location) -> LocationServiceMap.get(session.location)
-> SessionRunner.run({ sessionID, force? }) -> SessionRunner.drain({ sessionID, force? })
``` ```
`SessionExecution` and the read-side `SessionStore` are process-global. `SessionRunner`, catalog, model resolver, tool registry, permission state, and filesystem are cached per Location. No layer takes a Session ID. An omitted `Location.workspaceID` means implicit-local placement; explicit workspace identity remains reserved for future placement semantics. `SessionExecution` and the read-side `SessionStore` are process-global. `SessionRunner`, catalog, model resolver, tool registry, permission state, and filesystem are cached per Location. No layer takes a Session ID. An omitted `Location.workspaceID` means implicit-local placement; explicit workspace identity remains reserved for future placement semantics.