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:
_Kerman
2026-08-03 16:33:23 +08:00
parent 8b975edf44
commit a597763393
30 changed files with 81 additions and 54 deletions
+5 -1
View File
@@ -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')
}
})
})