fix(agent-loop): harden lifecycle edge cases
This commit is contained in:
5 files changed
+167
-21
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import LlmService, { StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import LlmService, { CallId, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { TurnEndReason } from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
@@ -406,6 +406,70 @@ describe('agent loop', () => {
|
||||
expect(reasons).toEqual([{ kind: 'max-tokens' }, { kind: 'completed' }])
|
||||
})
|
||||
|
||||
it('does not dispatch tool calls from a max-tokens-truncated step', async () => {
|
||||
const callId = CallId('c1')
|
||||
const adapter = new MockAdapter([[
|
||||
{ type: 'block-start', index: 0, blockType: 'tool-call' },
|
||||
{ type: 'tool-call-delta', index: 0, id: callId, name: 'echo', argumentsDelta: '{"text":"x"}' },
|
||||
{ type: 'block-end', index: 0, block: { type: 'tool-call', id: callId, name: 'echo', arguments: '{"text":"x"}' } },
|
||||
{ type: 'usage', usage: { inputTokens: 10, outputTokens: 5 } },
|
||||
{ type: 'finish', reason: { kind: 'max-tokens' } },
|
||||
]])
|
||||
const ctx = await harness(adapter)
|
||||
let executions = 0
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'echo',
|
||||
description: '',
|
||||
parameters: { text: { type: 'string' } },
|
||||
async execute() {
|
||||
executions += 1
|
||||
return [{ type: 'text', text: 'should not run' }]
|
||||
},
|
||||
}))
|
||||
const agent = ctx.agentLoop.create('a1', { model: 'mock' })
|
||||
|
||||
const reasons: TurnEndReason[] = []
|
||||
ctx.on('agent/turn-end', (_agent, _turn, reason) => void reasons.push(reason))
|
||||
|
||||
send(agent, 'go')
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
expect(executions).toBe(0)
|
||||
expect(agent.session.events.some(e => e.type === 'tool/call')).toBe(false)
|
||||
expect(agent.session.deriveMessages()).toEqual([
|
||||
{ role: 'user', content: [{ type: 'text', text: 'go' }] },
|
||||
])
|
||||
expect(reasons).toEqual([{ kind: 'max-tokens' }])
|
||||
})
|
||||
|
||||
it('stops the turn when agent/step-end listener failure has recorded an error', async () => {
|
||||
const adapter = new MockAdapter([
|
||||
toolCallResponse('c1', 'echo', { text: 'x' }),
|
||||
textResponse('should not run'),
|
||||
])
|
||||
const ctx = await harness(adapter)
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'echo',
|
||||
description: '',
|
||||
parameters: { text: { type: 'string' } },
|
||||
async execute(args) {
|
||||
return [{ type: 'text', text: String(args.text) }]
|
||||
},
|
||||
}))
|
||||
const agent = ctx.agentLoop.create('a1', { model: 'mock' })
|
||||
let threw = false
|
||||
ctx.on('agent/step-end', () => {
|
||||
if (!threw) { threw = true; throw new Error('bad step-end listener') }
|
||||
})
|
||||
|
||||
send(agent, 'go')
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
expect(adapter.requests).toHaveLength(1)
|
||||
const turnEnd = agent.session.events.findLast(e => e.type === 'turn/end')
|
||||
expect(turnEnd?.type === 'turn/end' && turnEnd.data.reason.kind).toBe('error')
|
||||
})
|
||||
|
||||
it('chains queued messages into consecutive turns', async () => {
|
||||
const adapter = new MockAdapter([textResponse('first'), textResponse('second')])
|
||||
const ctx = await harness(adapter)
|
||||
|
||||
Reference in New Issue
Block a user