fix: close recovery review gaps
This commit is contained in:
@@ -32,7 +32,7 @@ class TerminalModelRequestFailure extends Error {
|
||||
readonly requestError: RequestError,
|
||||
readonly failure: LlmFailure,
|
||||
) {
|
||||
super(requestError.message, { cause: requestError })
|
||||
super(failure.message, { cause: requestError })
|
||||
this.name = 'TerminalModelRequestFailure'
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,12 @@ function errorData(err: RequestError): { message: string; code?: string } {
|
||||
return { message: errorChain(err), ...typeof err.code === 'string' ? { code: err.code } : {} }
|
||||
}
|
||||
|
||||
/** Preserve cause diagnostics, falling back to adapter-normalized prose for a hostile Error. */
|
||||
function durableFailure(err: RequestError, failure: LlmFailure): LlmFailure {
|
||||
const message = errorChain(err)
|
||||
return { ...failure, message: message === '<unrenderable value>' ? failure.message : message }
|
||||
}
|
||||
|
||||
/** Map a successful max-token finish onto the turn reason; other successful finishes add nothing. */
|
||||
function stepFinishReason(finish: FinishReason): TurnEndReason | undefined {
|
||||
switch (finish.kind) {
|
||||
@@ -231,7 +237,7 @@ async function runTurn(
|
||||
errorReported = true
|
||||
reason = failure === undefined
|
||||
? { kind: 'error', step, ...errorData(err) }
|
||||
: { kind: 'error', step, failure: { ...failure, message: errorChain(err) } }
|
||||
: { kind: 'error', step, failure: durableFailure(err, failure) }
|
||||
try {
|
||||
events.emit('agent/error', turn, step, err)
|
||||
} catch {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Context } from 'cordis'
|
||||
import LlmService, {
|
||||
CallId,
|
||||
CONTEXT_WINDOW_EXCEEDED_CODE,
|
||||
HarnessError,
|
||||
LlmAdapter,
|
||||
LlmError,
|
||||
ProviderRequestId,
|
||||
@@ -419,6 +420,31 @@ describe('agent post-step and request-error lifecycle', () => {
|
||||
expect(seen).toBe(original)
|
||||
})
|
||||
|
||||
it('keeps an adapter error with a hostile message accessor on the recovery path', async () => {
|
||||
const original = Object.defineProperty(new HarnessError('provider failed', 'SERVER'), 'message', {
|
||||
get() { throw new Error('SDK message accessor trap') },
|
||||
})
|
||||
const ctx = await harness(new SynchronousDispatchFailureAdapter(original))
|
||||
const agent = ctx.agentLoop.create(SessionId('hostile-message-recovery'), { provider: 'mock', model: 'mock' })
|
||||
let seenError: Error | undefined
|
||||
let seenFailure: LlmFailure | undefined
|
||||
ctx.on('agent/request-error', async (_agent, _turn, _step, error, failure, _history, _signal, next) => {
|
||||
seenError = error
|
||||
seenFailure = failure
|
||||
return next()
|
||||
})
|
||||
|
||||
send(agent)
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
expect(seenError).toBe(original)
|
||||
expect(seenFailure).toEqual({ message: 'LLM adapter failed', code: 'SERVER' })
|
||||
expect(agent.session.events.at(-1)).toMatchObject({
|
||||
type: 'turn/end',
|
||||
data: { reason: { kind: 'error', failure: { message: 'LLM adapter failed', code: 'SERVER' } } },
|
||||
})
|
||||
})
|
||||
|
||||
it('passes structured facts beside the original Error and records its cause chain on exhaustion', async () => {
|
||||
const original = new LlmError('provider busy', 'RATE_LIMIT', {
|
||||
cause: new Error('upstream connection reset'),
|
||||
|
||||
@@ -60,7 +60,7 @@ Durable values need one accepted representation, not a check followed by a secon
|
||||
|
||||
### Session event vocabulary (`types.ts`)
|
||||
|
||||
The append-only log's event types, enumerated member by member — payloads, surface badges, provenance — in the generated [persistence log event catalog](../../../docs/persistence-catalog.md). Token usage and provider/model/replay provenance ride on `assistant/message`; an operational error's step is on `turn/end.reason` for `kind: 'error'`, with structured provider facts for a final model-request failure.
|
||||
The append-only log's event types, enumerated member by member — payloads, surface badges, provenance — in the generated [persistence log event catalog](../../../docs/persistence-catalog.md). Token accounting reads per-step `assistant/chunk { type: 'usage' }` records and treats `assistant/message.usage` as the committed-step fallback when no usage chunk exists; failed model-request attempts have no assistant message. Provider/model/replay provenance rides on `assistant/message`; an operational error's step is on `turn/end.reason` for `kind: 'error'`, with structured provider facts for a final model-request failure.
|
||||
|
||||
Merge-extensible via `SessionEventMap` — a plugin declaration-merges its own types (the compaction seam's `compact/*`, bounded recovery's non-surface `llm/retry`, the hook bridges' `hook/*`); merged members appear in the same catalog.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user