From f57e0a21344a6b0b36329341f0fea5121e3cb9ad Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 3 Jul 2026 16:46:17 -0400 Subject: [PATCH] refactor(core): derive continuation and held overflow from step ledger --- packages/core/src/session/runner/llm.ts | 26 ++++++------------- .../core/src/session/runner/step-ledger.ts | 22 +++++++++++++++- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index 7a1f76265e..9b3ab38941 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -1,15 +1,6 @@ export * as SessionRunnerLLM from "./llm" -import { - LLM, - LLMClient, - LLMError, - LLMEvent, - Message, - SystemPart, - isContextOverflowFailure, - type ProviderErrorEvent, -} from "@opencode-ai/llm" +import { LLM, LLMClient, LLMError, LLMEvent, Message, SystemPart, isContextOverflowFailure } from "@opencode-ai/llm" import { Cause, DateTime, Effect, Exit, FiberSet, Layer, Option, Semaphore, Stream } from "effect" import { AgentV2 } from "../../agent" import { Config } from "../../config" @@ -185,7 +176,6 @@ const layer = Layer.effect( session.id, ) const toolFibers = yield* FiberSet.make() - let needsContinuation = false let currentStep = step if (promotion) { let promoted = 0 @@ -236,14 +226,13 @@ const layer = Layer.effect( const serialized = (effect: Effect.Effect) => publication.withPermit(effect) const publish = (event: LLMEvent, outputPaths: ReadonlyArray = []) => serialized(ledger.publish(event, outputPaths)) - let overflowFailure: ProviderErrorEvent | undefined const providerStream = llm.stream(request).pipe( Stream.runForEach((event) => Effect.gen(function* () { - if (overflowFailure || ledger.hasProviderError()) return + if (ledger.heldOverflow() || ledger.hasProviderError()) return if (LLMEvent.is.providerError(event)) { if (isContextOverflowFailure(event) && !ledger.hasAssistantStarted()) { - overflowFailure = event + ledger.holdOverflow(event) return } } @@ -253,7 +242,7 @@ const layer = Layer.effect( yield* serialized(ledger.failUnsettledTools("Tools are disabled after the maximum agent steps")) return } - needsContinuation = true + ledger.admitLocalToolCall(event.id) const assistantMessageID = yield* ledger.assistantMessageID(event.id) yield* Effect.uninterruptibleMask((restore) => restore( @@ -320,7 +309,7 @@ const layer = Layer.effect( if ( recoverOverflow && !ledger.hasAssistantStarted() && - isContextOverflowFailure(overflowFailure ?? streamFailure) && + isContextOverflowFailure(ledger.heldOverflow() ?? streamFailure) && (yield* restore(recoverOverflow({ sessionID: session.id, messages: context, request }))) ) return { _tag: "RestartAfterOverflowCompaction", step: currentStep } as const @@ -328,7 +317,8 @@ const layer = Layer.effect( // 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) + const heldOverflow = ledger.heldOverflow() + if (heldOverflow) yield* publish(heldOverflow) const llmFailure = streamFailure instanceof LLMError ? streamFailure : undefined if (llmFailure && !ledger.hasProviderError()) { yield* serialized(ledger.failUnsettledTools("Provider did not return a tool result", true)) @@ -379,7 +369,7 @@ const layer = Layer.effect( return yield* Effect.failCause(settled.cause) return { _tag: "Completed", - needsContinuation: !providerFailed && needsContinuation, + needsContinuation: !providerFailed && ledger.hasLocalToolCalls(), step: currentStep, } as const }), diff --git a/packages/core/src/session/runner/step-ledger.ts b/packages/core/src/session/runner/step-ledger.ts index d94e21e383..d05fc08ce5 100644 --- a/packages/core/src/session/runner/step-ledger.ts +++ b/packages/core/src/session/runner/step-ledger.ts @@ -1,4 +1,11 @@ -import { ToolOutput, type LLMEvent, type ProviderMetadata, type ToolResultValue, type Usage } from "@opencode-ai/llm" +import { + ToolOutput, + type LLMEvent, + type ProviderErrorEvent, + type ProviderMetadata, + type ToolResultValue, + type Usage, +} from "@opencode-ai/llm" import { DateTime, Effect } from "effect" import { EventV2 } from "../../event" import { ModelV2 } from "../../model" @@ -69,7 +76,9 @@ export const createStepLedger = (events: EventV2.Interface, input: Input) => { let assistantActive = false let assistantFailed = false let providerFailed = false + let heldOverflowEvent: ProviderErrorEvent | undefined let stepSettlement: { readonly finish: string; readonly tokens: ReturnType } | undefined + const localToolCalls = new Set() const startAssistant = Effect.fnUntraced(function* () { if (assistantMessageID !== undefined) return assistantMessageID @@ -229,6 +238,11 @@ export const createStepLedger = (events: EventV2.Interface, input: Input) => { return tool ? Effect.succeed(tool.assistantMessageID) : Effect.die(new Error(`Unknown tool call: ${callID}`)) } + const admitLocalToolCall = (callID: string) => { + const tool = tools.get(callID) + if (tool?.called && !tool.providerExecuted) localToolCalls.add(callID) + } + const publish = Effect.fn("SessionRunner.publishLLMEvent")(function* ( event: LLMEvent, outputPaths: ReadonlyArray = [], @@ -397,8 +411,14 @@ export const createStepLedger = (events: EventV2.Interface, input: Input) => { flush, failAssistant, failUnsettledTools, + admitLocalToolCall, + holdOverflow: (event: ProviderErrorEvent) => { + heldOverflowEvent = event + }, + heldOverflow: () => heldOverflowEvent, hasActiveAssistant: () => assistantActive, hasAssistantStarted: () => assistantMessageID !== undefined, + hasLocalToolCalls: () => localToolCalls.size > 0, hasProviderError: () => providerFailed, stepSettlement: () => stepSettlement, startAssistant,