Agent Engineering 101|05|Messages and Events

Agent 系统需要同时区分持久消息模型和运行时事件模型。Message 是状态事实,Event 是过程事实。

Agent Engineering 101|05|Messages and Events

核心结论

配套代码仓库: github.com/llm-101/mini-agent-harness

读完本文,你应该能回答

  • Message 和 Event 的区别是什么?
  • 为什么 UI 不应该直接读取内部运行时状态?
  • 工具调用、工具结果、文本增量分别应该进入哪种模型?
  • 事件流如何让 CLI/TUI/日志共享同一套运行过程?

本篇在系列中的位置

  • 上一篇:04 Agent Loop 展示了多轮行动。
  • 本篇:本文区分 loop 中哪些是持久事实,哪些只是过程信号。
  • 下一篇:06 Session Store 会说明这些持久事实如何落盘和恢复。

贯穿例子

本系列会反复使用同一个任务来连接各章:

用户说:“帮我修复这个 repo 里的 failing tests。”

在这个任务里,Messages 记录“用户要求、模型决定、工具结果、最终回复”这些会话事实;Events 则记录“正在读文件、测试开始、输出增量、工具结束”这些过程信号。读者要关注的是:状态事实要能恢复,过程信号要能展示,但两者不能混在一起。

  • Message 是可持久化的对话状态;Event 是运行时发生的过程信号。二者分离后,session 可以保持干净,产品外壳也能实时渲染执行过程。
  • 如果把所有东西都当作 chat message,UI 进度、工具动画、usage、compaction、thinking 状态都会污染会话历史。
  • mini-agent-harness 中,Message 和 Event 被分开建模,是为了让恢复逻辑和 UI 展示各走各的边界。

定义

Message 是可持久化的对话状态;Event 是运行时发生的过程信号。二者分离后,session 可以保持干净,产品外壳也能实时渲染执行过程。

为什么要单独看这一层?

长任务里既有需要保存的事实,也有只用于展示的过程。如果二者不分开,恢复会话会带上 UI 噪音;如果只保存事实,用户又看不到 Agent 正在做什么。

Message Model vs Event Model

边界

这一层的职责可以拆成几个稳定部分:

  • AgentMessage:system/user/assistant/tool 四类持久消息
  • AssistantContent:text 与 toolCall 分离
  • ToolResultMessage:工具结果包含 toolCallId、toolName、isError
  • AgentEvent:turn、message、tool、usage、compaction 过程事件
  • Product Shell:TUI/print mode 消费事件流
  • Session Store:只保存最终状态,不保存动画细节

这一层的边界可以用一个问题检验:如果它的内部实现变化,模型适配、工具执行、状态存储和产品外壳是否都不需要跟着重写?如果答案是否定的,说明这个边界还没有真正收束变化。

代码锚点

本篇主要对应这些模块:

  • src/types.ts
  • src/core/agent-events.ts
  • src/cli/components/MessageList.tsx
  • src/cli/print-mode.ts

阅读代码时建议先看类型,再看运行路径。

类型定义告诉你这一层暴露什么 contract;运行路径告诉你这个 contract 在 Agent 执行中何时被消费、何时被写回、何时被产品层看见。

运行流程

From Stream Delta to Durable Message

一次典型执行可以概括为:

  1. Model Delta:textDelta / usage
  2. AgentEvent:message_update / usage
  3. UI Render:Markdown、spinner、status
  4. Message End:AssistantMessage
  5. Persist:append to JSONL
  • State Facts:messages
  • Process Facts:events
  • View Model:ChatMessage

这里最容易被忽略的是“中间态”。生产级 Agent 不是只关心最终答案;它还要在运行过程中展示进度、捕获错误、记录 usage、允许取消,并把可恢复状态写回 session。

读者抓手:Message、Event、UI Render 不要混在一起

层次 代表什么 是否持久化 例子
Message 会话事实 user message、assistant tool call、tool result
Event 运行过程 通常不作为对话事实 text delta、tool started、tool finished、usage update
UI Render 产品展示 spinner、进度条、折叠面板、状态栏

如果把 Event 当 Message 存,会话会被 UI 噪音污染;如果只存 Message 不发 Event,用户又看不到长任务正在发生什么。

可迁移伪实现:消息与事件分离

下面的伪代码是机制抽象,不对应真实 API 或文件结构。它只用来说明这一层的控制点:

type AgentMessage = SystemMessage | UserMessage | AssistantMessage | ToolResultMessage;
type AgentEvent = TurnEvent | MessageEvent | ToolEvent | UsageEvent | CompactionEvent;

// Event renders progress; Message persists state.

这个草图的价值在于说明控制点,而不是提供可复制的库代码。

真正的工程实现还要处理错误、取消、并发、token 预算、日志、权限、序列化和 provider 差异。

工程原则

将这一层从 Agent 系统中拆出来,通常带来四个直接收益。

第一,可替换。

外部系统、模型 provider、工具集合或产品外壳变化时,核心运行时不必整体重写。

第二,可观测。

边界清晰后,事件、trace、usage、错误和状态迁移都有稳定落点。

第三,可恢复。

只要状态写入 session,运行时就可以在进程重启、工具失败或长任务中断后继续推理。

第四,可治理。

权限、脱敏、审批、路径保护和执行策略可以放在稳定 hook 或 runtime boundary 上,而不是只靠 prompt 约束。

和 Agent Harness 的关系

Agent = Model + Harness 这个公式的重点,不是把模型之外的所有东西都称为“工程杂活”。

相反,它提示我们:模型之外存在一套必须被设计的运行时系统。

Messages and Events 就是这套系统中的一个切面。它不替代模型能力,也不替代产品体验;它让模型能力可以被组织成可执行、可观察、可恢复、可治理的任务流程。

小结

Messages and Events 的核心价值,是把一类容易扩散的复杂性收束到明确边界中。

mini-agent-harness 中,这个边界被刻意写得较小,方便阅读和教学。但它对应的问题并不小:只要一个 Agent 要长期运行、调用工具、管理上下文、支持 UI、保存状态并处理失败,这个边界就会出现。

下一步可以继续沿着系列计划,把这些边界组合成完整 Agent Harness:模型边界、工具边界、状态边界、上下文边界、扩展边界和产品外壳边界。