Agent 工作流状态机

Magent 现在把 agent loop 建模为显式的 thread -> turn -> item ledger。这个 ledger 是 agent workflow state 的唯一事实来源。Provider prompt、transcript、compaction 和 audit 都是按需生成的命名 projection,不会作为第二份持久化状态。

Codex 对齐

Codex 在 SDK/app-server 边界暴露 thread event stream:

  • thread.started
  • turn.queued
  • turn.started
  • item.started
  • item.updated
  • item.completed
  • turn.completed 或 turn.failed

Magent 保留同类工作流边界,但适配 Emacs:

  • Provider transport 仍然走 gptel-request 。
  • Magent loop 自己负责 tool dispatch、continuation outcome、abort 和 Emacs UI rendering;tool output 触发下一次 sampling,正常最终答复结束 turn;缺少最终正文时普通 turn 失败,符合条件的断流先进行有限重试。
  • 不引入 Codex sandbox、seatbelt、bubblewrap 或 shell isolation parity。

Codex 中最接近的参考点是 SDK event names、core sampling loop、app-server event conversion 和 thread status shape。Magent 借鉴的是事件边界和 turn/item 生命周期,不复制 Codex 的隔离和 app-server runtime。

状态对象

magent-ledger.el 定义 canonical ledger:

  • magent-thread
    • 状态: not-loaded 、 idle 、 active 、 system-error 、 closed
  • magent-thread-turn
    • 状态: queued 、 in-progress 、 completed 、 interrupted 、 failed 、 dropped
  • magent-thread-item
    • 状态: pending 、 in-progress 、 completed 、 failed 、 cancelled

每个用户 prompt 创建一个 turn。Assistant message、reasoning block、tool invocation 和 tool output 都是这个 turn 下的 item。Reasoning item 永远不会提升成 assistant message text,即使 provider 没有返回可见 content。

对象 运行态 终态
thread not-loaded, idle, active, system-error, closed closed
turn queued, in-progress completed, interrupted, failed, dropped
item pending, in-progress completed, failed, cancelled

Turn lifecycle:

queued -> in-progress -> completed
                      -> interrupted
                      -> failed
                      -> dropped

Item lifecycle:

pending -> in-progress -> completed
                       -> failed
                       -> cancelled

Tool Items

Tool call 和 tool result 是同一个 item lifecycle,不是两条持久记录。

模型请求工具时,item 开始:

(:type tool :status in-progress :name "grep" :input (:pattern "..."))

工具结果可用后,同一个 item 完成或失败:

(:type tool :status completed :output "...")

Tool item 在 provider prompt projection 中会转换成 gptel 所需的历史 (tool . PLIST) 形状。

如果模型请求工具而结果稍后到达,Magent 按 call-id 更新同一个 item。所有 tool result 都必须归属于显式的当前 turn。

持久化

持久化格式是 log + snapshot ,按变更频率拆成三个文件。

  • <id>.jsonl :磁盘上的 append-only event log,一行一个 JSON 对象,记录 turn-queued 、 turn-started 、 item-started 、 item-content-appended 、 item-completed 、 turn-completed 等生命周期事件。流式内容直接 append 到这里,因此一次保存的代价只和本次变化量成正比,而不是和整个会话成正比。压缩时保留由 magent-session-log-max-events 控制的有界尾部;tool/permission 审计由 magent-audit.el 单独负责。
  • <id>.snapshot :materialized full thread state。它保存当前 thread、turns 和 items,避免每次 resume 都从头 replay;只有在文件缺失或日志超过 magent-session-log-max-events 时才重写。
  • <id>.json :session header(id、scope、title、status、child-agent jobs、approval overrides)。文件很小,是 session listing 唯一读取的文件。

当前 session schema(version 7)同时写入三者:

// <id>.json
{ "id": "...", "schema-version": 7, "scope": "global", "agent-jobs": [] }

// <id>.jsonl   (一行一个事件)
{ "seq": 1, "type": "turn-queued", "payload": { "turn": { "...": "..." } } }

// <id>.snapshot
{ "id": "...", "turns": [ { "items": [ { "...": "..." } ] } ] }

