fix: propagate error events in AgentRunner.run() (#98)

run() only handled 'done' events from stream(), silently dropping
'error' events. This caused failed LLM calls to return an empty
RunResult that the caller treated as successful.
This commit is contained in:
JackChen 2026-04-12 22:16:33 +08:00
parent ced1d90a93
commit 9a446b8796
2 changed files with 87 additions and 0 deletions

View File

@ -490,6 +490,8 @@ export class AgentRunner {
for await (const event of this.stream(messages, options)) { for await (const event of this.stream(messages, options)) {
if (event.type === 'done') { if (event.type === 'done') {
Object.assign(accumulated, event.data) Object.assign(accumulated, event.data)
} else if (event.type === 'error') {
throw event.data
} }
} }

View File

@ -0,0 +1,85 @@
import { describe, it, expect } from 'vitest'
import { Agent } from '../src/agent/agent.js'
import { AgentRunner } from '../src/agent/runner.js'
import { ToolRegistry } from '../src/tool/framework.js'
import { ToolExecutor } from '../src/tool/executor.js'
import type { AgentConfig, LLMAdapter, LLMMessage } from '../src/types.js'
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Adapter whose chat() always throws. */
function errorAdapter(error: Error): LLMAdapter {
return {
name: 'error-mock',
async chat(_messages: LLMMessage[]) {
throw error
},
async *stream() {
/* unused */
},
}
}
function buildAgentWithAdapter(config: AgentConfig, adapter: LLMAdapter) {
const registry = new ToolRegistry()
const executor = new ToolExecutor(registry)
const agent = new Agent(config, registry, executor)
const runner = new AgentRunner(adapter, registry, executor, {
model: config.model,
systemPrompt: config.systemPrompt,
maxTurns: config.maxTurns,
agentName: config.name,
})
;(agent as any).runner = runner
return agent
}
const baseConfig: AgentConfig = {
name: 'test-agent',
model: 'mock-model',
systemPrompt: 'You are a test agent.',
}
// ---------------------------------------------------------------------------
// Tests — #98: AgentRunner.run() must propagate errors from stream()
// ---------------------------------------------------------------------------
describe('AgentRunner.run() error propagation (#98)', () => {
it('LLM adapter error surfaces as success:false in AgentRunResult', async () => {
const apiError = new Error('API 500: internal server error')
const agent = buildAgentWithAdapter(baseConfig, errorAdapter(apiError))
const result = await agent.run('hello')
expect(result.success).toBe(false)
expect(result.output).toContain('API 500')
})
it('AgentRunner.run() throws when adapter errors', async () => {
const apiError = new Error('network timeout')
const adapter = errorAdapter(apiError)
const registry = new ToolRegistry()
const executor = new ToolExecutor(registry)
const runner = new AgentRunner(adapter, registry, executor, {
model: 'mock-model',
systemPrompt: 'test',
agentName: 'test',
})
await expect(
runner.run([{ role: 'user', content: [{ type: 'text', text: 'hi' }] }]),
).rejects.toThrow('network timeout')
})
it('agent transitions to error state on LLM failure', async () => {
const agent = buildAgentWithAdapter(baseConfig, errorAdapter(new Error('boom')))
await agent.run('hello')
expect(agent.getState().status).toBe('error')
})
})