| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146 |
- import { describe, expect, it } from 'vitest'
- import { Context } from '@deepseek-ai/cordis'
- import AgentRegistry from '@deepseek-ai/dsh-agent'
- import AgentLoop from '@deepseek-ai/dsh-agent-loop'
- import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
- import LlmRuntime, { createUserMessage, LlmError } from '@deepseek-ai/dsh-llm'
- import type { LlmFailure, ResolvedRetryPolicy } from '@deepseek-ai/dsh-llm'
- import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
- import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
- import ToolRuntime from '@deepseek-ai/dsh-tools'
- import { MockAdapter, textResponse } from './mock-adapter.ts'
- async function harness(adapter: MockAdapter): Promise<Context> {
- const ctx = new Context()
- await ctx.plugin(LlmRuntime)
- await ctx.plugin(SessionStore)
- await ctx.plugin(SessionProjectionRegistry)
- await ctx.plugin(SystemPrompt)
- await ctx.plugin(ToolRuntime)
- await ctx.plugin(AgentRegistry)
- await ctx.plugin(AgentLoop, { agents: [] })
- ctx.llm.registerAdapter(['mock'], adapter)
- return ctx
- }
- function fail(message: string, code: string): () => never {
- return () => {
- throw new LlmError(message, code)
- }
- }
- describe('agent/request-error', () => {
- it('does not offer middleware failures to request recovery', async () => {
- const adapter = new MockAdapter([textResponse('unused')])
- const ctx = await harness(adapter)
- const agent = await ctx.agentLoop.create(SessionId('request-error-narrow'), { provider: 'mock', model: 'mock' })
- let recoveries = 0
- ctx.on('agent/request', () => {
- throw new LlmError('middleware failed', 'MIDDLEWARE')
- })
- ctx.on('agent/request-error', async () => {
- recoveries += 1
- })
- agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }))
- await agent.whenIdle()
- expect(recoveries).toBe(0)
- expect(adapter.requests).toHaveLength(0)
- })
- it('lets each failed request return a retry action before its turn closes', async () => {
- const adapter = new MockAdapter([
- fail('busy', 'RATE_LIMIT'),
- fail('unavailable', 'SERVICE_UNAVAILABLE'),
- textResponse('ok'),
- ])
- const ctx = await harness(adapter)
- const agent = await ctx.agentLoop.create(SessionId('request-error-retry'), { provider: 'mock', model: 'mock' })
- const seen: {
- turn: number
- step: number
- failure: LlmFailure
- retryPolicy: ResolvedRetryPolicy | undefined
- }[] = []
- const statuses: string[] = []
- ctx.on('agent/status', ({ agent: subject, status }) => {
- if (subject === agent) statuses.push(status)
- })
- ctx.on('agent/request-error', async ({ agent: subject, turn, step, failure, retryPolicy }) => {
- expect(subject).toBe(agent)
- seen.push({ turn, step, failure, retryPolicy })
- return { kind: 'retry' }
- })
- agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }))
- await agent.whenIdle()
- expect(seen.map(item => ({
- turn: item.turn,
- step: item.step,
- code: item.failure.code,
- }))).toEqual([
- {
- turn: 1,
- step: 1,
- code: 'RATE_LIMIT',
- },
- {
- turn: 1,
- step: 1,
- code: 'SERVICE_UNAVAILABLE',
- },
- ])
- expect(agent.session.snapshotEvents().filter(event => event.type === 'turn/start')).toHaveLength(1)
- expect(seen.map(item => item.retryPolicy)).toEqual([
- expect.objectContaining({ mode: 'normal' }),
- expect.objectContaining({ mode: 'normal' }),
- ])
- expect(statuses).toEqual(['running', 'idle'])
- expect(agent.session.snapshotEvents().flatMap(event =>
- event.type === 'request/header' ? [event.data.reason] : [])).toEqual(['initial'])
- })
- it('lets cancellation win over a retry action', async () => {
- const adapter = new MockAdapter([fail('busy', 'RATE_LIMIT'), textResponse('unused')])
- const ctx = await harness(adapter)
- const agent = await ctx.agentLoop.create(SessionId('request-error-cancel'), { provider: 'mock', model: 'mock' })
- ctx.on('agent/request-error', async ({ agent: subject }) => {
- subject.cancel({ kind: 'user' })
- return { kind: 'retry' }
- })
- agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }))
- await agent.whenIdle()
- expect(adapter.requests).toHaveLength(1)
- expect(agent.session.snapshotEvents().filter(event => event.type === 'turn/start')).toHaveLength(1)
- expect(agent.session.snapshotEvents().find(event => event.type === 'turn/end')).toMatchObject({
- type: 'turn/end',
- data: { reason: { kind: 'aborted', reason: { kind: 'user' } } },
- })
- })
- it('does not retry when the recovery listener fails before returning its action', async () => {
- const adapter = new MockAdapter([fail('busy', 'RATE_LIMIT'), textResponse('unused')])
- const ctx = await harness(adapter)
- const agent = await ctx.agentLoop.create(SessionId('request-error-recovery-failed'), {
- provider: 'mock',
- model: 'mock',
- })
- ctx.on('agent/request-error', async () => {
- throw new Error('recovery failed')
- })
- agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }))
- await agent.whenIdle()
- expect(adapter.requests).toHaveLength(1)
- expect(agent.session.snapshotEvents().filter(event => event.type === 'turn/start')).toHaveLength(1)
- expect(agent.session.snapshotEvents().find(event => event.type === 'turn/end')).toMatchObject({
- type: 'turn/end',
- data: { reason: { kind: 'error' } },
- })
- })
- })
|