基于文件的持久化目标执行:审计日志与动态转向模式
基于文件的持久化目标执行:审计日志与动态转向模式
学习目标
读完本章后,你将能够:
- 设计基于文件的多目标持久化执行系统
- 实现 JSONL 格式的追加式审计日志
- 构建动态转向(Steering)系统支持运行时计划修改
- 理解聚合目标与逐目标两种执行模式
- 设计最终质量门控的三方独立验证
前置知识
核心概念
1. 为什么需要基于文件的持久化目标执行
在多目标 AI Agent 执行场景中,传统方案面临以下问题:
| 问题 | 后果 | 基于文件方案的优势 |
|---|---|---|
| 会话重启丢失进度 | 需要从头开始 | JSON 文件跨会话持久化 |
| 无法审计每个决策 | 出问题后难以追溯 | JSONL 追加式审计日志 |
| 计划无法动态调整 | 需要停止重来 | 转向系统支持运行时修改 |
| 多个目标无法独立追踪 | 进度不透明 | 每个目标有独立状态 |
文件与数据库方案的对比:
| 维度 | 数据库(原子 checkout) | 文件(JSON + JSONL) |
|---|---|---|
| 部署复杂度 | 需要 DB 服务器 | 零依赖,本地文件 |
| 并发控制 | 行级锁,数据库事务 | 文件锁(wx 独占打开) |
| 审计能力 | 需要额外的审计表 | JSONL 天然追加式审计 |
| 可读性 | 需要 SQL 查询 | 直接 cat 查看 |
| 适用场景 | 多 Agent 并发操作同一系统 | 单 Agent 顺序执行多目标 |
2. 三文件架构
基于文件的目标执行系统使用三个核心文件:
.omx/ultragoal/├── brief.md # 原始简报(不可变参考)├── goals.json # 完整计划:目标列表 + 聚合目标└── ledger.jsonl # 追加式审计日志goals.json 结构:
伪代码:{ "version": 1, "createdAt": "2026-06-05T...", "updatedAt": "2026-06-05T...", "codexGoalMode": "aggregate", // aggregate | per_story "codexObjective": "Complete the...", // 聚合目标字符串 "goals": [ { "id": "G001", "title": "build the CLI parser", "objective": "Implement...", "status": "pending", // pending | in_progress | complete | failed | review_blocked | needs_user_decision "attempt": 0, "steeringStatus": null, // superseded | blocked | null "supersededBy": [], "supersedes": [], "blockedReason": null, "nonRetriable": false } ], "aggregateCompletion": null // 任务作用域的聚合完成记录}ledger.jsonl 结构:
每行一个 UltragoalLedgerEntry,记录每次状态变更:
伪代码:{"ts": "...", "event": "plan_created", "goalId": null, "message": "Plan created with 5 goals"}{"ts": "...", "event": "goal_started", "goalId": "G001", "message": "Starting G001"}{"ts": "...", "event": "goal_completed", "goalId": "G001", "message": "G001 completed"}{"ts": "...", "event": "goal_failed", "goalId": "G002", "message": "G002 failed: timeout", "failureReason": "..."}3. 目标解析器
从用户输入的 markdown 简报中解析出离散目标:
伪代码:function deriveGoalCandidates(briefMarkdown): // 从顶层列表项提取目标,忽略非目标章节 candidates = [] for line in parseMarkdownList(briefMarkdown): if not isNonGoalSection(line.parent): candidates.push({ id: generateSlugId(), // "G001", "G002", ... title: extractTitle(line), objective: line.text, status: "pending", attempt: 0 }) return candidates解析规则:
| 规则 | 说明 |
|---|---|
| 忽略章节标题 | ”Acceptance criteria”、“Verification checklist” 等非目标内容不解析为目标 |
| 忽略子列表 | 只提取顶级列表项,子列表项视为目标的细节 |
| 自动生成 ID | 使用 G001、G002 等 slug ID |
| 初始状态 | 所有目标初始状态为 pending |
4. 聚合目标 vs 逐目标模式
| 维度 | 聚合模式(aggregate) | 逐目标模式(per_story) |
|---|---|---|
| Codex 目标数 | 1 个 | N 个(每个目标 1 个) |
| 聚合目标字符串 | 稳定的全局指针 | 无 |
| 本地微目标 | ledger 中的 microgoal | Codex 原生目标 |
| 适用场景 | 大型项目,需要全局上下文 | 简单任务,目标间独立 |
| 完成判定 | 聚合目标状态为 complete | 所有 Codex 目标为 complete |
聚合模式的优势:
- Codex 只维护一个活跃目标,减少了目标管理的复杂度
- 本地 ledger 作为微目标追踪,灵活度高(可随时添加/拆分/重排序)
- 最终完成时可通过任务作用域的聚合调和(Task-Scoped Aggregate Reconciliation)判定完成
5. 动态转向系统(Steering)
转向系统允许在运行时修改计划,而不需要停止重启。
六种允许的变异类型:
| 变异类型 | 效果 | 示例 |
|---|---|---|
add_subgoal | 追加新目标 | 发现遗漏的测试场景 |
split_subgoal | 将目标拆分为子目标 | ”构建 API” → “设计 schema” + “实现 CRUD” + “编写测试” |
reorder_pending | 重排 pending 目标顺序 | 优先处理高风险目标 |
revise_pending_wording | 修改 pending 目标的标题/目标 | 根据反馈调整描述 |
annotate_ledger | 向审计日志追加注解 | 记录重要决策点 |
mark_blocked_superseded | 标记目标为阻塞或替代 | 原方案不可行,用新方案替代 |
转向不变量(禁止的修改):
| 禁止项 | 原因 |
|---|---|
| 修改聚合目标字符串 | 聚合目标是稳定的全局指针 |
| 修改简报约束 | 简报是用户原始需求的不可变参考 |
| 修改质量门控 | 质量标准不得在运行时降低 |
| 硬删除目标 | 审计日志需要完整记录所有历史目标 |
| 自动完成未完成的工作 | 完成必须由实际执行产出证明 |
| 削弱验证要求 | 验证是安全网,不得削弱 |
转向验证流程:
伪代码:function validateSteeringProposal(proposal): // 检查不变量 for key in PROTECTED_KEYS: // ["aggregateCompletion", "brief", "codexObjective", "qualityGate", "status"] if proposal.after[key] != undefined: return { valid: false, reason: "Cannot modify protected key: " + key }
// 检查证据 if not proposal.evidence or proposal.evidence.length == 0: return { valid: false, reason: "Steering requires evidence" }
// 检查理由 if not proposal.rationale or proposal.rationale.length == 0: return { valid: false, reason: "Steering requires rationale" }
// 幂等性检查 if existsIdempotencyKey(proposal.idempotencyKey): return { valid: false, reason: "Duplicate steering proposal" }
return { valid: true }6. 完成判定
目标计划完成的两种路径:
路径一:聚合调和
伪代码:if aggregateCompletion != null && aggregateCompletion.status == "complete": return true // 任务作用域的聚合调和已完成当外部完成的 Codex 目标与 ultragoal 简报内容重叠(通过内容重叠和 .omx/ultragoal 工件证据验证),可以跳过逐个目标的完成检查。
路径二:微目标全部完成
伪代码:function isUltragoalDone(plan): // 查找最终运行的完成候选目标 finalCandidate = findFinalRunCompletionCandidate(plan.goals)
if finalCandidate == null: return false
// 检查该目标是否完成 return finalCandidate.status == "complete"完成阻塞判定:目标在以下情况下阻塞完成:
| 条件 | 说明 |
|---|---|
steeringStatus == 'superseded' | 且不是所有替代目标都已解决 |
steeringStatus == 'blocked' | 被明确标记为阻塞 |
status != 'complete' 且 status != 'review_blocked' | 未完成且未被审查阻塞 |
7. 最终质量门控
最终故事完成时需要通过三方独立验证:
质量门控结构:
伪代码:{ "aiSlopCleaner": { "status": "passed" }, "verification": { "status": "passed", "commands": ["npm run build", "npm test", "npm run lint"] }, "codeReview": { "recommendation": "APPROVE", "architectStatus": "CLEAR", "independentReview": [ { "agent": "code-reviewer", "evidence": "..." }, { "agent": "architect", "evidence": "..." } ] }}为什么需要三方独立验证:
- AI Slop Cleaner 清除 AI 生成的冗余/重复代码
- 验证测试确保功能正确性和代码质量
- 代码审查需要两个独立 Agent(代码审查员 + 架构师)的证据,防止单一审查者的盲点
8. 审查阻塞流
当最终故事的代码审查非清洁时:
recordFinalReviewBlockers将当前目标标记为review_blocked- 自动追加一个新的 pending blocker 解决故事
- Codex 目标保持活跃
- 调度器在下一轮启动 blocker 解决故事
- blocker 解决后,原故事可以完成
9. 突变锁与并发安全
文件级互斥锁防止并发修改:
伪代码:async function withMutationLock(operation): lockFile = open(".omx/ultragoal/.mutation.lock", "wx") // 独占打开 try: return await operation() finally: lockFile.close() unlink(".omx/ultragoal/.mutation.lock")锁策略:
- 使用
wx模式(独占创建),如果文件已存在则失败 - 重试退避(最多 100 次尝试)
- 自动清理死亡 PID 的锁文件
陷阱与对策
| 陷阱 | 后果 | 对策 |
|---|---|---|
| JSONL 文件过大 | 读取审计日志时内存溢出 | 流式读取 + 分页,或定期压缩归档 |
| 转向无限拆分目标 | 目标碎片化,管理开销增大 | 设置最大拆分层级(如 3 层) |
| 文件锁持有者崩溃 | 后续操作永久阻塞 | PID 活跃度检查,超时自动释放 |
| 聚合目标字符串漂移 | 最终完成判定失效 | 转向不变量禁止修改聚合目标 |
| Ledger 与 goals.json 不一致 | 状态不同步 | 每次 mutations 同时更新两个文件(原子写入) |
| 审查阻塞无限循环 | blocker 解决后又阻塞 | 设置最大 blocker 解决次数上限 |
参考来源
- OpenAI Codex CLI — Goal tool specification
- JSONL (JSON Lines) 规范 — https://jsonlines.org/
- File locking patterns —
open(2)O_EXCLflag