feat(core): add session snapshot and revert system (#33226)

This commit is contained in:
Dax 2026-06-24 19:41:16 -04:00 committed by GitHub
commit 9bb5370205
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 2311 additions and 365 deletions

View file

@ -35,6 +35,7 @@ import { SessionRunnerModel } from "./model"
import { createLLMEventPublisher } from "./publish-llm-event"
import { toLLMMessages } from "./to-llm-message"
import { MAX_STEPS_PROMPT } from "./max-steps"
import { Snapshot } from "../../snapshot"
/**
* Runs one durable coding-agent Session until it settles.
@ -100,6 +101,7 @@ export const layer = Layer.effect(
const skillGuidance = yield* SkillGuidance.Service
const referenceGuidance = yield* ReferenceGuidance.Service
const config = yield* Config.Service
const snapshots = yield* Snapshot.Service
const db = (yield* Database.Service).db
const compaction = SessionCompaction.make({ events, llm, config: yield* config.entries() })
const getSession = Effect.fn("SessionRunner.getSession")(function* (sessionID: SessionSchema.ID) {
@ -205,6 +207,7 @@ export const layer = Layer.effect(
})
if (yield* compaction.compactIfNeeded({ sessionID: session.id, entries, model, request }))
return yield* Effect.die(continueAfterCompaction(currentStep))
const startSnapshot = yield* snapshots.capture()
const publisher = createLLMEventPublisher(events, {
sessionID: session.id,
agent: agent.id,
@ -213,6 +216,7 @@ export const layer = Layer.effect(
providerID: ProviderV2.ID.make(model.provider),
...(session.model?.variant === undefined ? {} : { variant: session.model.variant }),
},
snapshot: startSnapshot,
})
const withPublication = Semaphore.makeUnsafe(1).withPermit
const publish = (event: LLMEvent, outputPaths: ReadonlyArray<string> = []) =>
@ -302,6 +306,23 @@ export const layer = Layer.effect(
const message = failure instanceof Error ? failure.message : String(failure)
yield* withPublication(publisher.failUnsettledTools(`Tool execution failed: ${message}`))
}
const stepSettlement = publisher.stepSettlement()
if (stepSettlement && !publisher.hasProviderError()) {
const endSnapshot = yield* snapshots.capture()
const files = startSnapshot && endSnapshot
? yield* snapshots.files({ from: startSnapshot, to: endSnapshot }).pipe(Effect.catch(() => Effect.succeed(undefined)))
: undefined
yield* withPublication(events.publish(SessionEvent.Step.Ended, {
sessionID: session.id,
timestamp: yield* DateTime.now,
assistantMessageID: yield* publisher.startAssistant(),
finish: stepSettlement.finish,
cost: 0,
tokens: stepSettlement.tokens,
snapshot: endSnapshot,
files,
}))
}
if (publisher.hasProviderError())
yield* withPublication(publisher.failUnsettledTools("Tool execution interrupted"))
if (stream._tag === "Success" && !publisher.hasProviderError())

View file

@ -10,6 +10,7 @@ type Input = {
readonly sessionID: SessionSchema.ID
readonly agent: string
readonly model: ModelV2.Ref
readonly snapshot?: string
}
const safe = (value: number | undefined) => Math.max(0, Number.isFinite(value) ? (value ?? 0) : 0)
@ -68,6 +69,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
let assistantActive = false
let assistantFailed = false
let providerFailed = false
let stepSettlement: { readonly finish: string; readonly tokens: ReturnType<typeof tokens> } | undefined
const startAssistant = Effect.fnUntraced(function* () {
if (assistantMessageID !== undefined) return assistantMessageID
@ -77,6 +79,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
...input,
assistantMessageID,
timestamp: yield* timestamp,
snapshot: input.snapshot,
})
return assistantMessageID
})
@ -393,14 +396,8 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
case "step-finish":
yield* flush()
assistantActive = false
yield* events.publish(SessionEvent.Step.Ended, {
sessionID: input.sessionID,
timestamp: yield* timestamp,
assistantMessageID: yield* startAssistant(),
finish: event.reason,
cost: 0,
tokens: tokens(event.usage),
})
if (stepSettlement) return yield* Effect.die("Duplicate step finish")
stepSettlement = { finish: event.reason, tokens: tokens(event.usage) }
return
case "finish":
return
@ -419,6 +416,8 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
hasActiveAssistant: () => assistantActive,
hasAssistantStarted: () => assistantMessageID !== undefined,
hasProviderError: () => providerFailed,
stepSettlement: () => stepSettlement,
startAssistant,
assistantMessageID: assistantMessageIDForTool,
}
}