refactor(core): align runner naming with step vocabulary (#35227)
This commit is contained in:
parent
e66cbf36e9
commit
5d14c7a185
14 changed files with 47 additions and 50 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue