fix(llm): isolate retry policy histories

This commit is contained in:
Turtle
2026-07-25 15:38:58 +08:00
parent 38ce422f71
commit efc725b7e4
31 changed files with 666 additions and 28 deletions

View File

@@ -0,0 +1,12 @@
# Keyless provider-retry composition for the headless stream-json snapshot.
- id: base
name: '@cordisjs/plugin-include'
config:
path: ./cordis.yml
patches:
- id: llm-deepseek
name: '@deepseek-ai/dsh-llm-deepseek'
disabled: true
- insert:
- id: retry-snapshot-backend
name: './tests/fixtures/retry-snapshot-backend.mjs'

View File

@@ -0,0 +1,53 @@
/** Deterministic provider adapter for the headless retry-policy snapshot. */
import {
LlmAdapter,
LlmError,
resolveRetryPolicy,
} from '@deepseek-ai/dsh-llm'
class RetrySnapshotAdapter extends LlmAdapter {
requests = 0
firstMessages
policy = resolveRetryPolicy({
mode: 'normal',
maxRetries: 1,
retryableCodes: ['RATE_LIMIT'],
backoff: { initialDelayMs: 1, maxDelayMs: 1, jitterRatio: 0 },
}, 'retry-snapshot-backend.retryPolicy')
providerRetryPolicy() {
return this.policy
}
async * stream(options) {
const messages = JSON.stringify(options.messages)
this.requests++
if (this.requests === 1) {
this.firstMessages = messages
throw new LlmError('snapshot transient failure', 'RATE_LIMIT', { status: 429 })
}
if (this.requests === 2 && messages !== this.firstMessages) {
throw new Error('retry snapshot changed the model-visible messages')
}
const text = 'RETRY_OK'
yield { type: 'block-start', index: 0, blockType: 'text' }
yield { type: 'text-delta', index: 0, text }
yield { type: 'block-end', index: 0, block: { type: 'text', text } }
yield { type: 'usage', usage: { inputTokens: 4, outputTokens: 2 } }
yield { type: 'finish', reason: { kind: 'stop' } }
}
}
/** Cordis plugin name. */
export const name = 'retry-snapshot-backend'
/** Required LLM registry service. */
export const inject = ['llm']
/**
* Register the deterministic provider adapter.
* @param {import('cordis').Context} ctx - plugin context carrying the LLM service.
*/
export function apply(ctx) {
ctx.llm.registerAdapter(['deepseek'], new RetrySnapshotAdapter())
}

View File

@@ -24,6 +24,8 @@ const ptyStreamExpected = join(ptyScenarioDir, 'stream-json.expected.jsonl')
const ptyConfigPath = fileURLToPath(new URL('../pty.cordis.snapshot.yml', import.meta.url))
const goalScenarioDir = join(snapshotsDir, 'goal-tools')
const goalConfigPath = fileURLToPath(new URL('../goal.cordis.snapshot.yml', import.meta.url))
const retryScenarioDir = join(snapshotsDir, 'provider-retry')
const retryConfigPath = fileURLToPath(new URL('../retry.cordis.snapshot.yml', import.meta.url))
const ralphScenarioDir = join(snapshotsDir, 'ralph-loop')
const ralphConfigPath = fileURLToPath(new URL('../ralph.cordis.snapshot.yml', import.meta.url))
const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url))
@@ -124,6 +126,46 @@ async function persistedLogs(cwd: string): Promise<PersistedLog[]> {
}
describe('headless stream-json snapshots', () => {
it('retries a transient provider failure through the one-shot app', async () => {
const prompt = await scenarioPrompt(retryScenarioDir, 'provider-retry')
const streamExpected = join(retryScenarioDir, 'stream-json.expected.jsonl')
let runCwd = ''
const result = await runLoaderSmoke({
label: 'provider retry headless stream-json snapshot',
tempDirPrefix: 'headless-snapshot-provider-retry-',
binScript,
configPath: retryConfigPath,
binArgs: ['--config', retryConfigPath, '--output-format', 'stream-json', prompt],
tsconfigPath,
env: {
DSH_SNAPSHOT: 'replay',
NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '),
},
prepare: (cwd) => { runCwd = cwd },
inspect: async (cwd) => {
const logs = await persistedLogs(cwd)
expect(logs).toHaveLength(1)
const records = parseJsonl(logs[0]?.content ?? '')
const retries = records.filter(record => record.type === 'llm/retry')
expect(retries).toHaveLength(1)
expect(retries[0]?.data).toMatchObject({
provider: 'deepseek',
mode: 'normal',
policyKey: '["normal",1,["RATE_LIMIT"],1,1,0]',
retry: 1,
maxRetries: 1,
delayMs: 1,
failure: { message: 'snapshot transient failure', code: 'RATE_LIMIT', status: 429 },
})
},
})
expect(result.stderr).toBe('')
const normalized = normalizeHeadlessStream(result.stdout, runCwd)
if (refreshing) await writeFile(streamExpected, normalized)
expect(normalized).toBe(await readFile(streamExpected, 'utf8'))
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
it('replays the advanced toolchain through the one-shot app', async () => {
const prompt = await scenarioPrompt(advancedScenarioDir, 'advanced-toolchain')
const fixtureFiles = [

View File

@@ -0,0 +1,8 @@
{
"steps": [
{
"op": "prompt",
"text": "retry the transient provider failure"
}
]
}

View File

@@ -0,0 +1,17 @@
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"turn/start","seq":0,"time":0,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"retry the transient provider failure"}],"source":{"kind":"user"}},"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":2,"time":0,"data":{"title":"retry the transient provider failure","messageSeqs":[1],"source":{"kind":"fallback"}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":4,"time":0,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":5,"time":0,"data":{"turn":1,"step":1}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"llm/retry","seq":6,"time":0,"data":{"turn":1,"step":1,"provider":"deepseek","mode":"normal","policyKey":"[\"normal\",1,[\"RATE_LIMIT\"],1,1,0]","retry":1,"maxRetries":1,"delayMs":1,"failure":{"message":"snapshot transient failure","code":"RATE_LIMIT","status":429}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":7,"time":0,"data":{"turn":1,"step":2}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":8,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":9,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"RETRY_OK"}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":10,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"RETRY_OK"}}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":4,"outputTokens":2}}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":12,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":13,"time":0,"data":{"turn":1,"step":2,"content":[{"type":"text","text":"RETRY_OK"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":4,"outputTokens":2}},"sourceEventSeqs":[8,9,10,11,12],"surfaceOp":"append"}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":14,"time":0,"data":{"turn":1,"step":2}}}
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":15,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}}
{"type":"result","success":true,"sessionId":"{{sessionId}}","turn":1,"result":"RETRY_OK","reason":{"kind":"completed"},"usage":{"inputTokens":4,"outputTokens":2}}