第二章:核心类型系统 -- AgentMessage、AgentTool、AgentEvent

对应源文件:packages/agent/src/types.ts

1. AgentMessage -- 可扩展的消息类型

基础定义

type AgentMessage = Message | CustomAgentMessages[keyof CustomAgentMessages];

翻译成人话:AgentMessage 是 "LLM 标准消息" 加上 "你自定义的消息"。

如何添加自定义消息

TypeScript 有一个叫 declaration merging(声明合并) 的特性。你可以在自己的代码中"扩展"别人定义的接口:

// 在你的应用代码中写:
declare module "@mariozechner/agent" {
  interface CustomAgentMessages {
    // 你的自定义消息类型
    notification: { role: "notification"; text: string; timestamp: number };
    bashExecution: { role: "bashExecution"; command: string; output: string; timestamp: number };
  }
}

这样 AgentMessage 就自动变成了:

AgentMessage = UserMessage | AssistantMessage | ToolResultMessage
             | NotificationMessage | BashExecutionMessage

pi 的 coding-agent 就是这样添加了 bashExecutioncustombranchSummarycompactionSummary 等消息类型。

消息转换

LLM 不认识你的自定义消息,所以发给 LLM 前需要转换。这就是 convertToLlm 的作用:

convertToLlm: (messages: AgentMessage[]) => Message[]

例如,把 bashExecution 消息转成 LLM 能理解的 UserMessage

convertToLlm: (messages) => messages.flatMap(m => {
  if (m.role === "bashExecution") {
    // 转换为用户消息
    return [{ role: "user", content: `命令: ${m.command}\n输出: ${m.output}`, timestamp: m.timestamp }];
  }
  if (m.role === "notification") {
    // UI 专用消息,不发给 LLM
    return [];
  }
  // 标准消息直接通过
  return [m];
})

2. AgentTool -- 可执行的工具

AgentToolai 包的 Tool 基础上增加了执行能力

Tool(ai 包)                     AgentTool(agent 包)
├── name: "read"                  ├── name: "read"
├── description: "读取文件"        ├── description: "读取文件"
├── parameters: Schema            ├── parameters: Schema
                                  ├── label: "read"            ← 新增:UI 显示名称
                                  ├── execute(id, args)        ← 新增:实际执行函数
                                  ├── prepareArguments(args)   ← 新增:参数预处理
                                  └── executionMode: "parallel" ← 新增:执行策略

Tool 和 AgentTool 的区别

Tool 只是一个"描述":告诉 LLM "这个工具叫什么,参数是什么"。它没有执行逻辑。

AgentTool 有完整的执行能力。当 AI 说"我要调用 read 工具"时,agent 包调用 tool.execute() 实际执行。

execute 函数

execute: (
  toolCallId: string,           // 这次调用的唯一 ID
  params: { path: string },     // 经过验证的参数
  signal?: AbortSignal,         // 用于取消
  onUpdate?: (partial) => void, // 流式进度回调
) => Promise<AgentToolResult>

返回值 AgentToolResult 包含:

  • content:返回给 LLM 的文本/图片内容
  • details:任意结构化数据(给 UI 用,不发给 LLM)
  • terminate:是否建议提前终止循环

executionMode -- 并行 vs 串行

当 AI 一次返回多个工具调用(比如同时 read 三个文件)时:

模式行为
"parallel"三个 read 同时执行(默认)
"sequential"一个接一个执行

全局默认是 parallel,但单个工具可以覆盖。比如 bash 工具通常设为 sequential(避免并发命令冲突)。

prepareArguments -- 参数兼容性

LLM 有时候会生成不太标准的参数。prepareArguments 让你在验证前"修正"参数:

prepareArguments: (args) => {
  // LLM 有时用 "file_path" 而不是 "path"
  if (args.file_path && !args.path) {
    return { ...args, path: args.file_path };
  }
  return args;
}

3. AgentEvent -- 循环过程中的事件

agent 包在循环的每个关键节点发出事件。这些事件构成了 UI 更新的基础。

