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.startedturn.queuedturn.starteditem.starteditem.updateditem.completedturn.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 语义:
- 把
snapshot载入 materialized thread state。 - 附加持久化保留的
journal尾部,供恢复和 history 检查。 - 只应用
seq > snapshot.last-event-seq的事件。
因此 snapshot 是快速恢复点,持久化 journal 是有界恢复尾部,两者不可互换。checkpoint 先写 snapshot 、后重写日志,所以中途崩溃只会留下一些会被 last-event-seq 跳过的事件。
Loop Flow
- UI submission 进入
magent-runtime-api.el。 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中。- submission 真正开始时,
magent-runtime-api.el把 turn 转成in-progress。 magent-agent-run-turn幂等复用该 turn/user item,不重复写用户消息。magent-agent-loop消费 normalized LLM events。- Text 和 reasoning deltas 更新 snapshot 中 materialized in-progress items,但不会为每个 chunk 追加 journal event;terminal item event 携带最终内容。
- Tool-call events 累积到 provider-neutral
tool-call-batch-end。 - Tool dispatch 启动
toolitems,记录 approval metadata,并把同一 item 更新为completed或failed。 - Tool output 返回
tool-output等 continuation outcome;magent-agent-run-turn决定是否从 session ledger 重建 prompt 并继续 sampling。 - Provider 明确完成但 assistant text 为空时,Magent 记录
empty-completionmetadata 并将本轮标记为 failed,不再请求模型。 - Assistant completion 记录 assistant message item,并完成 turn。
- Abort、failure 和 dropped queued submissions 分别把 turn 转成
interrupted、failed或dropped;aborted turn 下的 in-progress items 标记为cancelled。 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。