fix(session): type turn/end error as one structured failure
TurnEndReasonMap.error now carries a single `error: LlmFailure` field: an LlmError keeps its structured facts, any other error flattens to errorChain text under the UNKNOWN code. Consumers read message/code directly instead of defending against an unknown union — this also fixes errorChain() rendering structured failures as '[object Object]' in the TUI and ACP error paths. Document the turn-stopping contract: a concludesTurn result never short-circuits already-submitted next-step work (same-step additionalContexts or racing steering still runs), data decides.
This commit is contained in:
@@ -522,9 +522,11 @@ interface TurnEndReasonMap {
|
||||
|
||||
blocked: { kind: 'blocked' }
|
||||
/**
|
||||
* The turn failed.
|
||||
* The turn failed. `error` is always a structured failure: the `LlmError`
|
||||
* facts verbatim, or `{ message: errorChain(error), code: 'UNKNOWN' }`
|
||||
* flattened from any other error.
|
||||
*/
|
||||
error: { kind: 'error'; error: unknown }
|
||||
error: { kind: 'error'; error: LlmFailure }
|
||||
/** At least one step reached its output-token ceiling, even if a plugin continued the turn. */
|
||||
'max-tokens': { kind: 'max-tokens' }
|
||||
/**
|
||||
|
||||
@@ -526,9 +526,11 @@ interface TurnEndReasonMap {
|
||||
|
||||
blocked: { kind: 'blocked' }
|
||||
/**
|
||||
* The turn failed.
|
||||
* The turn failed. `error` is always a structured failure: the `LlmError`
|
||||
* facts verbatim, or `{ message: errorChain(error), code: 'UNKNOWN' }`
|
||||
* flattened from any other error.
|
||||
*/
|
||||
error: { kind: 'error'; error: unknown }
|
||||
error: { kind: 'error'; error: LlmFailure }
|
||||
/** At least one step reached its output-token ceiling, even if a plugin continued the turn. */
|
||||
'max-tokens': { kind: 'max-tokens' }
|
||||
/**
|
||||
|
||||
@@ -37,4 +37,4 @@
|
||||
{"type":"step/start","seq":35,"time":1785731000386,"data":{"turn":3,"step":1}}
|
||||
{"type":"user/message","seq":36,"time":1785731000386,"data":{"content":[{"type":"text","text":"Now reply with exactly THIRD_OK."}],"source":{"kind":"coordinator","senderSessionId":"11111111-1111-4111-8111-111111111111"},"role":"user","id":"fda43b7e-8d31-449a-a0ef-d7b17e945f86"},"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":37,"time":1785731000391,"data":{"turn":3,"step":1}}
|
||||
{"type":"turn/end","seq":38,"time":1785731000391,"data":{"turn":3,"step":1,"reason":{"kind":"error","error":"snapshot disk full"}}}
|
||||
{"type":"turn/end","seq":38,"time":1785731000391,"data":{"turn":3,"step":1,"reason":{"kind":"error","error":{"message":"snapshot disk full","code":"UNKNOWN"}}}}
|
||||
|
||||
@@ -146,7 +146,7 @@ export function apply(ctx: Context, config: AcpConfig): void {
|
||||
inflight: NonNullable<SessionRecord['inflight']>,
|
||||
reason: Extract<TurnEndReason, { kind: 'error' }>,
|
||||
): void => {
|
||||
inflight.reject(internalError(`turn failed: ${errorChain(reason.error)}`))
|
||||
inflight.reject(internalError(`turn failed: ${reason.error.message}`))
|
||||
}
|
||||
|
||||
// Emit only committed assistant text. Raw chunks, reasoning, tools, plans,
|
||||
|
||||
@@ -9,7 +9,7 @@ describe('ACP codec', () => {
|
||||
[{ kind: 'aborted', reason: { kind: 'user' } }, 'cancelled'],
|
||||
[{ kind: 'interrupted' }, 'cancelled'],
|
||||
[{ kind: 'blocked' }, 'end_turn'],
|
||||
[{ kind: 'error', error: 'failed' }, 'end_turn'],
|
||||
[{ kind: 'error', error: { message: 'failed', code: 'UNKNOWN' } }, 'end_turn'],
|
||||
] satisfies Array<[TurnEndReason, string]>)('maps %o to %s', (reason, expected) => {
|
||||
expect(turnEndToStopReason(reason)).toBe(expected)
|
||||
})
|
||||
|
||||
@@ -829,8 +829,6 @@ export class Session implements SessionFace {
|
||||
&& !this.derivedNodes.some(node => node.kind === 'model-retry' && node.turn === event.data.turn)
|
||||
) {
|
||||
const failure = event.data.reason.error
|
||||
const code = failure !== null && typeof failure === 'object' && 'code' in failure
|
||||
&& typeof failure.code === 'string' ? failure.code : undefined
|
||||
this.derivedNodes.push({
|
||||
kind: 'turn-error',
|
||||
seq: event.seq,
|
||||
@@ -838,7 +836,7 @@ export class Session implements SessionFace {
|
||||
turn: event.data.turn,
|
||||
step: event.data.step,
|
||||
message: displayFailureMessage(failure),
|
||||
...(code === undefined ? {} : { code }),
|
||||
code: failure.code,
|
||||
})
|
||||
this.derivedRev++
|
||||
}
|
||||
|
||||
@@ -233,7 +233,7 @@ describe('inspectRequests', () => {
|
||||
}),
|
||||
at(2, 'step/start', { turn: 2, step: 1 }),
|
||||
at(3, 'turn/end', {
|
||||
turn: 2, step: 1, reason: { kind: 'error', error: 'plugin exploded' },
|
||||
turn: 2, step: 1, reason: { kind: 'error', error: { message: 'plugin exploded', code: 'UNKNOWN' } },
|
||||
}),
|
||||
]))
|
||||
|
||||
|
||||
@@ -234,7 +234,7 @@ describe('live event path', () => {
|
||||
ev.stepStart(12, 2, 1),
|
||||
at(13, {
|
||||
type: 'turn/end',
|
||||
data: { turn: 2, step: 1, reason: { kind: 'error', error: 'plugin exploded' } },
|
||||
data: { turn: 2, step: 1, reason: { kind: 'error', error: { message: 'plugin exploded', code: 'UNKNOWN' } } },
|
||||
}),
|
||||
]
|
||||
for (const event of failedTurns) feed(event)
|
||||
@@ -242,9 +242,10 @@ describe('live event path', () => {
|
||||
const errors = session.getSnapshot().nodes.filter(node => node.kind === 'turn-error')
|
||||
expect(errors).toMatchObject([
|
||||
{ seq: 9, turn: 1, step: 0, code: 'AUTH', message: 'API key is invalid' },
|
||||
{ seq: 13, turn: 2, step: 1, message: 'plugin exploded' },
|
||||
// Every failed turn carries a structured failure; unstructured errors
|
||||
// flatten to the UNKNOWN code.
|
||||
{ seq: 13, turn: 2, step: 1, code: 'UNKNOWN', message: 'plugin exploded' },
|
||||
])
|
||||
expect('code' in errors[1]!).toBe(false)
|
||||
|
||||
const replay = makeSession()
|
||||
replay.api.onHistory = () => histResponse([...plainTurn(0, 0, 'a', 'b'), ...failedTurns])
|
||||
@@ -410,7 +411,7 @@ describe('live event path', () => {
|
||||
feed(ev.retry(7, 1))
|
||||
feed(at(8, {
|
||||
type: 'turn/end',
|
||||
data: { turn: 1, step: 0, reason: { kind: 'error', error: 'retry failed' } },
|
||||
data: { turn: 1, step: 0, reason: { kind: 'error', error: { message: 'retry failed', code: 'UNKNOWN' } } },
|
||||
}))
|
||||
|
||||
expect(session.getSnapshot().nodes.at(-1)).toMatchObject({
|
||||
|
||||
@@ -60,6 +60,20 @@ function requestProposal(header: EpochHeader): LlmCallConfig {
|
||||
}
|
||||
|
||||
/** Drives one session through turn and step boundaries. */
|
||||
/**
|
||||
* Shape a caught turn error into its durable reason: every failure is a
|
||||
* structured LlmFailure — an `LlmError` keeps its facts, anything else
|
||||
* flattens to `errorChain` text under the `UNKNOWN` code.
|
||||
*/
|
||||
function turnErrorReason(error: unknown): Extract<TurnEndReason, { kind: 'error' }> {
|
||||
return {
|
||||
kind: 'error',
|
||||
error: error instanceof LlmError
|
||||
? error.failure
|
||||
: { message: errorChain(error), code: 'UNKNOWN' },
|
||||
}
|
||||
}
|
||||
|
||||
export class ReactLoopAgent implements Agent {
|
||||
readonly inbox: Inbox
|
||||
private phase: Phase
|
||||
@@ -279,10 +293,7 @@ export class ReactLoopAgent implements Agent {
|
||||
turnEnds = { kind: 'aborted', reason: signal.reason as AgentCancelCause }
|
||||
throw error
|
||||
}
|
||||
turnEnds = {
|
||||
kind: 'error',
|
||||
error: error instanceof LlmError ? error.failure : errorChain(error),
|
||||
}
|
||||
turnEnds = turnErrorReason(error)
|
||||
this.throwError(error)
|
||||
} finally {
|
||||
try {
|
||||
|
||||
@@ -339,7 +339,7 @@ describe('plugin exceptions are contained', () => {
|
||||
send(agent, 'first')
|
||||
await waitForIdle(ctx, agent)
|
||||
expect(agent.session.events.findLast(event => event.type === 'turn/end')).toMatchObject({
|
||||
data: { step: 1, reason: { kind: 'error', error: 'broken continuation plugin' } },
|
||||
data: { step: 1, reason: { kind: 'error', error: { message: 'broken continuation plugin', code: 'UNKNOWN' } } },
|
||||
})
|
||||
|
||||
// the loop is still alive: a second send works normally
|
||||
@@ -426,10 +426,10 @@ describe('adapter registration, routing, and accepted-input ownership', () => {
|
||||
await waitForIdle(ctx, agent)
|
||||
const turnEnd = agent.session.events.findLast(event => event.type === 'turn/end')
|
||||
expect(turnEnd?.type === 'turn/end' && turnEnd.data.reason.kind === 'error'
|
||||
? turnEnd.data.reason.error
|
||||
? turnEnd.data.reason.error.message
|
||||
: undefined).toContain('has no provider/model')
|
||||
expect(turnEnd?.type === 'turn/end' && turnEnd.data.reason.kind === 'error'
|
||||
? turnEnd.data.reason.error
|
||||
? turnEnd.data.reason.error.message
|
||||
: undefined).toContain('agent/request')
|
||||
})
|
||||
|
||||
@@ -762,7 +762,7 @@ describe('turn and step boundary recovery', () => {
|
||||
errors: 1,
|
||||
})
|
||||
expect(agent.session.events.findLast(event => event.type === 'turn/end')).toMatchObject({
|
||||
data: { step: 0, reason: { kind: 'error', error: 'reject step-start before commit' } },
|
||||
data: { step: 0, reason: { kind: 'error', error: { message: 'reject step-start before commit', code: 'UNKNOWN' } } },
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ describe('thrown-value propagation', () => {
|
||||
await waitForIdle(ctx, agent)
|
||||
const turnEnd = agent.session.events.find(e => e.type === 'turn/end')
|
||||
expect(turnEnd?.type === 'turn/end' && turnEnd.data.reason.kind === 'error'
|
||||
? turnEnd.data.reason.error
|
||||
? turnEnd.data.reason.error.message
|
||||
: undefined).toBe('[object Object]')
|
||||
})
|
||||
})
|
||||
@@ -464,7 +464,7 @@ describe('unrenderable failure settlement', () => {
|
||||
if (end?.type === 'turn/end' && end.data.reason.kind === 'error') {
|
||||
// The durable failure keeps the adapter facts' message, not the
|
||||
// unrenderable chain.
|
||||
expect(errorChain(end.data.reason.error)).not.toBe('<unrenderable value>')
|
||||
expect(errorChain(end.data.reason.error.message)).not.toBe('<unrenderable value>')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -235,7 +235,7 @@ describe('agent loop', () => {
|
||||
const turnEnd = agent.session.events.find(e => e.type === 'turn/end')
|
||||
expect(turnEnd?.type === 'turn/end' && turnEnd.data.reason.kind).toBe('error')
|
||||
expect(turnEnd?.type === 'turn/end' && turnEnd.data.reason.kind === 'error'
|
||||
? turnEnd.data.reason.error
|
||||
? turnEnd.data.reason.error.message
|
||||
: '').toContain('no value for this assembly')
|
||||
|
||||
// The loop survived: a waterfall listener rescues {{cwd}} and the SAME
|
||||
|
||||
@@ -361,10 +361,9 @@ describe('request stability across the loop', () => {
|
||||
expect(agent.session.events.findLast(event => event.type === 'turn/end')).toMatchObject({
|
||||
data: {
|
||||
step: 1,
|
||||
reason: {
|
||||
kind: 'error',
|
||||
error: failure instanceof LlmError ? failure.failure : failure.message,
|
||||
},
|
||||
reason: failure instanceof LlmError
|
||||
? { kind: 'error', error: failure.failure }
|
||||
: { kind: 'error', error: { message: failure.message, code: 'UNKNOWN' } },
|
||||
},
|
||||
})
|
||||
expect(adapter.requests).toHaveLength(0)
|
||||
@@ -505,7 +504,7 @@ describe('request stability across the loop', () => {
|
||||
const turnEnd = agent.session.events.findLast(event => event.type === 'turn/end')
|
||||
expect(turnEnd).toMatchObject({ data: { reason: { kind: 'error' } } })
|
||||
if (turnEnd?.type !== 'turn/end' || turnEnd.data.reason.kind !== 'error') throw new Error()
|
||||
expect(turnEnd.data.reason.error).toMatch(/not extensible|frozen|read only|readonly/i)
|
||||
expect(turnEnd.data.reason.error.message).toMatch(/not extensible|frozen|read only|readonly/i)
|
||||
})
|
||||
|
||||
it('a fresh loop instance over a seeded log anchors with a resume snapshot and stays cache-aligned', async () => {
|
||||
|
||||
@@ -683,7 +683,7 @@ describe('tool-call scheduler: failure quiescence', () => {
|
||||
expect(turnEndBeforeDrain).toBeUndefined()
|
||||
expect(gated.pending()).toEqual([])
|
||||
expect(events(agent).findLast(event => event.type === 'turn/end')).toMatchObject({
|
||||
data: { step: 1, reason: { kind: 'error', error: schedulerError.message } },
|
||||
data: { step: 1, reason: { kind: 'error', error: { message: schedulerError.message, code: 'UNKNOWN' } } },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -278,7 +278,10 @@ declare module 'cordis' {
|
||||
* re-reads its inbox: fresh steering runs another step, none closes the
|
||||
* turn. Data decides, so listener order cannot change the outcome. The
|
||||
* inverse control (stop a tool loop early) is data too: a tool result
|
||||
* carrying `concludesTurn` ends the turn at its step.
|
||||
* carrying `concludesTurn` ends the turn at its step. The conclusion
|
||||
* never short-circuits already-submitted next-step work: same-step
|
||||
* `additionalContexts` or racing steering still runs, and the turn
|
||||
* closes only when that inbox drains.
|
||||
* @param agent - the agent whose turn is at its stop boundary.
|
||||
* @param turn - the turn about to close.
|
||||
* @param signal - the current turn's explicit abort signal.
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
CallId,
|
||||
LlmCallConfig,
|
||||
LlmCallConfigAdapterDefaults,
|
||||
LlmFailure,
|
||||
StreamChunk,
|
||||
TokenUsage,
|
||||
ToolResultMessage,
|
||||
@@ -109,9 +110,11 @@ export interface TurnEndReasonMap {
|
||||
|
||||
blocked: { kind: 'blocked' }
|
||||
/**
|
||||
* The turn failed.
|
||||
* The turn failed. `error` is always a structured failure: the `LlmError`
|
||||
* facts verbatim, or `{ message: errorChain(error), code: 'UNKNOWN' }`
|
||||
* flattened from any other error.
|
||||
*/
|
||||
error: { kind: 'error'; error: unknown }
|
||||
error: { kind: 'error'; error: LlmFailure }
|
||||
/** At least one step reached its output-token ceiling, even if a plugin continued the turn. */
|
||||
'max-tokens': { kind: 'max-tokens' }
|
||||
/**
|
||||
|
||||
@@ -139,7 +139,7 @@ describe('SessionStore.fork', () => {
|
||||
const reasons: TurnEndReason[] = [
|
||||
{ kind: 'completed' },
|
||||
{ kind: 'aborted', reason: { kind: 'user' } },
|
||||
{ kind: 'error', error: 'model failed' },
|
||||
{ kind: 'error', error: { message: 'model failed', code: 'UNKNOWN' } },
|
||||
{ kind: 'aborted', reason: { kind: 'disposed' } },
|
||||
{ kind: 'max-tokens' },
|
||||
{ kind: 'interrupted' },
|
||||
|
||||
@@ -331,7 +331,7 @@ describe('session-log invariants', () => {
|
||||
unresolved.append('step/start', { turn: 1, step: 1 })
|
||||
unresolved.append('tool/call', { turn: 1, step: 1, callId: CallId('c1'), name: 'echo', arguments: '{}' })
|
||||
unresolved.append('step/end', { turn: 1, step: 1 })
|
||||
unresolved.append('turn/end', { turn: 1, step: 1, reason: { kind: 'error', error: 'boom' } })
|
||||
unresolved.append('turn/end', { turn: 1, step: 1, reason: { kind: 'error', error: { message: 'boom', code: 'UNKNOWN' } } })
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
|
||||
@@ -448,10 +448,14 @@ describe('provider-routed retry policy', () => {
|
||||
|
||||
expect(adapter.requests).toHaveLength(0)
|
||||
expect(agent.session.events.some(event => event.type === 'llm/retry')).toBe(false)
|
||||
expect(agent.session.events.at(-1)).toMatchObject({
|
||||
const end = agent.session.events.at(-1)
|
||||
expect(end).toMatchObject({
|
||||
type: 'turn/end',
|
||||
data: { step: 1, reason: { kind: 'error', error: { code: 'NO_ADAPTER' } } },
|
||||
})
|
||||
if (end?.type === 'turn/end' && end.data.reason.kind === 'error') {
|
||||
expect(end.data.reason.error.message).toContain('no adapter registered for provider')
|
||||
}
|
||||
})
|
||||
|
||||
it('selects policy by the failed request provider', async () => {
|
||||
|
||||
@@ -193,7 +193,7 @@ describe('bounded retry through the real DeepSeek HTTP/SSE adapter', () => {
|
||||
expect(agent.session.events.some(event => event.type === 'llm/retry')).toBe(false)
|
||||
expect(agent.session.events.at(-1)).toMatchObject({
|
||||
type: 'turn/end',
|
||||
data: { step: 1, reason: { kind: 'error', error: { code: 'STREAM_CLOSED' } } },
|
||||
data: { step: 1, reason: { kind: 'error', error: { message: 'SSE stream ended without [DONE]', code: 'STREAM_CLOSED' } } },
|
||||
})
|
||||
})
|
||||
|
||||
@@ -233,9 +233,13 @@ describe('bounded retry through the real DeepSeek HTTP/SSE adapter', () => {
|
||||
expect(server.requests).toHaveLength(3)
|
||||
expect(agent.session.events.filter(event => event.type === 'step/start')).toHaveLength(1)
|
||||
expect(agent.session.events.filter(event => event.type === 'llm/retry')).toHaveLength(2)
|
||||
expect(agent.session.events.at(-1)).toMatchObject({
|
||||
const end = agent.session.events.at(-1)
|
||||
expect(end).toMatchObject({
|
||||
type: 'turn/end',
|
||||
data: { step: 1, reason: { kind: 'error', error: { code: 'TRANSPORT' } } },
|
||||
})
|
||||
if (end?.type === 'turn/end' && end.data.reason.kind === 'error') {
|
||||
expect(end.data.reason.error.message).toContain('DeepSeek API request to')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -340,7 +340,7 @@ describe('SQLite session search', () => {
|
||||
{ type: 'user/message', seq: 2, time: 12, data: createUserMessage({
|
||||
content: [{ type: 'text', text: 'needle summary' }], source: { kind: 'plugin', plugin: 'test' },
|
||||
}), surfaceOp: { op: 'replace', start: 0, end: 0 }, sourceEventSeqs: [0] },
|
||||
{ type: 'turn/end', seq: 3, time: 13, data: { turn: 1, step: 1, reason: { kind: 'error', error: 'needle failure' } } },
|
||||
{ type: 'turn/end', seq: 3, time: 13, data: { turn: 1, step: 1, reason: { kind: 'error', error: { message: 'needle failure', code: 'UNKNOWN' } } } },
|
||||
]
|
||||
ctx.sessions.create(SessionId('a'), { seed: events, meta: { cwd: '/a', parentSession: parent, createdAt: 20 } })
|
||||
ctx.sessions.create(SessionId('b'), { seed: messageEvents('needle peer', 12), meta: { createdAt: 20 } })
|
||||
|
||||
@@ -44,7 +44,7 @@ export function extractSessionEventText(event: SessionEvent): string {
|
||||
function turnEndText(reason: SessionEvent<'turn/end'>['data']['reason']): string {
|
||||
switch (reason.kind) {
|
||||
case 'error':
|
||||
return joinText(['error', reason.error instanceof Error ? reason.error.message : String(reason.error)])
|
||||
return joinText(['error', reason.error.message])
|
||||
case 'aborted':
|
||||
return 'aborted'
|
||||
case 'max-tokens':
|
||||
|
||||
@@ -119,8 +119,8 @@ describe('session-query semantic extraction', () => {
|
||||
|
||||
it('extracts meaningful turn outcomes and skips structural or unknown events', () => {
|
||||
const reasons: Array<[SessionEvent<'turn/end'>['data']['reason'], string]> = [
|
||||
[{ kind: 'error', error: new Error('boom') }, 'error\nboom'],
|
||||
[{ kind: 'error', error: 'provider boom' }, 'error\nprovider boom'],
|
||||
[{ kind: 'error', error: { message: 'boom', code: 'UNKNOWN' } }, 'error\nboom'],
|
||||
[{ kind: 'error', error: { message: 'provider boom', code: 'UNKNOWN' } }, 'error\nprovider boom'],
|
||||
[{ kind: 'aborted', reason: { kind: 'user' } }, 'aborted'],
|
||||
[{ kind: 'aborted', reason: { kind: 'disposed' } }, 'aborted'],
|
||||
[{ kind: 'max-tokens' }, 'max-tokens'],
|
||||
|
||||
@@ -74,7 +74,7 @@ describe('sdkStopReason', () => {
|
||||
expect(sdkStopReason({ kind: 'completed' })).toBe('completed')
|
||||
expect(sdkStopReason({ kind: 'max-tokens' })).toBe('max-tokens')
|
||||
expect(sdkStopReason({ kind: 'aborted', reason: { kind: 'user' } })).toBe('aborted')
|
||||
expect(sdkStopReason({ kind: 'error', error: new Error('x') })).toBe('error')
|
||||
expect(sdkStopReason({ kind: 'error', error: { message: 'x', code: 'UNKNOWN' } })).toBe('error')
|
||||
expect(sdkStopReason({ kind: 'interrupted' })).toBe('error')
|
||||
expect(sdkStopReason({ kind: 'aborted', reason: { kind: 'disposed' } })).toBe('aborted')
|
||||
})
|
||||
|
||||
@@ -183,7 +183,7 @@ describe('deriveReplayScript', () => {
|
||||
const events: SessionEvent[] = [
|
||||
chunkEvent(1, 1, 1, { type: 'block-start', index: 0, blockType: 'text' }),
|
||||
chunkEvent(2, 1, 1, { type: 'text-delta', index: 0, text: 'par' }),
|
||||
{ type: 'turn/end', seq: 3, time: 0, data: { turn: 1, step: 1, reason: { kind: 'error', error: 'x' } } },
|
||||
{ type: 'turn/end', seq: 3, time: 0, data: { turn: 1, step: 1, reason: { kind: 'error', error: { message: 'x', code: 'UNKNOWN' } } } },
|
||||
]
|
||||
expect(() => deriveReplayScript(events)).toThrow(/without a finish chunk.*replay\.override\.json/s)
|
||||
})
|
||||
|
||||
@@ -113,7 +113,7 @@ describe('TelemetryOtel wire', () => {
|
||||
const { ctx, fiber } = await boot(url)
|
||||
const session = ctx.sessions.create(SessionId('wire'), { meta: { cwd: '/tmp/w' } })
|
||||
session.append('turn/start', { turn: 1 })
|
||||
session.append('turn/end', { turn: 1, step: 0, reason: { kind: 'error', error: 'boom' } })
|
||||
session.append('turn/end', { turn: 1, step: 0, reason: { kind: 'error', error: { message: 'boom', code: 'UNKNOWN' } } })
|
||||
await fiber.dispose()
|
||||
|
||||
expect(captures.length).toBeGreaterThan(0)
|
||||
|
||||
@@ -126,7 +126,7 @@ describe('TelemetryCoordinator capture', () => {
|
||||
}),
|
||||
}, { surfaceOp: 'append' })
|
||||
session.append('telemetry-test/opaque', { payload: { nested: [] } })
|
||||
session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'error', error: 'boom' } })
|
||||
session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'error', error: { message: 'boom', code: 'UNKNOWN' } } })
|
||||
const severities = backend.ledger().map(r => [r.attributes['event.type'], r.severity])
|
||||
expect(severities).toEqual([
|
||||
['turn/start', 'info'],
|
||||
|
||||
@@ -831,7 +831,7 @@ export function createTuiChat(
|
||||
liveErrors.delete(key)
|
||||
alreadyReported = true
|
||||
}
|
||||
const message = errorChain(reason.error)
|
||||
const message = reason.error.message
|
||||
if (!alreadyReported) appendNotice(message, 'error')
|
||||
break
|
||||
}
|
||||
|
||||
@@ -641,7 +641,7 @@ describe('TUI terminal-state snapshots', () => {
|
||||
todos: [{ content: `Unsafe todo ${CONTROL_PROBE}`, status: 'in_progress' }],
|
||||
})
|
||||
session.append('step/end', { turn: 1, step: 1 })
|
||||
session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'error', error: `Unsafe turn error ${CONTROL_PROBE}` },
|
||||
session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'error', error: { message: `Unsafe turn error ${CONTROL_PROBE}`, code: 'UNKNOWN' } },
|
||||
})
|
||||
},
|
||||
}, { columns: 100, rows: 34 })
|
||||
@@ -801,7 +801,7 @@ describe('TUI terminal-state snapshots', () => {
|
||||
harness.terminal.send('\r')
|
||||
agentEvents(harness.ctx, harness.agent).emit('agent/error', 1, 1, new Error('provider stream failed after partial output'))
|
||||
harness.session.append('step/end', { turn: 1, step: 1 })
|
||||
harness.session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'error', error: 'provider stream failed after partial output' },
|
||||
harness.session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'error', error: { message: 'provider stream failed after partial output', code: 'UNKNOWN' } },
|
||||
})
|
||||
harness.session.append('turn/start', { turn: 2 })
|
||||
harness.session.append('turn/end', { turn: 2, step: 0, reason: { kind: 'interrupted' },
|
||||
|
||||
@@ -466,7 +466,7 @@ describe('goodbye message and /resume', () => {
|
||||
|
||||
it.each([
|
||||
[{ kind: 'aborted', reason: { kind: 'user' } }, 'cancelled'],
|
||||
[{ kind: 'error', step: 1, error: 'failed' }, 'error'],
|
||||
[{ kind: 'error', error: { message: 'failed', code: 'UNKNOWN' } }, 'error'],
|
||||
[{ kind: 'aborted', reason: { kind: 'disposed' } }, 'disposed'],
|
||||
[{ kind: 'max-tokens' }, 'max tokens'],
|
||||
[{ kind: 'interrupted' }, 'interrupted'],
|
||||
@@ -3981,9 +3981,9 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
agentEvents(events.ctx, unrelatedAgent).emit('agent/disposed')
|
||||
agentEvents(events.ctx, events.agent).emit('agent/error', 1, 1, new Error('live failure'))
|
||||
events.session.append('step/end', { turn: 1, step: 1 })
|
||||
events.session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'error', error: 'live failure' } })
|
||||
events.session.append('turn/end', { turn: 1, step: 1, reason: { kind: 'error', error: { message: 'live failure', code: 'UNKNOWN' } } })
|
||||
events.session.append('turn/start', { turn: 2 })
|
||||
events.session.append('turn/end', { turn: 2, step: 0, reason: { kind: 'error', error: 'durable failure' } })
|
||||
events.session.append('turn/end', { turn: 2, step: 0, reason: { kind: 'error', error: { message: 'durable failure', code: 'UNKNOWN' } } })
|
||||
events.session.append('turn/start', { turn: 3 })
|
||||
events.session.append('turn/end', { turn: 3, step: 0, reason: { kind: 'aborted', reason: { kind: 'user' } },
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user