加载器只接受完整、字段精确的当前 schema。缺少 ledger、版本不同、字段未知或嵌套 shape 无效的文件都会被拒绝。schema 6 文件(把 journal 和 snapshot 内联在同一个文件里)会在首次读取时原地迁移。

Replay 语义:

  1. 把 snapshot 载入 materialized thread state。
  2. 附加持久化保留的 journal 尾部,供恢复和 history 检查。
  3. 只应用 seq > snapshot.last-event-seq 的事件。

因此 snapshot 是快速恢复点,持久化 journal 是有界恢复尾部,两者不可互换。checkpoint 先写 snapshot 、后重写日志,所以中途崩溃只会留下一些会被 last-event-seq 跳过的事件。

Loop Flow

  1. UI submission 进入 magent-runtime-api.el 。
  2. magent-runtime-api.el 创建 queued ledger turn、立即记录 completed user message item,并把 prompt、agent、tools、skills、route、effort、thinking mode、observer、approval 与 turn identity 一次冻结到唯一的 magent-request-context 中。
  3. submission 真正开始时, magent-runtime-api.el 把 turn 转成 in-progress 。
  4. magent-agent-run-turn 幂等复用该 turn/user item,不重复写用户消息。
  5. magent-agent-loop 消费 normalized LLM events。
  6. Text 和 reasoning deltas 更新 snapshot 中 materialized in-progress items,但不会为每个 chunk 追加 journal event;terminal item event 携带最终内容。
  7. Tool-call events 累积到 provider-neutral tool-call-batch-end 。
  8. Tool dispatch 启动 tool items,记录 approval metadata,并把同一 item 更新为 completed 或 failed 。
  9. Tool output 返回 tool-output 等 continuation outcome; magent-agent-run-turn 决定是否从 session ledger 重建 prompt 并继续 sampling。
  10. Provider 明确完成但 assistant text 为空时,Magent 记录 empty-completion metadata 并将本轮标记为 failed,不再请求模型。
  11. Assistant completion 记录 assistant message item,并完成 turn。
  12. Abort、failure 和 dropped queued submissions 分别把 turn 转成 interrupted 、 failed 或 dropped ;aborted turn 下的 in-progress items 标记为 cancelled 。
  13. magent-max-sampling-requests 是可选 safety guard,默认关闭;启用后,到达上限会直接使本轮失败。

Prompt reconstruction 是 ledger-driven。知道 current turn id 时,Magent 会包含 completed、interrupted、failed turn 的用户目标、按顺序完成的消息与工具结果,以及当前 turn,使后续请求能恢复已取消请求的上下文,同时避免后续 queued user submissions 泄漏进 active model request。

UI 投影

受支持的 frontend 是 agent-shell。 magent-agent-shell.el 创建由 magent-acp.el 实现的 in-process ACP client。ACP session/prompt request 把 registered slash command 分派给 magent-action.el ,普通 prompt 则直接提交 magent-runtime-api.el ;request 会保持 pending,直到对应 command invocation 或普通 runtime turn completed、failed 或 cancelled。Command-owned turn 和 workflow step 使用同一个 runtime API。Runtime 发出 Magent-native observer events; magent-acp.el 转换为 ACP session/update 消息。

magent-runtime-queue.el 负责 queued/active turn state。当前实现一次只有一个全局 active turn。每个 submission 捕获精确 runtime session wrapper 和唯一 request context,因此 cancellation 按对象 identity 隔离:取消一个 ACP session 会移除该 session 的 queued work 并 abort 该 session 的 active turn,不会丢掉其他 session 的 queued turns。

agent-shell + ACP 路径是完整的会话式 UI projection 和 slash-command surface。显式 M-x wrapper 进入同一个 magent-action invocation lifecycle,不会形成另一套会话 frontend;共享 runtime 行为保持 frontend-neutral。

保留的 Codex 差异

