Merge remote-tracking branch 'origin/master' into claude/unified-environment-credentials-c8841a

# Conflicts:
#	examples/headless-agent/tests/headless.snapshot.ts
#	examples/headless-agent/tests/snapshots/missing-credential/stream-json.expected.jsonl
#	packages/llm/llm-deepseek/tests/adapter.spec.ts
This commit is contained in:
Yichen Jiang
2026-08-06 10:52:32 +08:00
833 changed files with 19039 additions and 14776 deletions
+45 -73
View File
@@ -1,11 +1,11 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import SessionStore, { SessionId, type Session } from '@deepseek-ai/dsh-session'
import { createUserMessage, ProviderRequestId , createMessage } from '@deepseek-ai/dsh-llm'
import { createUserMessage, ProviderRequestId } from '@deepseek-ai/dsh-llm'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import InvariantService from '@deepseek-ai/dsh-invariants'
import * as RetryInvariant from '@deepseek-ai/dsh-llm-retry/invariant'
import { providerForClosedStep } from '../src/history.ts'
import { providerForOpenStep } from '../src/history.ts'
async function setup(): Promise<Context> {
const ctx = new Context()
@@ -15,26 +15,24 @@ async function setup(): Promise<Context> {
return ctx
}
function closeStep(ctx: Context, id: string, turn = 1, step = 1) {
function openStep(ctx: Context, id: string, turn = 1, step = 1) {
const session = ctx.sessions.create(SessionId(id))
session.append('turn/start', { turn, trigger: { kind: 'message', source: { kind: 'user' } } })
session.append('turn/start', { turn })
session.append('step/start', { turn, step })
session.append('request/header', {
header: { config: { provider: 'mock', model: 'mock' } },
reason: 'initial',
})
session.append('step/end', { turn, step })
return session
}
function appendRetryTurn(session: Session, turn: number) {
session.append('turn/start', { turn, trigger: { kind: 'retry' } })
session.append('turn/start', { turn })
session.append('step/start', { turn, step: 1 })
session.append('request/header', {
header: { config: { provider: 'mock', model: 'mock' } },
reason: 'initial',
})
session.append('step/end', { turn, step: 1 })
session.append('llm/retry', { turn, step: 1, ...normal })
}
@@ -58,28 +56,24 @@ const always = {
}
describe('llm-retry invariants', () => {
it('has no provider without the requested closed step or a route marker', () => {
expect(providerForClosedStep([], 1, 1)).toBeUndefined()
expect(providerForClosedStep([{
type: 'step/end',
it('has no provider without the requested open step or a route marker', () => {
expect(providerForOpenStep([], 1, 1)).toBeUndefined()
expect(providerForOpenStep([{
type: 'step/start',
data: { turn: 1, step: 1 },
}] as never, 1, 1)).toBeUndefined()
})
it('accepts bounded and unbounded records after successive closed steps', async () => {
it('accepts successive bounded and unbounded records inside their open steps', async () => {
const ctx = await setup()
const session = closeStep(ctx, 'retry-invariant-valid')
const session = openStep(ctx, 'retry-invariant-valid')
expect(() => {
session.append('llm/retry', { turn: 1, step: 1, ...normal })
session.append('turn/end', { turn: 1, reason: { kind: 'error', step: 1, failure } })
session.append('turn/start', { turn: 2, trigger: { kind: 'retry' } })
session.append('step/start', { turn: 2, step: 1 })
session.append('step/end', { turn: 2, step: 1 })
session.append('llm/retry', {
turn: 2, step: 1, ...normal, retry: 2, delayMs: 0,
turn: 1, step: 1, ...normal, retry: 2, delayMs: 0,
})
const unbounded = closeStep(ctx, 'retry-invariant-always')
const unbounded = openStep(ctx, 'retry-invariant-always')
unbounded.append('llm/retry', { turn: 1, step: 1, ...always })
}).not.toThrow()
expect(() => { ctx.emit('tools/change') }).not.toThrow()
@@ -87,7 +81,7 @@ describe('llm-retry invariants', () => {
it('validates the complete durable failure payload', async () => {
const ctx = await setup()
const complete = closeStep(ctx, 'retry-invariant-complete-failure')
const complete = openStep(ctx, 'retry-invariant-complete-failure')
expect(() => {
complete.append('llm/retry', {
turn: 1,
@@ -126,7 +120,7 @@ describe('llm-retry invariants', () => {
['request-id-empty', { message: 'failed', code: 'RATE_LIMIT', requestId: '' }, /failure\.requestId/],
]
for (const [name, invalidFailure, message] of invalidFailures) {
const session = closeStep(ctx, `retry-invariant-failure-${name}`)
const session = openStep(ctx, `retry-invariant-failure-${name}`)
expect(() => {
session.append('llm/retry', {
turn: 1, step: 1, ...always, failure: invalidFailure,
@@ -150,95 +144,75 @@ describe('llm-retry invariants', () => {
['delay-type', { ...normal, delayMs: '1' }, /delayMs/],
])('rejects invalid retry data: %s', async (name, data, message) => {
const ctx = await setup()
const session = closeStep(ctx, `retry-invariant-${name}`)
const session = openStep(ctx, `retry-invariant-${name}`)
expect(() => {
session.append('llm/retry', { turn: 1, step: 1, ...data } as never)
}).toThrow(message)
})
it('rejects records outside the latest closed step of an open turn', async () => {
it('rejects records outside the currently open turn and step', async () => {
const ctx = await setup()
const absent = ctx.sessions.create(SessionId('retry-invariant-no-turn'))
expect(() => {
absent.append('llm/retry', { turn: 1, step: 1, ...normal })
}).toThrow(/inside an open turn/)
const wrongTurn = closeStep(ctx, 'retry-invariant-wrong-turn')
const wrongTurn = openStep(ctx, 'retry-invariant-wrong-turn')
expect(() => {
wrongTurn.append('llm/retry', { turn: 2, step: 1, ...normal })
}).toThrow(/open turn is 1/)
const openStep = ctx.sessions.create(SessionId('retry-invariant-open-step'))
openStep.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
openStep.append('step/start', { turn: 1, step: 1 })
const closedStep = openStep(ctx, 'retry-invariant-closed-step')
closedStep.append('step/end', { turn: 1, step: 1 })
expect(() => {
openStep.append('llm/retry', { turn: 1, step: 1, ...normal })
}).toThrow(/step 1 is still open/)
closedStep.append('llm/retry', { turn: 1, step: 1, ...normal })
}).toThrow(/inside an open step/)
const noStep = ctx.sessions.create(SessionId('retry-invariant-no-step'))
noStep.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
noStep.append('turn/start', { turn: 1 })
expect(() => {
noStep.append('llm/retry', { turn: 1, step: 1, ...normal })
}).toThrow(/latest closed step is undefined/)
}).toThrow(/inside an open step/)
const wrongStep = closeStep(ctx, 'retry-invariant-wrong-step')
const wrongStep = openStep(ctx, 'retry-invariant-wrong-step')
expect(() => {
wrongStep.append('llm/retry', { turn: 1, step: 2, ...normal })
}).toThrow(/latest closed step is 1/)
}).toThrow(/open step is 1\/1/)
const closedTurn = closeStep(ctx, 'retry-invariant-closed-turn')
closedTurn.append('turn/end', { turn: 1, reason: { kind: 'aborted' } })
const closedTurn = openStep(ctx, 'retry-invariant-closed-turn')
closedTurn.append('step/end', { turn: 1, step: 1 })
closedTurn.append('turn/end', { turn: 1, reason: { kind: 'aborted', reason: { kind: 'user' } },
})
expect(() => {
closedTurn.append('llm/retry', { turn: 1, step: 1, ...normal })
}).toThrow(/inside an open turn/)
})
it('rejects a second retry record for the same step', async () => {
it('accepts successive retries in one step and rejects skipped numbering', async () => {
const ctx = await setup()
const session = closeStep(ctx, 'retry-invariant-duplicate')
const session = openStep(ctx, 'retry-invariant-number-sequence')
session.append('llm/retry', { turn: 1, step: 1, ...normal })
session.append('llm/retry', { turn: 1, step: 1, ...normal, retry: 2 })
expect(() => {
session.append('llm/retry', { turn: 1, step: 1, ...normal, retry: 2 })
}).toThrow(/duplicates the retry record/)
session.append('llm/retry', { turn: 1, step: 1, ...always, retry: 2 })
}).toThrow(/must equal provider policy retry 1/)
})
it('binds retry numbering to the provider policy and resets it after success', async () => {
it('binds retry numbering to the provider policy and resets it for a new step', async () => {
const ctx = await setup()
const mismatch = closeStep(ctx, 'retry-invariant-numbering')
const mismatch = openStep(ctx, 'retry-invariant-numbering')
mismatch.append('llm/retry', { turn: 1, step: 1, ...normal })
mismatch.append('turn/end', { turn: 1, reason: { kind: 'error', step: 1, failure } })
mismatch.append('turn/start', { turn: 2, trigger: { kind: 'retry' } })
mismatch.append('step/start', { turn: 2, step: 1 })
mismatch.append('step/end', { turn: 2, step: 1 })
expect(() => {
mismatch.append('llm/retry', { turn: 2, step: 1, ...normal, retry: 1 })
mismatch.append('llm/retry', { turn: 1, step: 1, ...normal, retry: 1 })
}).toThrow(/must equal provider policy retry 2/)
const reset = closeStep(ctx, 'retry-invariant-reset')
const reset = openStep(ctx, 'retry-invariant-reset')
reset.append('llm/retry', { turn: 1, step: 1, ...normal })
reset.append('turn/end', { turn: 1, reason: { kind: 'error', step: 1, failure } })
reset.append('turn/start', { turn: 2, trigger: { kind: 'retry' } })
reset.append('step/start', { turn: 2, step: 1 })
reset.append('assistant/message', {
turn: 2,
step: 1,
message: createMessage({
role: 'assistant',
content: [{ type: 'text', text: 'success' }],
source: {
kind: 'model',
...{ provider: 'mock', model: 'mock' },
},
}),
}, { surfaceOp: 'append' })
reset.append('step/end', { turn: 2, step: 1 })
reset.append('turn/end', { turn: 2, reason: { kind: 'completed' } })
reset.append('turn/start', { turn: 3, trigger: { kind: 'message', source: { kind: 'user' } } })
reset.append('step/start', { turn: 3, step: 1 })
reset.append('step/end', { turn: 3, step: 1 })
reset.append('step/end', { turn: 1, step: 1 })
reset.append('step/start', { turn: 1, step: 2 })
expect(() => {
reset.append('llm/retry', { turn: 3, step: 1, ...normal })
reset.append('llm/retry', { turn: 1, step: 2, ...normal })
}).not.toThrow()
})
@@ -262,9 +236,7 @@ describe('llm-retry invariants', () => {
appendRetryTurn(nonFailureEnd, 2)
const missingStart = ctx.sessions.create(SessionId('retry-invariant-missing-start'))
missingStart.append('turn/end', {
turn: 1,
reason: { kind: 'error', step: 1, failure },
missingStart.append('turn/end', { turn: 1, reason: { kind: 'error', error: failure },
})
appendRetryTurn(missingStart, 2)
@@ -274,7 +246,7 @@ describe('llm-retry invariants', () => {
it('rejects a provider that does not match the failed request route', async () => {
const ctx = await setup()
const session = closeStep(ctx, 'retry-invariant-provider')
const session = openStep(ctx, 'retry-invariant-provider')
expect(() => {
session.append('llm/retry', { turn: 1, step: 1, ...always, provider: 'other' })
}).toThrow(/does not match the failed request provider mock/)
@@ -284,7 +256,7 @@ describe('llm-retry invariants', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
const session = ctx.sessions.create(SessionId('retry-invariant-late'))
session.append('step/end', { turn: 1, step: 1 })
session.append('step/start', { turn: 1, step: 1 })
session.append('llm/retry', { turn: 1, step: 1, ...normal })
await ctx.plugin(InvariantService)
await expect(ctx.plugin(RetryInvariant)).rejects.toThrow(/inside an open turn/)
@@ -32,13 +32,12 @@ describe.each(['jsonl', 'sqlite'] as const)('%s retry-event persistence', (kind)
const ctx = await backend(kind)
try {
const session = ctx.sessions.create(SessionId(`retry-${kind}`))
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
session.append('turn/start', { turn: 1 })
session.append('step/start', { turn: 1, step: 1 })
session.append('request/header', {
header: { config: { provider: 'mock', model: 'mock' } },
reason: 'initial',
})
session.append('step/end', { turn: 1, step: 1 })
const event = session.append('llm/retry', {
turn: 1,
step: 1,
@@ -49,13 +48,9 @@ describe.each(['jsonl', 'sqlite'] as const)('%s retry-event persistence', (kind)
delayMs: 750,
failure: { message: 'provider busy', code: 'RATE_LIMIT', status: 429 },
})
session.append('turn/end', {
turn: 1,
reason: {
kind: 'error',
step: 1,
failure: { message: 'provider busy', code: 'RATE_LIMIT', status: 429 },
},
session.append('step/end', { turn: 1, step: 1 })
session.append('turn/end', { turn: 1, reason: { kind: 'error', error: { message: 'provider busy', code: 'RATE_LIMIT', status: 429 },
},
})
expect(session.deriveMessages()).toEqual([])
+28 -30
View File
@@ -149,15 +149,8 @@ function alwaysConfig(backoff: BackoffConfig = {}): AlwaysRetryPolicyConfig {
}
}
function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
return new Promise((resolve) => {
const dispose = ctx.on('agent/status', (subject, status) => {
if (subject === agent && status === 'idle') {
dispose()
resolve()
}
})
})
function waitForIdle(_ctx: Context, agent: Agent): Promise<void> {
return agent.whenIdle()
}
function waitForRetry(ctx: Context, agent: Agent, retryNumber: number): Promise<Extract<SessionEvent, { type: 'llm/retry' }>> {
@@ -180,7 +173,7 @@ afterEach(async () => {
})
describe('provider-routed retry policy', () => {
it('records the scheduled delay before opening a fresh request attempt', async () => {
it('records the scheduled delay before retrying the request', async () => {
vi.useFakeTimers()
const adapter = new ScriptedAdapter([
new LlmError('busy', 'RATE_LIMIT', { status: 429 }),
@@ -219,7 +212,7 @@ describe('provider-routed retry policy', () => {
expect(adapter.requests).toHaveLength(2)
expect(agent.session.events.filter(item => item.type === 'step/start').map(item => item.data))
.toEqual([{ turn: 1, step: 1 }, { turn: 2, step: 1 }])
.toEqual([{ turn: 1, step: 1 }])
expect(agent.session.deriveMessages().at(-1)).toEqual({
id: expect.any(String) as unknown,
role: 'assistant',
@@ -256,7 +249,7 @@ describe('provider-routed retry policy', () => {
expect(agent.session.events.filter(event => event.type === 'assistant/message').map(event => ({
turn: event.data.turn,
step: event.data.step,
}))).toEqual([{ turn: 2, step: 1 }])
}))).toEqual([{ turn: 1, step: 1 }])
expect(agent.session.deriveMessages().at(-1)).toMatchObject({
role: 'assistant',
content: [{ type: 'text', text: 'recovered' }],
@@ -289,14 +282,21 @@ describe('provider-routed retry policy', () => {
await vi.advanceTimersByTimeAsync(500)
await idle
const retryEvent = agent.session.events.find(event => event.type === 'llm/retry')
const failedChunks = agent.session.events.filter(event =>
event.type === 'assistant/chunk' && event.data.turn === 1 && event.data.step === 1,
event.type === 'assistant/chunk'
&& retryEvent !== undefined
&& event.seq < retryEvent.seq,
)
expect(failedChunks).toHaveLength(6)
expect(agent.session.events.filter(event => event.type === 'assistant/message').map(event => ({
expect(failedChunks).toHaveLength(7)
const assistantMessages = agent.session.events.filter(event => event.type === 'assistant/message')
expect(assistantMessages.map(event => ({
turn: event.data.turn,
step: event.data.step,
}))).toEqual([{ turn: 2, step: 1 }])
}))).toEqual([{ turn: 1, step: 1 }])
expect(failedChunks.every(event =>
!assistantMessages[0]?.sourceEventSeqs?.includes(event.seq),
)).toBe(true)
expect(agent.session.events.some(event => event.type === 'tool/call')).toBe(false)
expect(toolExecutions).toBe(0)
expect(agent.session.deriveMessages().at(-1)).toMatchObject({
@@ -337,7 +337,7 @@ describe('provider-routed retry policy', () => {
expect(agent.session.events.filter(event => event.type === 'llm/retry')).toHaveLength(2)
expect(agent.session.events.at(-1)).toMatchObject({
type: 'turn/end',
data: { reason: { kind: 'error', failure: { message: 'busy three', code: 'SERVER' } } },
data: { reason: { kind: 'error', error: { message: 'busy three', code: 'SERVER' } } },
})
})
@@ -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: { reason: { kind: 'error', failure: { code: 'NO_ADAPTER' } } },
data: { 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 () => {
@@ -539,9 +543,9 @@ describe('provider-routed retry policy', () => {
backoff: { initialDelayMs: 1, maxDelayMs: 1 },
}),
}, (ctx) => {
ctx.on('agent/request', async (_agent, turn, _step, _signal, next) => ({
ctx.on('agent/request', async (_agent, _turn, _step, _signal, next) => ({
...await next(),
provider: turn === 1 ? 'mock' : 'other',
provider: adapter.requests.length === 0 ? 'mock' : 'other',
}))
}))
const agent = context.agentLoop.create(SessionId('retry-provider-budgets'), {
@@ -913,9 +917,7 @@ describe('provider-routed retry policy', () => {
const captured = Promise.withResolvers<undefined>()
let invokeCaptured: (() => Promise<void>) | undefined
const mounted = await harness(adapter, {}, (ctx) => {
ctx.on('agent/request-error', (
_agent, _turn, _step, _error, _failure, _history, _retryPolicy, _signal, next,
) => {
ctx.on('agent/request-error', (_agent, _context, _signal, next) => {
return new Promise<RequestErrorAction>((resolve) => {
invokeCaptured = async () => { resolve(await next()) }
captured.resolve(undefined)
@@ -924,9 +926,7 @@ describe('provider-routed retry policy', () => {
})
context = mounted.ctx
let downstreamCalls = 0
context.on('agent/request-error', async (
_agent, _turn, _step, _error, _failure, _history, _retryPolicy, _signal, next,
) => {
context.on('agent/request-error', async (_agent, _context, _signal, next) => {
downstreamCalls += 1
return next()
})
@@ -980,9 +980,7 @@ describe('provider-routed retry policy', () => {
textResponse('must not run'),
])
;({ ctx: context } = await harness(adapter, { mock: policy }, (ctx) => {
ctx.on('agent/request-error', async (
agent, _turn, _step, _error, _failure, _history, _retryPolicy, _signal, next,
) => {
ctx.on('agent/request-error', async (agent, _context, _signal, next) => {
agent.cancel({ kind: 'user' })
return next()
})
@@ -55,14 +55,8 @@ async function harness(
return ctx
}
function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
return new Promise((resolve) => {
const dispose = ctx.on('agent/status', (subject, status) => {
if (subject !== agent || status !== 'idle') return
dispose()
resolve()
})
})
function waitForIdle(_ctx: Context, agent: Agent): Promise<void> {
return agent.whenIdle()
}
function sendAndWait(ctx: Context, agent: Agent): Promise<void> {
@@ -109,15 +103,15 @@ describe('bounded retry through the real DeepSeek HTTP/SSE adapter', () => {
expect(server?.requests).toHaveLength(1)
expect(agent.session.events.filter(event => event.type === 'step/start')
.map(event => [event.data.turn, event.data.step]))
.toEqual([[1, 1], [2, 1]])
.toEqual([[1, 1]])
expect(agent.session.events.filter(event => event.type === 'llm/retry').map(event => event.data.failure.code))
.toEqual(['TRANSPORT'])
expect(finalAssistantText(agent)).toBe('connected after retry')
})
it.each([
['stream_disconnect', 0] as const,
['partial_disconnect', 2] as const,
['stream_disconnect', 1] as const,
['partial_disconnect', 3] as const,
])('retries %s without committing failed chunks', async (behavior, failedChunkCount) => {
const server = await start([behavior, 'success'], {
apiKey: 'mock-key',
@@ -136,12 +130,15 @@ describe('bounded retry through the real DeepSeek HTTP/SSE adapter', () => {
expect(server.requests).toHaveLength(2)
expect(server.requests[0]?.body).toEqual(server.requests[1]?.body)
const retryEvent = agent.session.events.find(event => event.type === 'llm/retry')
expect(agent.session.events.filter(event =>
event.type === 'assistant/chunk' && event.data.turn === 1,
event.type === 'assistant/chunk'
&& retryEvent !== undefined
&& event.seq < retryEvent.seq,
)).toHaveLength(failedChunkCount)
expect(agent.session.events.filter(event => event.type === 'assistant/message')
.map(event => [event.data.turn, event.data.step]))
.toEqual([[2, 1]])
.toEqual([[1, 1]])
expect(agent.session.events.filter(event => event.type === 'llm/retry').map(event => event.data.failure.code))
.toEqual(['TRANSPORT'])
expect(finalAssistantText(agent)).toBe('recovered response')
@@ -166,7 +163,7 @@ describe('bounded retry through the real DeepSeek HTTP/SSE adapter', () => {
.toEqual(['EMPTY_RESPONSE'])
expect(agent.session.events.filter(event => event.type === 'assistant/message')
.map(event => [event.data.turn, event.data.step]))
.toEqual([[2, 1]])
.toEqual([[1, 1]])
expect(agent.session.events.at(-1)).toMatchObject({
type: 'turn/end',
data: { reason: { kind: 'completed' } },
@@ -191,12 +188,12 @@ describe('bounded retry through the real DeepSeek HTTP/SSE adapter', () => {
expect(server.requests).toHaveLength(1)
expect(agent.session.events.filter(event =>
event.type === 'assistant/chunk' && event.data.turn === 1,
)).toHaveLength(2)
)).toHaveLength(3)
expect(agent.session.events.some(event => event.type === 'assistant/message')).toBe(false)
expect(agent.session.events.some(event => event.type === 'llm/retry')).toBe(false)
expect(agent.session.events.at(-1)).toMatchObject({
type: 'turn/end',
data: { reason: { kind: 'error', failure: { code: 'STREAM_CLOSED' } } },
data: { reason: { kind: 'error', error: { message: 'SSE stream ended without [DONE]', code: 'STREAM_CLOSED' } } },
})
})
@@ -234,11 +231,15 @@ describe('bounded retry through the real DeepSeek HTTP/SSE adapter', () => {
await sendAndWait(context, agent)
expect(server.requests).toHaveLength(3)
expect(agent.session.events.filter(event => event.type === 'step/start')).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: { reason: { kind: 'error', failure: { code: 'TRANSPORT' } } },
data: { 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')
}
})
})