Skip to content

[bug] Monitor 'error' 事件在无监听器时触发 Node.js ERR_UNHANDLED_ERROR #3

@2217173240

Description

@2217173240

[bug] Monitor 'error' 事件在无监听器时触发 Node.js ERR_UNHANDLED_ERROR

报告人 / Reporter

  • GitHub 用户名(期望):2217173240
  • 邮箱:alanturing648@gmail.com

环境 (Environment)

  • @shareai-lab/kode-sdk 版本:2.7.0external/Kode-agent-sdk/package.json
  • Node.js:>= 18

背景 (Background)

Kode SDK 的错误处理文档 docs/ERROR_HANDLING.md 中的目标之一是:

  1. 程序永不崩溃 - 多层错误捕获,确保系统稳定运行

工具执行失败时,Agent 会发出 MonitorEvent,其中一种类型是:

// external/Kode-agent-sdk/src/core/agent.ts:1231+
this.events.emitMonitor({
  channel: 'monitor',
  type: 'error',
  severity: 'warn',
  phase: 'tool',
  message: errorMessage,
  detail: { ...outcome.content, errorType, retryable: isRetryable },
});

EventBus 在发出 Monitor 事件时,会直接用 event.type 作为 Node.js EventEmitter 的 event name:

// external/Kode-agent-sdk/src/core/events.ts:58-66
emitMonitor(event: MonitorEvent): AgentEventEnvelope<MonitorEvent> {
  const envelope = this.emit('monitor', event) as AgentEventEnvelope<MonitorEvent>;
  this.monitorEmitter.emit(event.type, envelope.event);
  this.notifySubscribers('monitor', envelope);
  return envelope;
}

event.type === 'error' 且未注册任何 'error' 监听器时,Node.js 会将其视为“未处理的 error 事件”,触发 ERR_UNHANDLED_ERROR,直接导致进程崩溃。

复现步骤 (Steps to Reproduce)

  1. 在 Node.js 中创建最小化环境,只使用 Kode SDK,不注册任何 error 监听器:

    import {
      Agent,
      JSONStore,
      AgentTemplateRegistry,
      ToolRegistry,
      SandboxFactory,
    } from '@shareai-lab/kode-sdk';
    
    const store = new JSONStore('./.kode');
    const templates = new AgentTemplateRegistry();
    const tools = new ToolRegistry();
    const sandboxFactory = new SandboxFactory();
    
    // 注册一个必然失败的工具
    tools.register('failing_tool', () => ({
      name: 'failing_tool',
      description: 'Always fail',
      async exec() {
        throw new Error('intentional failure');
        // 或者:
        // return { ok: false, error: 'intentional failure' };
      },
    }));
    
    templates.register({
      id: 'test-agent',
      systemPrompt: 'test',
      tools: ['failing_tool'],
      model: 'dummy-model',
    });
    
    (async () => {
      const agent = await Agent.create(
        {
          agentId: 'agt_test',
          templateId: 'test-agent',
          model: {} as any, // stub ModelProvider
        },
        {
          store,
          templateRegistry: templates,
          toolRegistry: tools,
          sandboxFactory,
        }
      );
    
      // 注意:不注册 agent.on('error', ...)
      await agent.complete([
        { role: 'user', content: [{ type: 'text', text: 'trigger failing_tool' }] } as any,
      ]);
    })();
  2. 工具执行失败后,processToolCall 会调用 this.events.emitMonitor({ type: 'error', ... })

  3. EventBus.emitMonitor 再执行 this.monitorEmitter.emit('error', envelope.event)

  4. 因为未注册任何 'error' 监听器,Node.js 抛出 ERR_UNHANDLED_ERROR: Unhandled 'error' event,进程直接退出。

实际行为 (Actual Behavior)

  • 未注册 agent.on('error', handler) 的情况下,任何工具失败都可能触发 monitorEmitter.emit('error', ...),并在 Node.js 中引发 ERR_UNHANDLED_ERROR,导致进程崩溃。
  • 这与文档中“程序永不崩溃”的设计目标不一致,对集成方来说是一个隐藏的 footgun(必须知道要显式注册 'error',否则进程会挂)。

预期行为 (Expected Behavior)

  • 即使集成方没有注册 agent.on('error', ...),SDK 仍应保证:
    • 进程不会因为 MonitorEvent.type === 'error' 而触发 Node 层的 ERR_UNHANDLED_ERROR
    • 错误可以通过其他方式(如 agent.subscribe([...])Store、日志)被观察和处理。
  • 注册 agent.on('error', handler) 应该是“增强监控”的选项,而不是“避免进程崩溃的硬性前提”。

影响范围 (Impact)

  • 所有没有显式注册 agent.on('error', ...) 的集成方,只要工具执行失败,就有可能因为 monitor 'error' 事件导致主进程崩溃。
  • 在实际业务项目中(商用车资料检索 Agent),早期版本就出现过“工具逻辑错误 → MonitorEvent 'error' → 进程崩溃”的链路,目前只能通过在业务层统一注册 agent.on('error', ...) 来规避。

建议修复方向 (Suggested Fix)

  1. 避免使用 'error' 作为 Node EventEmitter 的 event name,例如在 EventBus.emitMonitor 中统一使用 'monitor_error' 作为 event name,将 MonitorEvent.type 保持为 'error' 即可。

  2. 或者在 EventBus 构造函数中为 monitorEmitter 预先注册一个空的 'error' 监听器,屏蔽 Node 的特殊处理语义:

    this.monitorEmitter.on('error', () => {
      // no-op or re-route to logging
    });
  3. 同时在文档中明确说明:

    • agent.on('error', ...) 是推荐的监控方式;
    • 即使没有注册该监听器,SDK 也不会因为工具失败而让进程崩掉。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions