소스 검색

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

Kit Langton 1 개월 전
부모
커밋
5d14c7a185

+ 1 - 1
packages/core/src/session/context-checkpoint.ts

@@ -18,7 +18,7 @@ const decodeApplied = Schema.decodeUnknownOption(SystemContext.Applied)
  * Loads or creates the session's durable context checkpoint, narrating any
  * drift since the model was last told as a chronological update. Completed
  * 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* (
   db: DatabaseService,

+ 1 - 1
packages/core/src/session/execution/local.ts

@@ -20,7 +20,7 @@ const layer = Layer.effect(
       drain: Effect.fnUntraced(function* (sessionID: SessionSchema.ID, force) {
         const session = yield* store.get(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.tapCause((cause) =>
             Cause.hasInterruptsOnly(cause)

+ 1 - 1
packages/core/src/session/history.ts

@@ -34,7 +34,7 @@ const messageRows = Effect.fnUntraced(function* (
       and(
         eq(SessionMessageTable.session_id, sessionID),
         // 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.
         compaction
           ? or(

+ 2 - 2
packages/core/src/session/instructions.ts

@@ -36,9 +36,9 @@ const layer = Layer.effect(
     // 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.
     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
-    // 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 load = Effect.fn("SessionInstructions.load")(function* (input: {

+ 1 - 1
packages/core/src/session/message-updater.ts

@@ -19,7 +19,7 @@ export interface Adapter {
 export function memory(state: MemoryState): Adapter {
   const assistantIndex = (messageID: SessionMessage.ID) =>
     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 activeShellIndex = (callID: string) =>
     state.messages.findLastIndex((message) => message.type === "shell" && message.callID === callID)

+ 3 - 2
packages/core/src/session/projector.ts

@@ -168,7 +168,8 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
         .get()
         .pipe(Effect.orDie)
     : 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
     .select({ seq: SessionMessageTable.seq })
     .from(SessionMessageTable)
@@ -357,7 +358,7 @@ function run(db: DatabaseService, event: MessageEvent) {
     const adapter: SessionMessageUpdater.Adapter = {
       getCurrentAssistant() {
         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
             .select()
             .from(SessionMessageTable)

+ 3 - 7
packages/core/src/session/runner/index.ts

@@ -9,16 +9,12 @@ import type { SystemContext } from "../../system-context/index"
 import type { ToolOutputStore } from "../../tool-output-store"
 
 export type RunError =
-  | LLMError
-  | SessionRunnerModel.Error
-  | MessageDecodeError
-  | SystemContext.InitializationBlocked
-  | ToolOutputStore.Error
+  LLMError | SessionRunnerModel.Error | MessageDecodeError | SystemContext.InitializationBlocked | ToolOutputStore.Error
 
 /** Runs one local continuation from already-recorded Session history. */
 export interface Interface {
-  /** Drains eligible durable work. Explicit runs perform one provider attempt even when no work is eligible. */
-  readonly run: (input: {
+  /** Drains eligible durable work. Explicit runs perform one physical attempt even when no work is eligible. */
+  readonly drain: (input: {
     readonly sessionID: SessionSchema.ID
     readonly force: boolean
   }) => Effect.Effect<void, RunError>

+ 22 - 22
packages/core/src/session/runner/llm.ts

@@ -61,11 +61,11 @@ import { llmClient } from "../../effect/app-node-platform"
  * - Runtime context assembly
  *   - 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
  *     `@opencode-ai/llm` messages.
  *   - [ ] 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.
  *   - [ ] Persist snapshots, patches, and retry notices 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.
  *   - [ ] Add scoped runtime context, progress updates, attachment normalization,
  *     plugins, and cancellation settlement.
- *   - [x] Reload projected history and start the next explicit provider turn after local tool results.
- *   - [x] Continue for durable user steering accepted during an active provider turn.
+ *   - [x] Reload projected history and start the next explicit step after local tool results.
+ *   - [x] Continue for durable user steering accepted during an active step.
  *   - [ ] Continue for compaction or another continuation condition when required.
  *
  * - Post-run maintenance
@@ -86,12 +86,12 @@ import { llmClient } from "../../effect/app-node-platform"
  *   - [ ] Coalesce streamed deltas and add covering projected-history indexes.
  *   - [ ] 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.
  *
  * 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
- * explicit loop starts the next provider turn after local settlement. Configured agent step limits bound the loop.
+ * step. Registry definitions are advertised, local tool calls are settled durably, and an
+ * explicit loop starts the next step after local settlement. Configured agent step limits bound the loop.
  */
 
 const layer = Layer.effect(
@@ -114,7 +114,7 @@ const layer = Layer.effect(
     const db = (yield* Database.Service).db
     const compaction = yield* SessionCompaction.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
     // re-fire a redundant LLM call; `SessionTitle` itself is idempotent based on durable history.
     const titleAttempted = new Set<SessionSchema.ID>()
@@ -166,7 +166,7 @@ const layer = Layer.effect(
         { concurrency: "unbounded" },
       ).pipe(Effect.map(SystemContext.combine))
 
-    const runTurnAttempt = Effect.fn("SessionRunner.runTurnAttempt")(function* (
+    const attemptStep = Effect.fn("SessionRunner.attemptStep")(function* (
       sessionID: SessionSchema.ID,
       promotion: SessionInput.Delivery | undefined,
       step: number,
@@ -177,7 +177,7 @@ const layer = Layer.effect(
         return yield* Effect.interrupt
       const agent = yield* agents.select(session.agent)
       // 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(
         db,
         events,
@@ -231,7 +231,7 @@ const layer = Layer.effect(
         snapshot: startSnapshot,
       })
       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.
       const serialized = <A, E, R>(effect: Effect.Effect<A, E, R>) => publication.withPermit(effect)
       const publish = (event: LLMEvent, outputPaths: ReadonlyArray<string> = []) =>
@@ -282,7 +282,7 @@ const layer = Layer.effect(
         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.
       const publishStepEnd = (settlement: NonNullable<ReturnType<typeof publisher.stepSettlement>>) =>
         Effect.gen(function* () {
@@ -316,7 +316,7 @@ const layer = Layer.effect(
           const streamInterrupted = stream._tag === "Failure" && Cause.hasInterrupts(stream.cause)
 
           // 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 (
             recoverOverflow &&
             !publisher.hasAssistantStarted() &&
@@ -325,7 +325,7 @@ const layer = Layer.effect(
           )
             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
           // error was already recorded from the stream.
           if (overflowFailure) yield* publish(overflowFailure)
@@ -346,12 +346,12 @@ const layer = Layer.effect(
           if (questionDismissed || streamInterrupted || toolsInterrupted) {
             yield* FiberSet.clear(toolFibers)
             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.
             if (questionDismissed) 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 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
           // could not be persisted) also fails the assistant and then fails the drain.
           const settledFailure = settled._tag === "Failure" && !toolsInterrupted ? settled.cause : undefined
@@ -387,7 +387,7 @@ const layer = Layer.effect(
       )
     }, Effect.scoped)
 
-    const runTurn = Effect.fnUntraced(function* (
+    const runStep = Effect.fnUntraced(function* (
       sessionID: SessionSchema.ID,
       promotion: SessionInput.Delivery | undefined,
       step: number,
@@ -399,7 +399,7 @@ const layer = Layer.effect(
       let currentPromotion = promotion
       let currentStep = step
       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 === "RestartAfterOverflowCompaction") recoverOverflow = undefined
         yield* Effect.yieldNow
@@ -410,7 +410,7 @@ const layer = Layer.effect(
 
     // ExecutionSettled is published per execution (busy period) by SessionExecution, not per
     // drain here.
-    const run = Effect.fn("SessionRunner.run")(function* (input: {
+    const drain = Effect.fn("SessionRunner.drain")(function* (input: {
       readonly sessionID: SessionSchema.ID
       readonly force: boolean
     }) {
@@ -428,8 +428,8 @@ const layer = Layer.effect(
         // a provider error suppresses it. Pending steers also continue the loop so
         // interjections are answered before the session goes idle.
         while (needsContinuation) {
-          const result = yield* runTurn(input.sessionID, promotion, step)
-          // Steer/queue promotion inside runTurn has already made the pending input a visible
+          const result = yield* runStep(input.sessionID, promotion, step)
+          // 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.
           if (!titleAttempted.has(input.sessionID)) {
             titleAttempted.add(input.sessionID)
@@ -445,7 +445,7 @@ const layer = Layer.effect(
       }
     })
 
-    return Service.of({ run })
+    return Service.of({ drain })
   }),
 )
 

+ 1 - 1
packages/core/src/session/runner/publish-llm-event.ts

@@ -50,7 +50,7 @@ const settledOutput = (value: ToolOutput | undefined, result: ToolResultValue):
   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) => {
   const tools = new Map<
     string,

+ 1 - 1
packages/core/src/system-context/index.ts

@@ -12,7 +12,7 @@ import { Effect, Option, Schema } from "effect"
  * The durable `Applied` record tracks what the model was last told, per source:
  * it is the model's current belief. Interpreters uphold one invariant —
  * `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.
  *
  * Returning `unavailable` means observation failed temporarily. It differs from

+ 3 - 3
packages/core/test/session-runner-message.test.ts

@@ -354,7 +354,7 @@ Recent work
               state: SessionMessage.ToolStateError.make({
                 status: "error",
                 input: { query: "Effect" },
-                error: { type: "unknown", message: "Provider turn interrupted" },
+                error: { type: "unknown", message: "Step interrupted" },
                 content: [],
                 structured: {},
               }),
@@ -362,7 +362,7 @@ Recent work
             }),
           ],
           finish: "error",
-          error: { type: "unknown", message: "Provider turn interrupted" },
+          error: { type: "unknown", message: "Step interrupted" },
           time: { created, completed: created },
         }),
       ],
@@ -386,7 +386,7 @@ Recent work
         result: {
           type: "error",
           value: {
-            error: { type: "unknown", message: "Provider turn interrupted" },
+            error: { type: "unknown", message: "Step interrupted" },
             content: [],
             structured: {},
           },

+ 1 - 1
packages/core/test/session-runner-recorded.test.ts

@@ -98,7 +98,7 @@ const execution = Layer.effect(
   Effect.gen(function* () {
     const sessionRunner = yield* SessionRunner.Service
     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({
       active: coordinator.active,

+ 6 - 6
packages/core/test/session-runner.test.ts

@@ -260,7 +260,7 @@ const execution = Layer.effect(
   Effect.gen(function* () {
     const sessionRunner = yield* SessionRunner.Service
     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({
       active: coordinator.active,
@@ -575,7 +575,7 @@ const verifyPartialFlushOnInterruption = (kind: FragmentKind) =>
     )
 
     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* Fiber.interrupt(fiber)
     expect(yield* session.context(sessionID)).toMatchObject([
@@ -583,7 +583,7 @@ const verifyPartialFlushOnInterruption = (kind: FragmentKind) =>
       {
         type: "assistant",
         finish: "error",
-        error: { type: "unknown", message: "Provider turn interrupted" },
+        error: { type: "unknown", message: "Step interrupted" },
         content: [
           kind === "tool input"
             ? { type: "tool", id: fragmentID(kind, "interrupted"), state: { status: "error" } }
@@ -2983,7 +2983,7 @@ describe("SessionRunnerLLM", () => {
       expect(requests).toHaveLength(1)
       expect(yield* session.context(sessionID)).toMatchObject([
         { 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")
       yield* session.interrupt(sessionID)
@@ -3007,7 +3007,7 @@ describe("SessionRunnerLLM", () => {
       ]
 
       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* Fiber.interrupt(run)
       toolExecutionGate = undefined
@@ -3018,7 +3018,7 @@ describe("SessionRunnerLLM", () => {
         {
           type: "assistant",
           finish: "error",
-          error: { type: "unknown", message: "Provider turn interrupted" },
+          error: { type: "unknown", message: "Step interrupted" },
           content: [
             {
               type: "tool",

+ 1 - 1
specs/v2/session.md

@@ -42,7 +42,7 @@ Execution routing starts from only the Session ID:
 SessionExecution.resume(sessionID)
 -> SessionStore.get(sessionID)
 -> 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.