feat(core): publish interrupted session steps
This commit is contained in:
parent
40ecfceec2
commit
59e8dbb066
12 changed files with 140 additions and 17 deletions
|
|
@ -227,6 +227,12 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||
draft.error = event.data.error
|
||||
})
|
||||
},
|
||||
"session.next.step.interrupted": (event) => {
|
||||
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
||||
draft.time.completed = event.data.timestamp
|
||||
draft.finish = "interrupted"
|
||||
})
|
||||
},
|
||||
"session.next.text.started": (event) => {
|
||||
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
||||
draft.content.push(
|
||||
|
|
|
|||
|
|
@ -381,6 +381,7 @@ export const layer = Layer.effectDiscard(
|
|||
yield* events.project(SessionEvent.Step.Started, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Step.Ended, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Step.Failed, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Step.Interrupted, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Text.Started, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Text.Ended, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Tool.Input.Started, (event) => run(db, event))
|
||||
|
|
|
|||
|
|
@ -290,6 +290,7 @@ export const layer = Layer.effect(
|
|||
if (settled._tag === "Failure" && isQuestionRejected(settled.cause)) {
|
||||
yield* FiberSet.clear(toolFibers)
|
||||
yield* withPublication(publisher.failUnsettledTools("Tool execution interrupted"))
|
||||
yield* withPublication(publisher.interruptAssistant())
|
||||
return yield* Effect.interrupt
|
||||
}
|
||||
if (
|
||||
|
|
@ -298,8 +299,7 @@ export const layer = Layer.effect(
|
|||
) {
|
||||
yield* FiberSet.clear(toolFibers)
|
||||
yield* withPublication(publisher.failUnsettledTools("Tool execution interrupted"))
|
||||
if (publisher.hasActiveAssistant())
|
||||
yield* withPublication(publisher.failAssistant("Provider turn interrupted"))
|
||||
yield* withPublication(publisher.interruptAssistant())
|
||||
}
|
||||
if (settled._tag === "Failure" && !Cause.hasInterrupts(settled.cause)) {
|
||||
const failure = Cause.squash(settled.cause)
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
const timestamp = DateTime.now
|
||||
let assistantMessageID: SessionMessage.ID | undefined
|
||||
let assistantActive = false
|
||||
let assistantFailed = false
|
||||
let assistantSettled = false
|
||||
let providerFailed = false
|
||||
let stepSettlement: { readonly finish: string; readonly tokens: ReturnType<typeof tokens> } | undefined
|
||||
|
||||
|
|
@ -197,11 +197,11 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
})
|
||||
|
||||
const failAssistant = Effect.fnUntraced(function* (message: string) {
|
||||
if (assistantFailed) return
|
||||
if (assistantSettled) return
|
||||
yield* flush()
|
||||
const assistantMessageID = yield* startAssistant()
|
||||
assistantActive = false
|
||||
assistantFailed = true
|
||||
assistantSettled = true
|
||||
yield* events.publish(SessionEvent.Step.Failed, {
|
||||
sessionID: input.sessionID,
|
||||
timestamp: yield* timestamp,
|
||||
|
|
@ -210,6 +210,19 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
})
|
||||
})
|
||||
|
||||
const interruptAssistant = Effect.fnUntraced(function* () {
|
||||
if (assistantSettled) return
|
||||
yield* flush()
|
||||
const assistantMessageID = yield* startAssistant()
|
||||
assistantActive = false
|
||||
assistantSettled = true
|
||||
yield* events.publish(SessionEvent.Step.Interrupted, {
|
||||
sessionID: input.sessionID,
|
||||
timestamp: yield* timestamp,
|
||||
assistantMessageID,
|
||||
})
|
||||
})
|
||||
|
||||
const failUnsettledTools = Effect.fn("SessionRunner.failUnsettledTools")(function* (
|
||||
message: string,
|
||||
hostedOnly = false,
|
||||
|
|
@ -412,6 +425,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
publish,
|
||||
flush,
|
||||
failAssistant,
|
||||
interruptAssistant,
|
||||
failUnsettledTools,
|
||||
hasActiveAssistant: () => assistantActive,
|
||||
hasAssistantStarted: () => assistantMessageID !== undefined,
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ const toolResult = (tool: SessionMessage.AssistantTool, providerMetadata: Provid
|
|||
const assistant = (message: SessionMessage.Assistant, model: Model) => {
|
||||
const sameModel =
|
||||
String(message.model.providerID) === String(model.provider) && String(message.model.id) === String(model.id)
|
||||
const reuseProviderMetadata = sameModel && message.error === undefined
|
||||
const reuseProviderMetadata = sameModel && message.error === undefined && message.finish !== "interrupted"
|
||||
const content = message.content.flatMap((item): ContentPart[] => {
|
||||
if (item.type === "text") return [{ type: "text", text: item.text }]
|
||||
if (item.type === "reasoning")
|
||||
|
|
|
|||
|
|
@ -327,7 +327,7 @@ Recent work
|
|||
])
|
||||
})
|
||||
|
||||
test("drops provider-native continuation metadata from failed assistant turns", () => {
|
||||
test("drops provider-native continuation metadata from interrupted assistant turns", () => {
|
||||
const messages = toLLMMessages(
|
||||
[
|
||||
SessionMessage.Assistant.make({
|
||||
|
|
@ -361,8 +361,7 @@ Recent work
|
|||
time: { created, completed: created },
|
||||
}),
|
||||
],
|
||||
finish: "error",
|
||||
error: { type: "unknown", message: "Provider turn interrupted" },
|
||||
finish: "interrupted",
|
||||
time: { created, completed: created },
|
||||
}),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -542,12 +542,19 @@ const verifyPartialFlushOnInterruption = (kind: FragmentKind) =>
|
|||
const fiber = yield* runner.run({ sessionID, force: true }).pipe(Effect.forkChild)
|
||||
yield* Deferred.await(streamed)
|
||||
yield* Fiber.interrupt(fiber)
|
||||
const { db } = yield* Database.Service
|
||||
const interrupted = yield* db
|
||||
.select({ type: EventTable.type })
|
||||
.from(EventTable)
|
||||
.where(eq(EventTable.type, EventV2.versionedType(SessionEvent.Step.Interrupted.type, 1)))
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
expect(interrupted).toHaveLength(1)
|
||||
expect(yield* session.context(sessionID)).toMatchObject([
|
||||
{ type: "user", text: prompt },
|
||||
{
|
||||
type: "assistant",
|
||||
finish: "error",
|
||||
error: { type: "unknown", message: "Provider turn interrupted" },
|
||||
finish: "interrupted",
|
||||
content: [
|
||||
kind === "tool input"
|
||||
? { type: "tool", id: fragmentID(kind, "interrupted"), state: { status: "error" } }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue