refactor(core): simplify session runner bookkeeping (#36200)

This commit is contained in:
Kit Langton
2026-07-10 09:46:34 -04:00
committed by GitHub
parent 0d6ccd2a50
commit 278c510549
4 changed files with 11 additions and 28 deletions
+1 -1
View File
@@ -581,7 +581,7 @@ const layer = Layer.effect(
yield* shellLocks.withLock(input.sessionID)( yield* shellLocks.withLock(input.sessionID)(
Effect.gen(function* () { Effect.gen(function* () {
activeShells.add(input.sessionID) activeShells.add(input.sessionID)
if ((yield* execution.active).has(input.sessionID)) yield* execution.awaitIdle(input.sessionID) yield* execution.awaitIdle(input.sessionID)
const started = yield* Effect.gen(function* () { const started = yield* Effect.gen(function* () {
const shell = yield* Shell.Service const shell = yield* Shell.Service
return yield* shell.create({ command: input.command, cwd: session.location.directory, timeout: 0 }) return yield* shell.create({ command: input.command, cwd: session.location.directory, timeout: 0 })
+2 -5
View File
@@ -14,7 +14,6 @@ import { SessionError } from "@opencode-ai/schema/session-error"
import { Money } from "@opencode-ai/schema/money" import { Money } from "@opencode-ai/schema/money"
import { Cause, Effect, Exit, Fiber, FiberSet, Layer, Option, Semaphore, Stream } from "effect" import { Cause, Effect, Exit, Fiber, FiberSet, Layer, Option, Semaphore, Stream } from "effect"
import { AgentV2 } from "../../agent" import { AgentV2 } from "../../agent"
import { Config } from "../../config"
import { Database } from "../../database/database" import { Database } from "../../database/database"
import { EventV2 } from "../../event" import { EventV2 } from "../../event"
import { Location } from "../../location" import { Location } from "../../location"
@@ -209,11 +208,10 @@ const layer = Layer.effect(
const toolFibers = yield* FiberSet.make<void, ToolOutputStore.Error>() const toolFibers = yield* FiberSet.make<void, ToolOutputStore.Error>()
const ownedToolFibers: Array<Fiber.Fiber<void, ToolOutputStore.Error>> = [] const ownedToolFibers: Array<Fiber.Fiber<void, ToolOutputStore.Error>> = []
let needsContinuation = false let needsContinuation = false
const hookedRequest = request
// Automatic compaction completed; rebuild the request from compacted history. // Automatic compaction completed; rebuild the request from compacted history.
if ( if (
!(yield* SessionPending.compaction(db, session.id)) && !(yield* SessionPending.compaction(db, session.id)) &&
(yield* compaction.compactIfNeeded({ sessionID: session.id, messages: context, request: hookedRequest })) (yield* compaction.compactIfNeeded({ sessionID: session.id, messages: context, request }))
) )
return { _tag: "RestartAfterCompaction", step: currentStep } as const return { _tag: "RestartAfterCompaction", step: currentStep } as const
const startSnapshot = yield* snapshots.capture() const startSnapshot = yield* snapshots.capture()
@@ -233,7 +231,7 @@ const layer = Layer.effect(
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, error?: SessionError.Error) => serialized(publisher.publish(event, error)) const publish = (event: LLMEvent, error?: SessionError.Error) => serialized(publisher.publish(event, error))
let overflowFailure: ProviderErrorEvent | undefined let overflowFailure: ProviderErrorEvent | undefined
const providerStream = llm.stream(hookedRequest).pipe( const providerStream = llm.stream(request).pipe(
Stream.runForEach((event) => Stream.runForEach((event) =>
Effect.gen(function* () { Effect.gen(function* () {
if (overflowFailure || publisher.hasProviderError()) return if (overflowFailure || publisher.hasProviderError()) return
@@ -594,7 +592,6 @@ export const node = makeLocationNode({
InstructionEntry.node, InstructionEntry.node,
SessionCompaction.node, SessionCompaction.node,
SessionTitle.node, SessionTitle.node,
Config.node,
Snapshot.node, Snapshot.node,
Database.node, Database.node,
PluginSupervisor.node, PluginSupervisor.node,
@@ -57,13 +57,12 @@ const settledOutput = (value: ToolOutput | undefined, result: ToolResultValue):
} }
/** Persist one step without executing tools or starting a continuation step. */ /** Persist one step without executing tools or starting a continuation step. */
export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) => { export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish">, input: Input) => {
const tools = new Map< const tools = new Map<
string, string,
{ {
readonly assistantMessageID: SessionMessage.ID readonly assistantMessageID: SessionMessage.ID
readonly name: string readonly name: string
inputEnded: boolean
called: boolean called: boolean
settled: boolean settled: boolean
providerExecuted: boolean providerExecuted: boolean
@@ -140,7 +139,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
const flush = Effect.fnUntraced(function* () { const flush = Effect.fnUntraced(function* () {
for (const id of chunks.keys()) yield* end(id) for (const id of chunks.keys()) yield* end(id)
}) })
return { start, append, end, flush } return { start, append, end, flush, has: (id: string) => chunks.has(id) }
} }
const text = fragments( const text = fragments(
@@ -180,7 +179,6 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
callID, callID,
text: value, text: value,
}) })
tool.inputEnded = true
}), }),
) )
@@ -196,7 +194,6 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
tools.set(event.id, { tools.set(event.id, {
assistantMessageID, assistantMessageID,
name: event.name, name: event.name,
inputEnded: false,
called: false, called: false,
settled: false, settled: false,
providerExecuted: false, providerExecuted: false,
@@ -215,7 +212,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
if (!tool) return yield* Effect.die(new Error(`Tool input end before start: ${event.id}`)) if (!tool) return yield* Effect.die(new Error(`Tool input end before start: ${event.id}`))
if (tool.name !== event.name) if (tool.name !== event.name)
return yield* Effect.die(new Error(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`)) return yield* Effect.die(new Error(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`))
if (tool.inputEnded) return yield* Effect.die(new Error(`Duplicate tool input end: ${event.id}`)) if (!toolInput.has(event.id)) return yield* Effect.die(new Error(`Duplicate tool input end: ${event.id}`))
yield* toolInput.end(event.id) yield* toolInput.end(event.id)
}) })
@@ -330,7 +327,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
if (!tool) return yield* Effect.die(new Error(`Tool input delta before start: ${event.id}`)) if (!tool) return yield* Effect.die(new Error(`Tool input delta before start: ${event.id}`))
if (tool.name !== event.name) if (tool.name !== event.name)
return yield* Effect.die(new Error(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`)) return yield* Effect.die(new Error(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`))
if (tool.inputEnded) return yield* Effect.die(new Error(`Tool input delta after end: ${event.id}`)) if (!toolInput.has(event.id)) return yield* Effect.die(new Error(`Tool input delta after end: ${event.id}`))
yield* toolInput.append(event.id, event.text) yield* toolInput.append(event.id, event.text)
yield* events.publish(SessionEvent.Tool.Input.Delta, { yield* events.publish(SessionEvent.Tool.Input.Delta, {
sessionID: input.sessionID, sessionID: input.sessionID,
@@ -347,7 +344,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
retryEvidence = true retryEvidence = true
if (!tools.has(event.id)) yield* startToolInput(event) if (!tools.has(event.id)) yield* startToolInput(event)
const tool = tools.get(event.id)! const tool = tools.get(event.id)!
if (!tool.inputEnded) yield* endToolInput(event) if (toolInput.has(event.id)) yield* endToolInput(event)
if (tool.name !== event.name) if (tool.name !== event.name)
return yield* Effect.die(new Error(`Tool call name changed for ${event.id}: ${tool.name} -> ${event.name}`)) return yield* Effect.die(new Error(`Tool call name changed for ${event.id}: ${tool.name} -> ${event.name}`))
if (tool.called) return yield* Effect.die(new Error(`Duplicate tool call: ${event.id}`)) if (tool.called) return yield* Effect.die(new Error(`Duplicate tool call: ${event.id}`))
@@ -364,7 +361,6 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
return return
} }
case "tool-result": { case "tool-result": {
retryEvidence = true
const tool = tools.get(event.id) const tool = tools.get(event.id)
if (!tool?.called) return yield* Effect.die(new Error(`Tool result before call: ${event.id}`)) if (!tool?.called) return yield* Effect.die(new Error(`Tool result before call: ${event.id}`))
if (tool.name !== event.name) if (tool.name !== event.name)
@@ -401,7 +397,6 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
return return
} }
case "tool-error": { case "tool-error": {
retryEvidence = true
const tool = tools.get(event.id) const tool = tools.get(event.id)
if (!tool?.called) return yield* Effect.die(new Error(`Tool error before call: ${event.id}`)) if (!tool?.called) return yield* Effect.die(new Error(`Tool error before call: ${event.id}`))
if (tool.name !== event.name) if (tool.name !== event.name)
@@ -1,5 +1,5 @@
import { expect, test } from "bun:test" import { expect, test } from "bun:test"
import { Effect, Schema, Stream } from "effect" import { Effect, Schema } from "effect"
import { LLMEvent } from "@opencode-ai/llm" import { LLMEvent } from "@opencode-ai/llm"
import { Money } from "@opencode-ai/schema/money" import { Money } from "@opencode-ai/schema/money"
import { EventV2 } from "@opencode-ai/core/event" import { EventV2 } from "@opencode-ai/core/event"
@@ -16,7 +16,7 @@ const base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
const capture = (providerMetadataKey = "anthropic") => { const capture = (providerMetadataKey = "anthropic") => {
const published: Array<{ readonly type: string; readonly data: unknown }> = [] const published: Array<{ readonly type: string; readonly data: unknown }> = []
const events = EventV2.Service.of({ const events: Pick<EventV2.Interface, "publish"> = {
publish: (definition, data) => publish: (definition, data) =>
Effect.sync(() => { Effect.sync(() => {
const event = { id: EventV2.ID.create(), type: definition.type, data } as EventV2.Payload<typeof definition> const event = { id: EventV2.ID.create(), type: definition.type, data } as EventV2.Payload<typeof definition>
@@ -28,16 +28,7 @@ const capture = (providerMetadataKey = "anthropic") => {
}) })
return event return event
}), }),
subscribe: () => Stream.empty, }
log: () => Stream.empty,
sequences: () => Effect.succeed(new Map()),
listen: () => Effect.succeed(Effect.void),
project: () => Effect.void,
replay: () => Effect.void,
replayAll: () => Effect.succeed(undefined),
remove: () => Effect.void,
claim: () => Effect.void,
})
return { return {
published, published,
publisher: createLLMEventPublisher(events, { publisher: createLLMEventPublisher(events, {