Magent 刻意保留以下差异:

  • UI 是 Emacs-native;受支持的交互是 agent-shell + in-process ACP。
  • Provider streaming 经 magent-sampling-gptel.el 规范化,但 transport 仍是 gptel。
  • Codex core 有 app-server mailbox、steering queue 和更丰富的 intra-turn input 语义;Magent 把 request serialization 放在 runtime queue,用 Codex-style tool continuation 处理工具结果:工具执行记录 model-visible output,然后 turn layer 继续 sampling,直到模型返回 assistant completion。Magent 仍不实现 Codex 的 app-server mailbox、steering queue、stop hooks 或 mid-turn auto-compaction。
  • Magent 用自己的 session JSON snapshot + journal ,不直接使用 Codex rollout files。
  • Tool call 和 output 在 Magent 中是一个 source-of-truth item 的状态更新。
  • Tool execution 当前由 Magent tool queue 串行化;Codex 更丰富的 per-tool runtime、approval、MCP 和 process execution 机制不在当前范围。
  • Child agents 是 AGENT_JOBS.org 中描述的 durable Magent jobs,不是 Codex app-server threads。

Backlog / TODO

Agent-workflow/UI refactor backlog 已无开放 TODO。未来 hardening 候选:

  • 为更多结构化输出增加 per-tool renderer plugins。
  • 增加超大 transcript 和长 streaming session 的 live visual smoke coverage。

设计评审摘要

当前主 agent-loop 相比 Codex 的主要差距已经不再是缺少显式 thread/turn/item lifecycle;Magent 已经拥有并持久化该 lifecycle。

剩余差异是有意的产品/运行时边界:

  • 不提供 Codex app-server lifecycle manager、subscriber model、unloaded thread cache 或 active flags。
  • 不复制 Codex 的 intra-turn input queue/mailbox semantics。
  • 不直接使用 Codex rollout files。
  • 不把 tool call 和 tool output 分成两条 source-of-truth records。

有序消息、原生续接与计划

工具执行前的 assistant 进度消息和最终答复分别保存为独立 item。Provider 提供 phase 时原样保留;没有 phase 时,工具前文字标为 commentary,最后一次 sample 的正文作为答复。系统提示要求用无标题、无项目符号的短段落说明目标、证据、 决策变化和验证结果,隐藏的 reasoning 仍独立处理,不额外调用“反思”或 “评判”模型。OpenAI-compatible Chat 续接会在 provider 原生 assistant tool-call 消息中保留工具调用前的可见正文。

Responses 原始 output item 以不透明 JSON 存入 provider item,并记录 backend 、endpoint identity 和 model。适配层观察 gptel 已解码的事件,HTTP/SSE 仍由 gptel 处理。原始消息 阶段、加密 reasoning 与 function call 用于当前续接和同一路由的历史恢复, 避免重复消息或调用。切换 provider/model 时使用可见消息与工具结果;compaction 排除不透明续接数据。中断后没有结果的调用会在 replay 中得到明确的未知结果说明, 不会伪称已执行。

Schema v6 已支持这些 item、phase、metadata 和 output,不需要迁移文件。 旧的合并消息原样保留,缺失的边界不作推测。普通 turn 缺少最终答复会失败; 明确允许空结果的直接 Action sampler 保持其契约。completed 只表示执行正常结束, 不代表工程目标、测试或所有工具都成功。

Chat Completions 流缺少停止原因、且本次尚未输出正文时,错误事件可以提供 重试 continuation,覆盖提前正常关闭和 HTTP 200 下的 curl 18 部分传输错误。 适配层在 gptel 清理阶段推进状态机之前标记传输失败,阻止未完成的工具调用执行。 turn 层由 magent-stream-retry-limit 控制,默认最多 2 次, 并计入 magent-max-sampling-requests。适配层延迟恢复同一份 provider 输入, 清除未完成的响应状态;已执行的工具不会重跑。取消 turn 会销毁请求 buffer 并取消重试 timer。notice item 与 sampling-retry 事件显示重试进度,但不进入 模型历史。provider 错误、token 上限、已输出正文的断流不重试;直接 Action sampler 保持其原有错误处理行为。

update_plan 使用本地 plan 权限组,将完整计划保存为现有 ledger 的 plan item, 通过 ACP 原生 plan 视图实时显示并恢复。每一步包含 step 和 status,状态为 pending、in_progress 或 completed,最多一步 in_progress。它仅记录进度,不运行 步骤、不自动重规划,也不因步骤全部完成而结束 turn。