feat: implement bounded LLM request recovery

This commit is contained in:
Tianyi Cui
2026-07-20 03:34:19 +08:00
parent 7cf966fc0e
commit 3b0b0cefeb
115 changed files with 3311 additions and 366 deletions

View File

@@ -44,6 +44,7 @@ import {
} from '@agentclientprotocol/sdk'
import type { ContentBlock, LlmCallConfig, LlmModelInfo, LlmProviderInfo } from '@deepseek-ai/dsh-llm'
import { assertNever, CallId } from '@deepseek-ai/dsh-llm'
import type {} from '@deepseek-ai/dsh-llm-retry'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { SessionId } from '@deepseek-ai/dsh-session'
// Side-effect type import: resolves `ctx.get('permission')` to the service.
@@ -481,7 +482,7 @@ export function apply(ctx: Context, config: AcpConfig): void {
reason: TurnEndReason,
): void => {
if (reason.kind === 'error') {
inflight.reject(internalError(`turn failed: ${reason.message}`))
inflight.reject(internalError(`turn failed: ${'failure' in reason ? reason.failure.message : reason.message}`))
} else {
inflight.resolve(turnEndToStopReason(reason))
}
@@ -1035,6 +1036,7 @@ function validateMcpServers(params: { mcpServers?: unknown[] }): void {
* identical update stream from the same event log.
*
* - `assistant/chunk` text-delta/reasoning-delta → message/thought chunks
* - `llm/retry` and terminal model failure → visible discarded-attempt markers
* - `user/message` → `user_message_chunk` during load replay only — so a
* loaded transcript reconstructs the USER side of each turn without echoing
* a live `session/prompt` back to the client
@@ -1081,6 +1083,13 @@ export function streamSessionEventUpdate(
}
return
}
case 'llm/retry': {
const text = '\n\n[Previous model attempt discarded; retrying '
+ `${event.data.retry}/${event.data.maxRetries} in ${event.data.delayMs}ms: `
+ `${event.data.failure.message}]\n\n`
notify({ sessionId, update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text } } })
return
}
case 'user/message': {
if (!includeUserMessages) return
// Replay the user's prompt so a loaded session shows both sides of each
@@ -1108,7 +1117,16 @@ export function streamSessionEventUpdate(
notify({ sessionId, update: { sessionUpdate: 'plan', ...todosToPlan(event.data.todos) } })
return
}
// turn/step boundaries, context/message, steering,
case 'turn/end': {
if (event.data.reason.kind !== 'error') return
const message = 'failure' in event.data.reason
? event.data.reason.failure.message
: event.data.reason.message
const text = `\n\n[Model attempt failed; any partial output above is discarded: ${message}]\n\n`
notify({ sessionId, update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text } } })
return
}
// non-error turn/step boundaries, context/message, steering,
// assistant/message — no direct ACP client update.
default:
return