第二章:核心类型系统 -- 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 就是这样添加了 bashExecution、custom、branchSummary、compactionSummary 等消息类型。
消息转换
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 -- 可执行的工具
AgentTool 在 ai 包的 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 循环"如何运行"的配置对象:
| 配置项 | 类型 | 说明 |
|---|---|---|
model | Model | 使用哪个模型 |
convertToLlm | 函数 | AgentMessage[] → Message[],必须提供 |
transformContext | 函数 | 可选,在 convertToLlm 前转换消息(如裁剪上下文) |
getApiKey | 函数 | 可选,动态获取 API key |
getSteeringMessages | 函数 | 可选,返回插队消息 |
getFollowUpMessages | 函数 | 可选,返回后续消息 |
beforeToolCall | 函数 | 可选,工具执行前的钩子 |
afterToolCall | 函数 | 可选,工具执行后的钩子 |
toolExecution | "sequential" | "parallel" | 工具执行策略,默认 parallel |
reasoning | string | 思考级别 |
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,看核心循环是如何工作的。