core: track retry attempts with detailed error context on assistant entries

users can now see when transient failures occur during assistant responses,
such as rate limits or provider overloads, giving visibility into what
issues were encountered and automatically resolved before the final response
This commit is contained in:
Dax Raad
2026-04-18 10:35:25 -04:00
parent 95edbc0ae6
commit 882b8e1e75
4 changed files with 130 additions and 4 deletions

View File

@@ -1,4 +1,4 @@
import { castDraft, produce, type WritableDraft } from "immer"
import { produce, type WritableDraft } from "immer"
import { SessionEvent } from "./session-event"
import { SessionEntry } from "./session-entry"
@@ -235,7 +235,15 @@ export function stepWith<Result>(adapter: Adapter<Result>, event: SessionEvent.E
)
}
},
retried: () => {},
retried: (event) => {
if (currentAssistant) {
adapter.updateAssistant(
produce(currentAssistant, (draft) => {
draft.retries = [...(draft.retries ?? []), SessionEntry.AssistantRetry.fromEvent(event)]
}),
)
}
},
compacted: (event) => {
adapter.appendEntry(SessionEntry.Compaction.fromEvent(event))
},

View File

@@ -104,6 +104,24 @@ export class AssistantReasoning extends Schema.Class<AssistantReasoning>("Sessio
text: Schema.String,
}) {}
export class AssistantRetry extends Schema.Class<AssistantRetry>("Session.Entry.Assistant.Retry")({
attempt: Schema.Number,
error: SessionEvent.RetryError,
time: Schema.Struct({
created: Schema.DateTimeUtc,
}),
}) {
static fromEvent(event: SessionEvent.Retried) {
return new AssistantRetry({
attempt: event.attempt,
error: event.error,
time: {
created: event.timestamp,
},
})
}
}
export const AssistantContent = Schema.Union([AssistantText, AssistantReasoning, AssistantTool]).pipe(
Schema.toTaggedUnion("type"),
)
@@ -113,6 +131,7 @@ export class Assistant extends Schema.Class<Assistant>("Session.Entry.Assistant"
...Base,
type: Schema.Literal("assistant"),
content: AssistantContent.pipe(Schema.Array),
retries: AssistantRetry.pipe(Schema.Array, Schema.optional),
cost: Schema.Number.pipe(Schema.optional),
tokens: Schema.Struct({
input: Schema.Number,
@@ -137,6 +156,7 @@ export class Assistant extends Schema.Class<Assistant>("Session.Entry.Assistant"
created: event.timestamp,
},
content: [],
retries: [],
})
}
}

View File

@@ -53,6 +53,15 @@ export namespace SessionEvent {
source: Source.pipe(Schema.optional),
}) {}
export class RetryError extends Schema.Class<RetryError>("Session.Event.Retry.Error")({
message: Schema.String,
statusCode: Schema.Number.pipe(Schema.optional),
isRetryable: Schema.Boolean,
responseHeaders: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
responseBody: Schema.String.pipe(Schema.optional),
metadata: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
}) {}
export class Prompt extends Schema.Class<Prompt>("Session.Event.Prompt")({
...Base,
type: Schema.Literal("prompt"),
@@ -386,14 +395,16 @@ export namespace SessionEvent {
export class Retried extends Schema.Class<Retried>("Session.Event.Retried")({
...Base,
type: Schema.Literal("retried"),
error: Schema.String,
attempt: Schema.Number,
error: RetryError,
}) {
static create(input: BaseInput & { error: string }) {
static create(input: BaseInput & { attempt: number; error: RetryError }) {
return new Retried({
id: input.id ?? ID.create(),
type: "retried",
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
metadata: input.metadata,
attempt: input.attempt,
error: input.error,
})
}