事件类型

Agent 生命周期
├── agent_start        // agent 开始处理
└── agent_end          // agent 结束,返回所有新消息

Turn 生命周期(一个 turn = 一次 LLM 调用 + 可能的工具执行)
├── turn_start         // 新一轮开始
└── turn_end           // 本轮结束

Message 生命周期
├── message_start      // 消息开始(用户、AI 或工具结果)
├── message_update     // AI 消息流式更新(token by token)
└── message_end        // 消息结束

Tool 执行生命周期
├── tool_execution_start   // 工具开始执行
├── tool_execution_update  // 工具执行中的进度更新
└── tool_execution_end     // 工具执行完成

一次典型的事件序列

用户说"读取 main.ts":

1. agent_start
2. turn_start
3. message_start   (user: "读取 main.ts")
4. message_end     (user: "读取 main.ts")
5. message_start   (assistant: 开始流式输出)
6. message_update  (assistant: thinking "用户想读取...")
7. message_update  (assistant: toolCall read({path: "main.ts"}))
8. message_end     (assistant: 完整回复)
9. tool_execution_start  (read, {path: "main.ts"})
10. tool_execution_end   (read, 文件内容)
11. message_start  (toolResult: 文件内容)
12. message_end    (toolResult: 文件内容)
13. turn_end
14. turn_start     // 第二轮开始
15. message_start  (assistant: "这个文件是...")
16. message_update (assistant: 流式输出文本)
17. message_end    (assistant: 完整的分析回复)
18. turn_end
19. agent_end      // 返回所有新消息

4. AgentLoopConfig -- 循环配置

这是告诉 agent 循环"如何运行"的配置对象:

配置项类型说明
modelModel使用哪个模型
convertToLlm函数AgentMessage[] → Message[],必须提供
transformContext函数可选,在 convertToLlm 前转换消息(如裁剪上下文)
getApiKey函数可选,动态获取 API key
getSteeringMessages函数可选,返回插队消息
getFollowUpMessages函数可选,返回后续消息
beforeToolCall函数可选,工具执行前的钩子
afterToolCall函数可选,工具执行后的钩子
toolExecution"sequential" | "parallel"工具执行策略,默认 parallel
reasoningstring思考级别

beforeToolCall 和 afterToolCall

这两个钩子让你在工具执行前后介入:

beforeToolCall -- 可以阻止工具执行:

beforeToolCall: async ({ toolCall, args }) => {
  // 用户确认机制
  if (toolCall.name === "bash" && args.command.includes("rm")) {
    const confirmed = await askUser("确定要执行删除命令吗?");
    if (!confirmed) return { block: true, reason: "用户拒绝了删除操作" };
  }
  return undefined; // 允许执行
}

afterToolCall -- 可以修改工具结果:

afterToolCall: async ({ toolCall, result }) => {
  // 截断过长的输出
  const text = result.content[0]?.text;
  if (text && text.length > 100000) {
    return { content: [{ type: "text", text: text.slice(0, 100000) + "\n[已截断]" }] };
  }
  return undefined; // 保持原样
}

5. AgentState -- 公开的状态

Agent 类对外暴露的状态快照:

属性说明
systemPrompt当前系统提示
model当前使用的模型
thinkingLevel思考级别
tools可用工具列表
messages对话历史
isStreaming是否正在处理中
streamingMessage当前正在流式生成的消息
pendingToolCalls正在执行的工具调用 ID 集合
errorMessage最近一次错误信息

小结

AgentMessage ──── 可扩展的消息类型
     │              通过 declaration merging 添加自定义消息
     │              发送前通过 convertToLlm 转换
     │
AgentTool ─────── 可执行的工具
     │              name + description + parameters + execute()
     │              支持 parallel/sequential 执行模式
     │
AgentEvent ────── 生命周期事件
     │              agent > turn > message > tool 四层嵌套
     │
AgentLoopConfig ─ 循环配置
                    model + convertToLlm + 钩子们

下一章我们会深入 agent-loop.ts,看核心循环是如何工作的。