feat: harden agent verification and runtime contracts

Introduce two-gate checker settlement, explicit graph visibility and checkpoints, structured LLM reliability, centralized action specifications, and corresponding tests and design documentation.
This commit is contained in:
somew1nd committed 2026-08-31 20:42:24 -07:00
1 parent ddfa420945
commit cbe8f4f0c6
106 files changed
+11542 -4445

No files matched your search

+370
View File
@@ -0,0 +1,370 @@
# Checker 全流程验证重构(计划驱动检查点 + 乐观异步 + 出口结算)
> 交接文档:本计划自包含,执行者不需要任何前置对话上下文。
> 日期:2026-08-30(v2.1 实施版)。文中行号以当日工作区为准,**执行前一律先按内容搜索确认,行号可能漂移**。
---
## 0. 工作区纪律(先读)
- 本工作区可能有**多个并发 Claude 会话**在改代码,工作树里有大量未提交改动(部分不是你产生的)。
- **严禁 `git stash`**(会卷走别人的半成品)。
- 需要对比基线用 `git show HEAD:<path>` 或临时 worktree,不要动工作区里已有的未提交修改。
- 不要 commit/push,除非用户明确要求。
- 每个 Work Package 完成后跑对应的单测(见 §8),基础设施类测试可用 `ARTEMIS_FAKE_LLM=1` 环境变量走假 LLM。
---
## 1. 背景与结论(已定,不要重新设计)
### 1.1 旧中途 Checker 的病根(仍然成立,全部要摘除)
1. **Checker 失败的 turn 不写入 DataEngine 历史**——`graph.py` `execution_check_node` 里 `if checker_success:` 才 `record_step`。
2. **notes 目录全量快照回滚**——Operator 每轮前全量快照,checker 失败整体恢复。
3. **Prompt 整体切换**——存在 checker feedback 时换 `troubleshooter_template` + verification chat 多轮辩论。
4. **僵尸 checker**——`ctx.checker_task` 单槽位覆盖不 cancel,副作用在任务体内。
**关键认知:病根是"回滚过去、切换模板、副作用失控",不是异步验证本身。**
### 1.2 目标架构与能力边界
系统承担**测试任务**,短暂状态只能在发生时刻附近取证,所以中途检查点必须存在,以**乐观异步**方式运行。架构分四层:
- **配置层**:`中途检查` 与 `结尾终审` 两个独立开关(§6)。用户关闭的检查一律尊重;**未执行的断言报告为 unchecked,绝不计为通过**。
- **计划层**:Planner 把检查项声明进 task_plan.md(`- verify:` / `- assert:`,带时机,§4)。计划文件是唯一权威载体(锚定子目标、随重写存活、受守护覆盖);但**相关 prompt 注入全部条件化**(§4.4)——检查全关时 Planner 不生成 check 行、语法说明不进任何 prompt,零上下文污染。
- **能力层**:Checker 享有与 Operator 等价的观察能力(历史、截图、当前屏幕),但工具权限独立——只读检索 + 受约束探测,零副作用,只出裁决(§5.4)。
- **控制层**:Graph 识别检查点、spawn 任务、收割裁决、执行副作用、出口结算(§5.1-5.3)。
**能力边界(必须如实声明,写进 planner prompt 与最终报告语义)**:
- 本期的中途检查是**事后核验**:它能在事后发现流程违规并留下证据,**不能阻止**违规发生。"必须确认 A 之后才允许执行 B"这类**强制前置关卡本期不支持**——Planner 遇到此类用户要求时不得静默接受:断言照常生成(保留用户原语义),但计划与报告中标注"顺序核验为事后判定"。
- 异步检查只能核验**已被采集的证据**。从未被记录的瞬时提示无法找回——瞬态断言的证据来源是执行历史中的前后截图;Planner 应优先把预期表述为可持久探测的状态。
**三条铁律**:
1. Operator 的历史与上下文 **append-only**——不换 prompt、不回滚 notes、不丢 step、不开平行对话。
2. 验证影响未来(打回状态、注入 finding),**不修改过去**。
3. **放行、裁决、任务收尾三者分离**:fail-open 只影响放行决策,裁决原值(inconclusive)永远入账;assert 失败不修复但必须产生机器可读的测试失败结果并传导到 SDK 收尾(§5.5)。
**裁决 append-only**:每次检查有独立 `attempt_id`,所有裁决(含被 supersede 的)永久入账;断言首次失败不被覆盖;报告呈现每条断言 passed / failed / inconclusive / unchecked 与完整裁决序列。
### 1.3 横切原则:Prompt 与工具按配置装配(No Dead Instructions)
本次全部改动遵守同一条装配纪律,**任何 agent 的 prompt 段与工具注册都是"配置 × 当前场景"的函数**:
1. 配置关闭的能力,不得在任何 prompt 中被提及,其工具不得注册——不存在"指令还在、机制已关"的死指令;
2. 反向同样成立:prompt 中出现的每条规则,必须对应一个当前激活的机制(如"删除会被恢复"只在守护生效时可说);
3. 装配点集中在代码里的组件列表/工具注册表(PromptComponent 列表、工具 dict 构造函数),**不在 prompt 模板内部堆条件文本**;
4. 场景也参与装配:同一 agent 的不同入口(如 checker 的中途/终审)按各自的证据纪律装配不同的工具与指南段。
落点:Planner(§4.1/§4.2 条件语法段与生成指令)、Operator(§4.4 说明段、§3.1 feedback 组件按内容渲染)、Checker(§5.4 两入口装配表)、SDK 返回结构(§5.5 test summary 仅在存在 check 项时出现)。
---
## 2. 代码地图(改动涉及的锚点)
Graph 拓扑(`artemis/graph/graph.py` `get_graph`,~622):
```
START → planner → convergence
perception → operator → execution_check ─(execute_decisions)→ validator → summarizer → convergence
└(review_subgoals)→ convergence
convergence ─(continue)→ perception
└(end)→ END
```
注意时序:Operator 在自己的 turn 内通过 note 工具写计划(含完成标记);该 turn 的设备动作在**其后的 validator 节点**才执行;`execution_check_node` 的 `record_step` 记录的是本 turn 的观察(pre 截图)与决策。因此"子目标完成"的证据 = 截至本 turn step 的历史(上一 turn 的 post 截图 + 本 turn 的 pre 截图)。
| 锚点 | 位置(按内容搜索确认) | 处置 |
|---|---|---|
| `execution_check_node` | `graph.py` ~88-289 | 重构(§3.2、§5.1、§5.2) |
| checker 中途触发 ①(chat_path 分支) | ~100-132 | 删除 |
| checker 单槽消费段 | ~134-153 | 删除 |
| planner validation 消费段 + 拒绝提前 return | ~155-211,**return 在 ~190** | 保留机制;拒绝分支补 record_step(§3.2) |
| record_step(`if checker_success:` 门控) | ~213-250 | 无条件化(§3.2) |
| notes 快照 commit/rollback 段 | ~252-280 | 删除 |
| checker 中途触发 ②(`new_completions`) | `_process_plan_write` 末段 ~499-506 | 改为排队 pending checkpoint(§5.1) |
| planner validation 漂移检测 | `_process_plan_write` `milestones_changed`(~452) | 保留;旁加确定性 check 行守护(§4.3) |
| `convergence_gate` | ~625 | 扩为三值路由(§5.3) |
| notes 快照创建 / troubleshooter 分支 / verification chat 机制 | `operator.py` | 删除(§3) |
| `CheckerFeedbackPromptComponent` | `operator/prompts.py` ~231 | 改写为读 state 的 append 注入组件 |
| checker 本体 | `agents/checker/checker.py` `run_async_check`(~80)、`CheckerResult`(~68) | 重写(§5.4) |
| context 字段 | `context.py`:`checker_task`、`disable_checker=True`(~108)、`checker_max_chat_rounds`(~110)、`disable_planner_validation=True`(~111,**注意默认关**)、`task_plan_snapshot` | §3、§6 |
| state 字段 | `graph/state.py`:`checker_success`、`initial_goal` | 保留;新增字段见 §5.2/§5.5 |
| plan 语法 | `utils/plan_grammar.py`:`CHECKBOX_LINE_RE`(~49)、`parse_plan`(~149)、`PlanSnapshot`(~100)、`subgoal_hash`(~56)、`milestones_changed`(~167)、`new_top_level_completions`(~172)、`PLAN_GRAMMAR_SPEC`(~199) | 扩展(§4.1) |
| DataEngine | `engine.py`:`record_step`(~491,存 pre/post 截图)、`get_step_number`(~922)、`get_agent_friendly_steps`(~1045)、`end_session(status)`(~300);`storage.py` `get_steps`(~651) | 只加只读检索辅助 + 收尾状态核对(§5.5) |
| SDK 收尾 | `sdk/agent.py`:图完成后无条件 success 日志 + `end_session("completed")`(~718-721)、task.status 收敛(~1035-1040)、trace 命名 `_PASS/_FAIL`(~1061) | 接入 run outcome(§5.5) |
| verification 工具函数 | `utils/verification.py` | 删除 |
| 快照工具函数 | `utils/file.py` `create_snapshot`/`restore_snapshot` | 删前 grep 确认无其他调用方 |
| SDK 配置透传 | `sdk/types/agent.py`、`sdk/builders/agent_config_builder.py`、`sdk/agent.py` | §6 |
---
## 3. WP1 — 摘除旧干扰源(先做)
### 3.1 Prompt 不再切换
- `operator.py` `_build_prompt`:删 `has_feedback` 分支,恒用 `main_template`。
- `CheckerFeedbackPromptComponent` 改写为 `CheckFeedbackPromptComponent`:读 state 的 `check_feedback`(§5.2 写入),无内容不渲染,加入恒定组件列表。
- 删 `_has_checker_feedback`、`_get_verification_chat_rounds`、`_get_reply_to_checker_tool` 及工具注册逻辑;`operator.json` `troubleshooter_template` 相关键清理(grep 确认无他处引用)。
### 3.2 每个 operator turn 都必须 record_step(含 planner 拒绝的 turn)
- 仅移除 `if checker_success:` 门控**不够**:planner 拒绝分支在 ~190 提前 `return`,同样跳过记录。
- 做法:把现有 record_step 代码抽成节点内局部函数 `_record_turn(extra_metadata: dict) -> str | None`:
- 正常路径原位调用(extra 不变);
- planner 拒绝分支在 `return` 前调用,extra 加 `{"planner_rejected": True}`(该 turn 的 terminal actions 被拦截未执行,记录如实反映决策与拦截)。
- 保留"planner 拒绝的 turn 不清零 subagent 循环计数"语义(`checker_success` 今后仅由 planner 拒绝路径置 False,维持现有 `if checker_success: update["subagent_calls"] = []` 写法)。
### 3.3 删除 notes 快照/回滚
- `operator.py` `create_snapshot` 段、`ctx.task_plan_snapshot` 赋值:删。
- `execution_check_node` "Handle optimistic execution rollback/commit" 整段(~252-280):删。
- `context.py` `task_plan_snapshot`:删。`utils/file.py` 快照函数 grep 无调用方后删。
- planner validation 拒绝时的 task_plan.md 单文件回滚(`ctx.task_plan_content_before`)**保留**。
### 3.4 删除单槽位与 chat 机制
- `execution_check_node`:删 chat_path 触发段与 `await ctx.checker_task` 消费段。
- `context.py`:删 `checker_task`、`checker_max_chat_rounds`。`planner_task` 保留。
- `_process_plan_write` `new_completions` 触发段:暂时整段删除(§5.1 重建)。
- `utils/verification.py`、`checker_status.json` 写入、相关 import:删。
WP1 完成后系统处于"无任何语义验证"的干净中间态,可独立运行与测试。
---
## 4. WP2 — 计划语法、Planner 与检查标准保护
### 4.1 plan grammar 扩展(`utils/plan_grammar.py`)
- 新语法:顶层子目标下缩进附属行,关键字带可选时机后缀:
```
- [ ] 在时钟应用创建 7:30 AM 闹钟
- verify: 闹钟列表应出现 7:30 AM 且开关为开
- assert: 创建后应弹出"已设定闹钟"提示
- [ ] …
(任务级,可置于计划末尾游离段)
- assert@end: 全程不应出现应用崩溃对话框
```
- 解析为:
```python
@dataclass(frozen=True)
class CheckItem:
kind: Literal["verify", "assert"]
when: Literal["on_complete", "at_end"] # 无后缀=on_complete;@end=at_end
text: str
parent_key: str | None # 挂靠子目标的 key;任务级为 None
```
`when` 语义:`on_complete` 在锚定子目标完成时刻用**当时证据**判定;`at_end` 只在出口用**最终状态**判定。**禁止**在出口用当前状态重判 `on_complete` 项,也禁止在中途提前执行 `at_end` 项。
- 新增 `CHECK_LINE_RE`;先确认 `parse_plan` 对非 checkbox 缩进行的容忍度(`CHECKBOX_LINE_RE` 只匹配 checkbox 行),确保 check 行**不影响** `milestones_changed`/`subgoal_hash`/`new_top_level_completions` 现有行为。
- `PlanSnapshot` 新增 `check_items_of(item)`、`task_level_check_items`、`all_check_items`;新增模块函数 `check_items_changed(before, after)`(比较 check 行多重集合)。
- **语法说明条件化**:`PLAN_GRAMMAR_SPEC` 从模块常量拆为"基础语法 + check 扩展"两段,新增 `render_plan_grammar_spec(include_checks: bool) -> str`。check 扩展段内容:两关键字语义(verify=完成度,失败触发修复;assert=测试断言,失败如实记录不修复)、`@end` 后缀、§1.2 能力边界声明(事后核验、不支持强制前置关卡、瞬态断言的证据约束)。三个现有注入点改用该函数:`planner.py` ~105/~178、`operator/prompts.py` ~206;`include_checks = not(两开关全关)`。全关时任何 prompt 里都不出现 check 语法。
### 4.2 Planner 生成检查项(`planner.json`)
- **生成指令是 planner prompt 的条件段**:检查项相关的全部指令(下述各条 + §4.1 的 check 语法扩展)只在 `include_checks` 为真时注入。两开关全关 → Planner 的 prompt 与产出和无此功能时完全一致,不生成任何 check 行。
- 用户目标里的显式预期 → `- assert:` 行,逐字保留用户语义;属于全程/最终状态的预期用 `@end`。
- `- verify:` 行**只为确需验收的关键节点**生成——判断标准:不可逆操作完成点、对最终目标有直接贡献的里程碑。**不是**每个子目标一条;克制是明确要求(成本与 check 项数量成正比,见 §6)。
- 不可验证的子目标不硬造断言;瞬态预期优先改写为可持久探测的状态断言。
- 用户提出"必须先确认 A 才能做 B"类强序要求时:断言照常生成,但按 §1.2 标注"事后核验",不得静默承诺前置拦截。
### 4.3 检查标准保护(确定性守护,不依赖 LLM)
- **前提事实**:planner validation 默认关闭(`disable_planner_validation=True`),不能作为 check 行不可删的唯一保障。
- **确定性守护**(`_process_plan_write` 内,Operator 来源的写入):写入后若 `check_items_changed(before, after)` 检出 check 行被删除或改写,Graph **把缺失的 check 行合并回写**进新计划(锚定其 `parent_key`;父子目标整个被删时,该行转为任务级 `@end` 项)。合并回写是纯文本操作、同步完成,不打断 Operator;Operator 新增 check 行允许保留。Planner 节点自身的计划写入不受此守护(Planner 有权修订检查标准)。
- planner validation **开启时**:check 行差异同样纳入其审计范围(prompt 增加"检查标准被弱化应拒绝"要点)。机制本体(CAS 回滚、ratchet baseline)不动。
### 4.4 Operator 可选说明段(`operator/prompts.py`)
- 新增 `CheckItemsExplainerPromptComponent`,加入 `main_template` 恒定组件列表,**仅当当前 task_plan.md 解析出 check 行时渲染**(以计划实际内容为准,不看开关——续跑的旧计划带 check 行时同样需要解释),否则输出空串。
- 内容(一段,短):
- `- verify:` 是该子目标的验收标准,执行时可以参考它确认做到位,但**判定由独立 Checker 执行**,不要自行宣布通过或替 checker 记录结论;
- `- assert:` 是测试断言,**不需要为它做额外动作**,更不得为满足断言去构造/伪造状态——断言失败是合法的测试结果;
- check 行**不可删除或改写**(删除会被系统自动恢复);重写计划时原样保留,新增允许;
- 正常按语法打完成标记即可,检查在后台异步进行,不阻塞执行。
- 这与 §4.1 注入 operator 的语法扩展分工:语法扩展讲"这些行是合法语法",本组件讲"你该怎么对待它们";前者受开关控制,后者受计划内容控制。
### 5.1 触发:排队与 spawn 分离(证据锚点修正)
- **`_process_plan_write` 只排队**:`new_top_level_completions` 检出新完成项后,对每个带 `on_complete` check 项的完成项,追加 `ctx.pending_checkpoints: list[PendingCheckpoint]`(字段:`checkpoint_id=subgoal_hash`、check 项、完成时计划全文、触发时间戳)。**不在此处 spawn**——此刻本 turn 的 step 尚未记录,`get_step_number` 读到的是上一轮,锚点会错位。
- **`execution_check_node` 在 record_step 之后 spawn**:对每个 pending checkpoint 生成 `attempt_id`(`{checkpoint_id}#{递增序号}`),构造证据锚:
```python
@dataclass(frozen=True)
class EvidenceAnchor:
anchor_step_id: str # 刚记录的本 turn step(含 pre 截图;上一 turn 的 post 截图已在库)
trigger_ts: float
plan_text: str # 完成时刻的计划全文
```
然后 `ctx.checkpoint_tasks[checkpoint_id] = (attempt_id, asyncio.create_task(run_checkpoint_check(...)))`。
- **supersede 不丢裁决**:spawn 前若同 `checkpoint_id` 已有旧条目——旧任务 `done()` → **先收割入账**(§5.2)再替换;仍在运行 → `cancel()` 并 `await`(吞 `CancelledError`,完成资源清理),账本记一条 `superseded` 记录(attempt_id、时间),再 spawn 新 attempt。
- 每个新完成项都触发(修复旧"只验 `new_completions[-1]`"bug)。中途开关关闭时不排队不 spawn(相关项出口按 §5.3 处理)。`user_stop_requested` 后不再 spawn 任何新检查。
- **并发上限**:`max_concurrent_checkpoints`(默认 3),超出的 pending 留在队列,下轮 execution_check 再 spawn。
### 5.2 收割(`execution_check_node` 内,非阻塞)
- 每轮遍历 `ctx.checkpoint_tasks`,**只收割 `task.done()` 的**(绝不 await 未完成的),收割后从 dict 移除:
1. **入账**:所有裁决 append-only 写入 DataEngine 账本(session 元数据或专用 jsonl:attempt_id、checkpoint_id、item_text、kind、when、status、evidence、anchor_step_id、时间)。这是"首次失败永久保留"的唯一载体,任何后续结果只能追加。
2. **状态副作用的适用性校验**:仅当该 attempt 仍是此 checkpoint 的**当前 attempt**、且锚定子目标在当前计划中文本未变时,才允许改执行状态;过期/失配的裁决只入账不作用。
3. `verify` FAIL(适用时)→ 子目标 `[x]`→`[/]`(`subgoal_hash` 匹配,直接写文件不走 wrapped note tool)+ finding 追加进 state `check_feedback`;**每 checkpoint 修复配额 `checkpoint_max_repairs`(默认 2)**:超配额不再打回,裁决保持 failed,留待出口如实呈现。`user_stop_requested` 后不再打回。
4. `assert` FAIL → 只入账,不打回不注入(可配 `assert_failure_policy: "continue" | "halt"`,halt=置停止标记走 §5.3 收尾)。
5. cancelled 记 `superseded`;异常/超时:verify 记 `inconclusive` + warning(**裁决不改写为 passed**,仅放行决策按通过处理),assert 记 `inconclusive`。
- **单次检查超时**:`checkpoint_timeout`(默认 180s,spawn 时用 `asyncio.wait_for` 包裹)→ 超时按异常处理。
- 副作用只发生在收割点与出口结算,任务体内零副作用。
### 5.3 出口:结算与终审分离
- `convergence_gate` 扩为 `Literal["continue", "exit_settlement", "end"]`:
- `ctx.data_engine` 为 None → `"end"`;
- 任务到达终态(`all_top_level_done`,或 continuous+user_stop,或 halt 标记)→ 只要**存在任何 check 项/未决任务/账本记录,或结尾终审开启**,一律 `"exit_settlement"`;两者皆无才 `"end"`。
- **空计划(`not has_top_level`)且结尾终审开启 → 仍走 `"exit_settlement"`**:没有计划项不等于无可验之物,终审对象是用户原始目标。
- 新增节点 `exit_settlement_node`,两阶段,**阶段一无条件执行,阶段二受结尾开关控制**:
1. **结算(barrier)**:收齐 `checkpoint_tasks` 全部未决任务并按 §5.2 入账——这是全流程唯一有意的阻塞等待,带**加总超时** `settlement_timeout`(默认 120s):超时 cancel 剩余任务、对应项记 `unchecked(timeout)`。已启动的检查必须在退出前有账,不论结尾开关。
2. **终审(`disable_final_check=False` 时)**:运行 `run_final_check`,对照 **`state.initial_goal`(用户原始目标,不是计划)** + 全部 check 项:
- `at_end` 项:用最终状态判定;
- `on_complete` 项:**引用账本裁决**(当前 attempt 的 PASS/FAIL 直接采信,FAIL 过的 assert 保持 FAIL);从未检查过的(中途关闭/配额限制),基于锚点附近的**历史证据**补判,证据不足记 `unchecked`——禁止用当前屏幕重判过程断言;
- 终审自身超时/异常:fail-open 放行,账记 `inconclusive`。
3. 路由:终审通过或关闭 → `END`;verify 类未达成(assert 失败**不算**)→ 打回 unmet 项 + `check_feedback` 注入 → `perception`;`final_check_attempts` 达上限(默认 3)或 user_stop/halt → `END`。
- 收尾状态见 §5.5;continuous+user_stop 时 FAIL 不回循环。
### 5.4 Checker 本体(`agents/checker/checker.py` 重写)
- 两个入口共用旧 `run_async_check` 的工具循环骨架(多轮循环、迭代上限 `checker_max_iterations`、结构化兜底,可整体沿用):
- `run_checkpoint_check(check_items, anchor: EvidenceAnchor, goal, ...)`
- `run_final_check(goal, plan_text, ledger, ...)`
- **输入**:用户原始目标、计划、待验项、证据锚、`get_agent_friendly_steps()` 摘要历史。
- **工具表按入口 × 配置装配(§1.3;工具表是唯一权限来源,全部只读)**:
| 工具 | `run_checkpoint_check` | `run_final_check` | 配置开关 |
|---|---|---|---|
| `get_step_detail(step_no)` / `get_step_screenshot(step_no, which: pre\|post)`(`storage.get_steps` + step 目录,新增只读辅助) | ✓ | ✓ | 恒定 |
| `read_note`(只读) | ✓ | ✓ | 恒定 |
| `probe_device(kind, params)`(固定探测类型 + 程序生成 argv,见下) | ✓ | ✓ | `disable_device_probes`(默认 False);关闭则**不注册且 prompt 零提及** |
| 实时屏幕读取(`UnifiedMobileController.get_screen_data()` + OCR,旧 checker 有现成代码) | **✗ 不装配** | ✓ | 恒定(仅 final 入口) |
中途检查点**不装配实时屏幕**是刻意的证据纪律:检查点的证据是锚点历史与持久状态探测,当前屏幕早已随 Operator 前进,装配它只会诱导用错证据——纪律靠工具表落实,不靠 prompt 劝告。
**任何入口都不注册**:设备 action、write_note、子代理。
- **`checker.json` 拆为组件段,按待验内容与入口装配**(不在单一模板里堆条件文本):
| prompt 段 | 装配条件 |
|---|---|
| 基础裁决规则(逐字引用、证据具体化、inconclusive 语义) | 恒定 |
| verify 语义段(完成度判定、fail-open 放行规则、suggestion 要求) | 待验项含 verify |
| assert 语义段(测试断言、失败即结果、证据缺失记 inconclusive 不记 failed) | 待验项含 assert |
| 锚点取证指南(瞬态/顺序断言必须查锚点附近历史) | `run_checkpoint_check` 入口 |
| 终审指南(对照原始目标、引用账本、at_end 用最终状态、禁止重判过程断言) | `run_final_check` 入口 |
| probe 使用指南(持久状态优先探测、探测类型速查) | `probe_device` 已注册 |
- **`probe_device` 设计(不做前缀白名单)**:`dumpsys` 等含状态修改子命令(如 `dumpsys battery set level 100` 可改电量模拟),且前缀判断挡不住参数拼接。改为枚举探测表,argv 列表由代码构造、不经 shell 字符串拼接,参数正则校验(拒绝空白符与 shell 元字符):
```python
PROBES: dict[str, ProbeSpec] = {
"alarms": argv=["dumpsys", "alarm"], # 无参数
"battery": argv=["dumpsys", "battery"], # 无参数(禁 set/unplug 等子命令)
"foreground": argv=["dumpsys", "activity", "activities"],
"notifications":argv=["dumpsys", "notification"],
"setting": argv=["settings", "get", <ns>, <key>], # ns∈{system,secure,global};key: ^[A-Za-z0-9._-]+$
"content": argv=["content", "query", "--uri", <uri>], # uri: ^content://[A-Za-z0-9./_-]+$
"packages": argv=["pm", "list", "packages"],
"prop": argv=["getprop", <key>], # key: ^[A-Za-z0-9._-]+$
}
```
未列举的探测类型一律拒绝;扩表需改代码,不接受运行时自由命令。底层复用 `run_adb_command` 的执行通道但以 argv 形式传递。
- **输出**(structured output):
```python
class CheckVerdict(BaseModel):
item_text: str # check 行原文,逐字
kind: Literal["verify", "assert"]
status: Literal["passed", "failed", "inconclusive"]
evidence: str # 具体观察,禁止空泛
suggestion: str # 仅 verify failed 需要
class CheckReport(BaseModel):
verdicts: list[CheckVerdict]
unmet_subgoals: list[str] # 仅 final check:逐字引用计划项
```
放行决策(passed 布尔)由 **node 侧**从 verdicts 计算(verify 全 passed/inconclusive → 放行),不由模型直接输出——保证"放行≠裁决改值"。
- **各段共同要点**:failed 必须附具体证据(探测输出或截图元素),空泛否决 node 侧降级 inconclusive;证据从未被采集时输出 inconclusive 并说明"证据缺失"而非 failed;引用计划项逐字。
### 5.5 任务收尾与 SDK 传导(放行 / 测试结果 / 收尾状态三轴分离)
- Graph 在 END 前汇总 **run outcome**(写入 state 新字段 `run_outcome` 与 DataEngine session 元数据):
```python
class RunOutcome(BaseModel):
task_status: Literal["completed", "partial", "blocked"] # 目标达成轴
tests: TestSummary # passed/failed/inconclusive/unchecked 计数 + failed 明细
```
- verify 未达成且预算耗尽 → `blocked`(或 `partial`);
- **assert 有任何 FAIL → `tests.failed > 0`,即使 `task_status="completed"`**——任务做完与测试通过是两回事。
- **SDK 链路必改**(`sdk/agent.py`):
- 图完成处(~718-721):不再无条件打 success 日志 / `end_session("completed")`——读 `run_outcome`:`blocked` → `end_session("failed")`(确认 `data_engine.end_session` 支持的状态集,必要时扩);`completed` 但 `tests.failed>0` → `end_session("completed")` 且 session 元数据带 test summary,日志明确打出"任务完成、N 条断言失败";
- task.status 收敛处(~1035-1040)与 trace 命名处(~1061 `_PASS/_FAIL`):断言失败的运行 trace 命名用 `_TESTFAIL`(或并入 `_FAIL`,二选一但必须与 `_PASS` 区分);
- `mobile_run_task`/`mobile_manage_task` 返回结构透出 test summary,调用方无需翻报告正文即可拿到机器可读结果。
- Outputter 报告呈现完整裁决序列与三态统计;BLOCKED/partial 收尾时最后一次 finding 入元数据。
---
## 6. WP5 — 配置与 SDK 透传
- `ExecutionSetup`(`context.py`):
- `disable_midway_checks: bool = False`、`disable_final_check: bool = False`(默认开。成本与计划中 check 项数量成正比——§4.2 已要求 Planner 只在关键节点生成 verify,这是成本上界的来源,**不存在"零成本"**);
- `disable_checker` 保留为兼容别名:True 时两者同关;
- `final_check_max_attempts: int = 3`、`checkpoint_max_repairs: int = 2`、`max_concurrent_checkpoints: int = 3`、`checkpoint_timeout: float = 180.0`、`settlement_timeout: float = 120.0`、`assert_failure_policy: Literal["continue","halt"] = "continue"`、`disable_device_probes: bool = False`;
- `disable_planner_validation`、`checker_max_iterations` 语义不变。
- SDK 透传链:`sdk/types/agent.py`、`sdk/builders/agent_config_builder.py`(`with_disable_checker` 保留 + 新开关映射)、`sdk/agent.py`。
- 四组合行为:
| 中途 | 结尾终审 | 行为 |
|---|---|---|
| 关 | 关 | 不 spawn、不终审;**已启动的任务仍出口结算**(本组合下无任务);全部 check 项报 unchecked |
| 关 | 开 | 出口结算(空)+ 终审:`at_end` 判最终状态,`on_complete` 凭历史证据补判,不足记 unchecked |
| 开 | 关 | 检查点照跑;出口**只结算不终审**(阶段一无条件),账本与 test summary 照常产出 |
| 开 | 开 | 检查点 + 结算 + 终审 |
---
## 7. 明确不做(Out of scope,勿顺手扩大)
- **强制前置关卡**("确认 A 后才允许执行 B"的中途阻塞等待)——本期只提供事后核验;出口 barrier 保证的是**退出前完成裁决结算**,不保证执行顺序约束。此边界必须如实声明(§1.2),不得静默接受此类需求。
- "动作前"检查时机(前置状态是 Operator 感知职责)。
- 逐动作依赖分析。
- 多验证器投票、版本化 VerificationKey、shadow 遥测。
- Validator 拆分、统一 FailureEvent/RecoveryCoordinator、`_check_infinite_loop` 降级。
---
## 8. 测试
现有测试(先跑通再动手,改完逐个修):
- `tests/unit/graph/test_graph.py` —— execution_check_node / convergence_gate 断言大改。
- `tests/unit/agents/test_plan_mutation_guardrails.py` —— planner validation 主体不动;补 check 行确定性守护用例(§4.3)。
- `tests/unit/utils/test_plan_grammar.py` —— verify/assert/@end 解析、不影响 milestone hash。
- 全仓 grep `verification_chat`、`checker_status`、`task_plan_snapshot`、`create_snapshot`、`troubleshooter`、`ctx.checker_task`、`checker_max_chat_rounds`:零残留。
新增(建议 `tests/unit/graph/test_checkpoints.py` + `test_exit_settlement.py`):
1. 排队/spawn 分离:`_process_plan_write` 只排队;execution_check 在 record_step 后 spawn,anchor_step_id 等于本 turn 刚记录的 step。
2. 多个同轮完成项全部排队;无 check 项不排队;中途开关关不排队;user_stop 后不 spawn。
3. supersede:旧任务 done 未收割 → 先入账再替换(**失败裁决不丢**);运行中 → cancel 且 await 完成,账本有 superseded 记录。
4. 收割只取 done;适用性校验:过期 attempt / 子目标文本已变 → 只入账不打回。
5. verify FAIL 打回 + 注入;超 `checkpoint_max_repairs` 后不再打回、裁决保持 failed。
6. assert FAIL 只入账;`halt` 策略触发收尾;账本 append-only(先 FAIL 后 PASS 两条都在)。
7. 超时:单任务超 `checkpoint_timeout` → inconclusive;结算超 `settlement_timeout` → 剩余 cancel + unchecked(timeout)。
8. 出口路由:结算与终审解耦——终审关、有未决任务 → 仍进 exit_settlement 且只走阶段一;空计划 + 终审开 → 进终审;全关全无 → 直接 end。
9. `when` 语义:`at_end` 不在中途执行;`on_complete` 在终审引用账本、无账本凭历史补判、证据不足 unchecked(构造"当前状态已满足但历史证据缺失"的用例,断言**不**判 passed)。
10. record_step 无条件:正常 turn 与 planner 拒绝 turn 都有记录,后者带 `planner_rejected` 标记。
11. check 行守护:Operator 写入删除 check 行 → 合并回写;父目标被删 → 转任务级 `@end`;Planner 写入可修订;planner validation 关闭时守护独立生效。
12. `probe_device`:枚举表内放行、表外拒绝、参数带空白/元字符拒绝、`battery set` 类子命令无法表达。
13. fail-open 三轴分离:verify 异常 → 放行但账记 inconclusive(**不是 passed**)。
14. run outcome / SDK:assert 失败 → `tests.failed>0`、trace 名非 `_PASS`、`end_session` 元数据含 summary;verify 预算耗尽 → `blocked` + `end_session("failed")`。
15. continuous+user_stop+FAIL → 不回循环。
16. 四开关组合矩阵(§6 表逐行)。
17. prompt 条件化:两开关全关 → `render_plan_grammar_spec` 输出不含 check 语法、planner prompt 无生成指令;`CheckItemsExplainerPromptComponent` 仅在计划含 check 行时渲染(含"开关关但旧计划带 check 行仍渲染"的用例)。
18. checker 装配:`run_checkpoint_check` 的工具表**无实时屏幕**、`run_final_check` 有;`disable_device_probes=True` → `probe_device` 未注册且 prompt 无探测指南;待验项全 verify → prompt 无 assert 段(反之亦然);checkpoint 入口无终审指南段。
运行:`python -m pytest tests/unit/graph tests/unit/agents tests/unit/utils -x`(需要假 LLM 设 `ARTEMIS_FAKE_LLM=1`)。
---
## 9. 验收标准
1. Operator 的 prompt 模板、notes、DataEngine 历史在任何验证结果下不被回滚/切换/丢弃;**每个 operator turn(含 planner 拒绝的)都有 step 记录**;中途路径上不存在对未完成验证任务的等待。
2. verification chat、`checker_status.json`、notes 全量快照、单槽 `ctx.checker_task` 从代码库消失。
3. 检查项由 Planner 声明(verify/assert × on_complete/at_end),Operator 删除/弱化 check 行会被**确定性守护**恢复,不依赖 planner validation 开关。
3a. **§1.3 装配纪律全局成立**:检查全关时 planner/operator prompt 与无此功能时一致(零污染);计划含 check 行时 Operator 收到行为说明段;checker 的 prompt 段与工具表按入口 × 配置装配(checkpoint 入口无实时屏幕、probe 关闭则工具不注册且 prompt 零提及、无 assert 项则无 assert 段);不存在指向已关闭机制的死指令。
4. 检查点在 record_step 之后 spawn,证据锚指向正确的 step;裁决全部 append-only 入账,supersede 不丢已完成裁决;检查有单次超时、并发上限、修复配额。
5. 出口结算无条件执行(含加总超时与 unchecked 标记),终审受结尾开关控制;空计划不绕过已开启的终审;终审对照用户原始目标;`at_end`/`on_complete` 判定时机不错位。
6. 放行、裁决、收尾三轴分离:fail-open 不改写裁决值;assert 失败产生机器可读 test summary 并传导至 `sdk/agent.py` 收尾(日志、end_session、trace 命名、返回结构)。
7. `probe_device` 为枚举探测 + 程序生成 argv,无自由命令通道。
8. 计划/报告如实声明"事后核验"边界,强序需求不被静默接受。
9. planner validation(入口卡点)除新增 check 行审计外与现状一致。
10. §8 测试全绿。
---
## 10. 建议执行顺序
WP1(3.2 → 3.3 → 3.1 → 3.4,摘干扰源,得到干净中间态)→ WP2(§4 语法 + Planner + 守护,独立可测)→ §5.4 checker 本体与 probe 表(可单测)→ §5.1 触发 → §5.2 收割 → §5.3 出口 → §5.5 收尾传导 → WP5 配置透传 → 全量测试。
每步完成后系统都应可运行:WP1 后=无验证;WP2 后=计划里有 check 项但无人消费(守护已生效,无害);§5.4 后=checker 可单测;接线后=全功能。
+586
View File
@@ -0,0 +1,586 @@
# ARTEMIS 上下文与记忆重构:显式状态、按 agent 可见域、统一记忆内核
状态:提案 v2(2026-08-31,经"不损伤"审计细化,待终审)
范围:State / ArtemisContext 的类型化与生命周期分域;感知数据通道;Flash 压缩机制上提为通用记忆内核。
底线与目标:**不损伤为最低要求**——每一处删除/搬迁都在附录 B 台账中核销其全部消费者,
任何信息流不得静默中断;**优雅处理所有信息为目标**——每份数据有唯一属主、显式通道、
声明的可见域,重复实现收敛为单一入口。
姊妹文档:`docs/design/pro-context-memory-redesign.md`(2026-08-25)深度覆盖了 Pro 侧
"压缩什么、怎么压"(视觉摘要、历史分块、召回工具、token 预算)。本提案覆盖它没有处理的
另一半——**数据放在哪、谁能看见什么、通道是否显式**——并在 §6 把两份文档的交叠处收敛为
同一个记忆运行时。
---
## 1. 调查结论:现状与证据
### 1.1 实际存在四个上下文平面,只有一个有主人
| 平面 | 载体 | 生命周期 | 现状 |
|---|---|---|---|
| P1 图通道 | `State`(pydantic + `extra="allow"` + reducer) | 跨节点、跨回合 | 声明字段与实际使用严重脱节(见 1.2) |
| P2 就地突变 | 同一个 State 实例被节点/工具直接赋值 | **节点内**(经 `InjectedState` 同实例) | 无声明的隐式契约;经核验其全部消费都是节点内的(见 5.3) |
| P3 ctx 动态属性 | `ArtemisContext`(`extra="allow"`) | 进程/运行期 | `latest_perception_data` 等动态挂载,无 schema |
| P4 持久存储 | DataEngine(步骤/截图/摘要)+ notes 文件 | 持久 | **唯一健康的平面**:单一写入路径,多消费者 |
Pro 的真实架构其实很好:Operator 每回合从 P4 **重新编译**全量 prompt
(`operator.py:624` → `build_plan_and_history`),不靠累积消息。问题不在架构方向,
而在 P1–P3 三个平面无 schema、无属主、互相兜圈子。
### 1.2 `extra="allow"` 已经吞掉的真 bug(本次调查实证)
**B1 幽灵键 `ui_tree`:4 个读取点,0 个写入点。**
`operator.py:995`、`validator/tool_declarations.py:477`、`mcp/action_executor.py:401`、
`tools/mobile/exec_tools.py:548` 均 `getattr(state, "ui_tree", None)`,但全仓无人写入
(真实数据在 `latest_ui_hierarchy`)。后果:`utils/coordinates.py:185-215` 的
"从层级树找主滚动容器"分支**永远走不到**,smart swipe 静默降级。
**B2 异步 Planner 驳回反馈被静默丢弃。**
`graph.py:195` 把驳回理由写进 `validator_messages` 通道,但该通道**没有任何节点读取**
(`operator.py:388` 和 `tool_wrapper.py:175` 只是当场拆 `Command.update` 信封,从不读
State 通道)。后果:计划被回滚了,Operator 下一回合看到回滚后的计划,却永远看不到
驳回理由。同时 `validator_messages` 经 `add_messages` 单调累积整轮所有 ToolMessage,
无人消费、无人裁剪。
**B3 `ctx.latest_perception_data` 孤儿写。**
`perception.py:187` 每轮把整张截图 b64 挂上 ctx,注释声称"给 Checker 零延迟复用";
但工作区的 checker 重写已改为自己抓屏(`checker.py:620-652`)+ 从 DataEngine 取历史
证据。全仓**零读取点**,纯内存驻留。
**B4 `indexed_elements` 只写实例不回通道。**
`operator.py:649-650` 就地写两个字段,但 `operator.py:766` 的 update 只回传
`indexed_points`,漏了 `indexed_elements`。审计确认跨节点消费实际不存在(validator 侧
自行重推导,`action_executor.py:173-174`),但两键不对称是持续的误导源。
**B5 更多幽灵/半死键与坏路径。** 全表见附录 B。要点:
约 9 个只初始化/只读不写的死键;`graph_runner.py:48` 的
`getattr(self.ctx, "artemis_context", None)` 恒 None → 构造出丢失 data_engine 的空
ArtemisContext;且 `reactive_runner.py` 的 `State(messages=[])` 缺必填字段构造即抛
——engine 双 runner 的真机路径今日均必炸;`_active_driver` 未声明为 `PrivateAttr`。
(v2.1 更正:初版审计将 `operator_agent.py:63` 的 `task_goal` 与
`failure_analyzer_agent.py:48-50` 的 `failure_context` 列为图 State 幽灵键,实为
engine/BaseAgent 栈自己的 state 协议——`ExecutionContextState.task_goal` 存在
(core/state.py:48),`.get()` 有 `isinstance(state, dict)` 防护。两处均无需修改。)
**B6 感知数据同屏最多 4 份拷贝、抓屏三处重复实现。**
同一帧 b64 同时在 `ctx.latest_perception_data`(死)、`state.operator_raw_data`
(走图通道且随 `astream` values 流每超步复制,`sdk/agent.py:694-696`)、operator 局部、
prompt data-URL。抓屏管线在 perception_node(settle+OCR+融合)、validator 动作后轮询
(`validator.py:117-140`)、checker 终检(`checker.py:620-652`)三处各写一遍,
planner 首帧还有第四处自拍(`planner.py:211-236`,注意 planner 在图中先于 perception
运行,这是常规路径不是边角)。
### 1.3 Flash 与 Pro 的压缩机制现状
| | Flash | Pro |
|---|---|---|
| 上下文模型 | 累积消息 + 每轮幂等原地压缩(`compress_flash_messages`) | 每回合从 DataEngine 重编译(`build_plan_and_history`) |
| 步骤摘要 | `VisualStepSummarizer`:前后截图 + 动作红标叠加,客观中立措辞契约,异步无阻塞,无界重试 | `SummarizerNode`:**纯文本**(不加载 pre/post 图),3 次退避重试,无 flush 收尾语义 |
| 摘要落点 | `data_engine.update_step_summary`(同一个 API!) | `data_engine.update_step_summary` |
| 未就绪降级 | 摘要 pending → 保留原图(lossless pending) | 无 summary → detailed 渲染(同构原则的另一形态,`task_tree.py:917-921`) |
| 历史可见性策略 | 无(30 轮上限内全量) | 三种互斥策略 + 6 个调用点各自硬编码 kwargs |
关键事实:**两条路径的摘要已经写进同一个 DataEngine 字段**。但两者的摘要语义结构性
不同(见 6.1),统一的只能是运行时,不是语义。
---
## 2. 设计原则
1. **上下文是编译出来的,不是累积出来的。** Pro 已经如此;把它升格为正式模型,
Flash 的消息列表只是同一份记忆的另一种渲染。
2. **一份情景记忆,多种视图。** DataEngine 是唯一事实源;每个 agent 看到的是按其
角色声明的"最高价值密度视图"。agent 的智能上限由输入质量决定。
3. **可见域靠声明与类型约束。** `extra="forbid"` 拦截幽灵**写**与非法构造;幽灵**读**
(`getattr` 带默认值不会因 forbid 报错)由节点 READS/WRITES manifest 的测试期
代理拦截。两者缺一不可。
4. **重物不过通道。** 截图 b64 不进 LangGraph 通道;通道里只放引用,重物放进有属主、
有生命周期的 store。
5. **就地突变不消灭,而是正名。** P2 平面的全部用途经审计都是"节点内工具通信"——
给它一个声明的、节点局部的载体(TurnWorkspace),而不是假装它不存在。
6. **后台增强不可成为依赖。**(继承姊妹文档 §4.6)摘要未就绪必有无损降级。
7. **不损伤:删除必先核销。** 每个字段/属性/通道的处置都对照附录 B 台账,列明全部
消费者与同步改动点;无消费者证据不删。
---
## 3. 方案 A:State 显式化与按 agent 可见域
### 3.1 State 收敛为四组显式字段,`extra="forbid"`
```python
class State(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")
# ── 控制面(跨回合、路由与信号)──────────────────────
initial_goal: str
injected_instruction: str | None # take_last
user_stop_requested: bool # sticky_or
checker_success: bool | None # take_last
operator_feedback: list[str] | None # take_last;见 3.3
exit_settlement_route: str | None # take_last
run_outcome: dict | None # take_last
# ── 回合产物(上一回合的决策与结果)───────────────────
current_step_id: str | None
structured_decisions: str | None
operator_raw_thinking: str | None
operator_native_thinking: str | None
short_term_memory: str | None # 活跃字段;移除权属于姊妹文档 §3.3
last_execution_result: dict | None
subagent_calls: list[str]
operator_tool_limit_exceeded: bool | None
# ── 感知引用(重物不进通道,见方案 C)─────────────────
perception: PerceptionRef | None # take_last
# ── 节点局部工作区(声明但非通道语义,见 5.3)─────────
workspace: TurnWorkspace | None = None # 不进 update、不跨节点
```
**处置总表见附录 B。** 与 v1 的差别:`short_term_memory` 保留(它是活跃读写对,
operator.py:743-765 写 → prompts.py:331 读;删除属姊妹文档 Phase 1 的事,本方案
不越权);`indexed_*` 不再声称"并入冻结快照"(见 5.3);`messages` 删除需同步改
三处消费代码(`sdk/agent.py:1182-1187` 兜底输出、`sdk/agent.py:1321-1325` stderr
打印、`engine/graph_runner.py:57` 入参——三处今日均为空转/坏路径,删除无信息损失,
但必须同 PR 内改掉)。
`asanitize_update`(`state.py:152-159` 空壳直通)删除;其在 14 处工具里的调用
随信封迁移(3.3)一并消失。
### 3.2 可见域:节点声明读/写集,测试期强制
每个节点类声明清单,运行期零开销,测试期强制:
```python
class OperatorNode:
READS = frozenset({"initial_goal", "perception", "operator_feedback",
"short_term_memory", "subagent_calls", "current_step_id",
"operator_tool_limit_exceeded", "injected_instruction",
"structured_decisions"})
WRITES = frozenset({"structured_decisions", "operator_raw_thinking",
"operator_native_thinking", "short_term_memory",
"current_step_id", "subagent_calls",
"operator_tool_limit_exceeded"})
```
- 新增 `artemis/graph/visibility.py`:`StateView(state, reads)` 代理,未声明字段访问
即抛 `VisibilityError`;`check_update(update, writes)` 校验返回键。
- 仅在测试与 `ARTEMIS_STRICT_STATE=1` 下启用;生产路径直接传 State。
- 附录 B 的实证读写清单即初始 manifest;上线先按现状放宽,再逐步收紧。
- **幽灵读的防线在这里**,不在 `extra="forbid"`(forbid 管不住 `getattr(x, k, None)`)。
### 3.3 工具信封与反馈通道拆分(修 B2)
`validator_messages` 承担两个不相干职责,拆开:
**1)工具返回信封 → 显式返回类型。**
审计确认全部 14 个工具写入点(`exec_tools.py` 5 处、`scratchpad.py` 6 处、
`launch_app.py:191`、`ocr.py:185`、`video_tool.py:161`、`object_detector.py:210,227`)
的 `Command.update` **只含**这一个键(以 `exec_tools.py:215-221` 为样本核验),
迁移是纯机械的:工具直接返回 `ToolOutcome(content, status)`,
`tool_wrapper.get_tool_result_content`(`tool_wrapper.py:175-176`)与 operator 循环
(`operator.py:388-391`)的拆信封逻辑同步简化。`graph.py:526` 的计划驳回信封同理
(它经 `_process_plan_write` 返回给 operator 循环当场消费,属节点内信封)。
通道及 `add_messages` 无界累积随之消失。
**2)跨回合反馈 → 统一 `operator_feedback` 通道。**
`check_feedback` 更名并扩容为 `operator_feedback`:来源包括 checker harvest
(`graph.py:198,230`)、exit settlement 弹回(`graph.py:367`)、以及**新接入的**
异步 Planner 驳回(`graph.py:195` 改写到这里,条目带 `[planner]` 前缀,与现有
`[final check]`/`[verify failed]` 前缀惯例一致)。渲染组件更名
`FeedbackPromptComponent`,提示语从"来自独立 Checker"改为按条目前缀中性表述。
现有读者同步改名:operator prompt(`prompts.py:243`)与 diagnoser
(`diagnoser.py:178`)——diagnoser 因此也能看到 planner 驳回上下文,属增益。
---
## 4. 方案 B:ArtemisContext 分解为组合根
ctx 保留为唯一入口(避免全仓签名大改),字段按属主聚类成显式子对象,
`extra="forbid"`:
```python
class ArtemisContext(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")
# 基础设施(启动后只读)
device: DeviceContext
llm_config: LLMConfig | None
model_router: Any | None
agent_config: Any
adb_client / ui_adb_client / actuator / action_session / mcp_*
# 运行服务(有生命周期、有属主)
execution_setup: ExecutionSetup | None
data_engine: DataEngine | None
perception: PerceptionStore # 方案 C,取代 latest_perception_data
step_memory: StepMemoryService | None # 方案 D,统一摘要运行时
background: BackgroundTaskGroup # 吸收 background_tasks/jobs + 宽限期
# 编排状态(从散字段聚成对象)
checkpoints: CheckpointLedger # 吸收 pending_checkpoints/checkpoint_tasks/
# attempt_seq/repairs/final_check_attempts/assert_halt
plan_governor: PlanGovernor # 吸收 task_plan_content_before/
# last_validated_plan/pending_validated_plan/planner_task
run_outcome: dict | None = None # 显式化(今日为 sdk/agent.py:721 动态挂载)
package_cache: dict[str, str | None]
_active_driver: Any | None = PrivateAttr(default=None)
```
`CheckpointLedger` 与 `PlanGovernor` 不是新逻辑,只是把 `graph/checkpoints.py` 和
`graph/graph.py::_process_plan_write` 已在操作的字段簇搬进属主类型。
`ExecutionSetup` 审计后可直接上 `forbid`(唯一运行期赋值 `app_lock_status` 为已声明
字段,`sdk/agent.py:997`)。`graph_runner.py:48` 的 `artemis_context` 恒 None 坏路径
在 Phase 0 修复(显式传入或显式报错,禁止静默构造空 ctx)。
---
## 5. 方案 C:感知走显式通道(PerceptionStore + TurnWorkspace)
### 5.1 数据模型
```python
class PerceptionSnapshot(BaseModel):
"""一帧感知的产物。frozen:每次捕获生成新快照,绝不就地改。"""
model_config = ConfigDict(frozen=True, extra="forbid")
step_id: str | None # 关联的 DataEngine step(轮询帧可为 None)
image_name: str # sha256,DataEngine 图片主键
screenshot_path: str | None
screenshot_b64: str # 重物:只存在于 store,不进图通道
width: int
height: int
captured_at: float
kind: Literal["decision", "post_action", "probe"]
# 完整管线产物(raw 帧为 None)
raw_xml: list[dict] | None
fused_elements: list[dict] | None # OCR+XML 融合
ocr_results: list | None
minimal_list: str | None # 派生物在此算一次,消费者不再各自重算
class PerceptionRef(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
step_id: str | None
image_name: str
width: int
height: int
class PerceptionStore:
"""ctx.perception。环形缓存最近 N=4 帧;更早的按 image_name/step_id 从
DataEngine 回捞重建(b64 懒加载)。"""
async def capture_full(self, *, settle: bool, kind: str) -> PerceptionSnapshot: ...
async def capture_raw(self, *, kind: str) -> PerceptionSnapshot: ...
def put(self, snap: PerceptionSnapshot) -> PerceptionRef: ...
def latest(self, *, full_only: bool = False) -> PerceptionSnapshot | None: ...
def get(self, ref: PerceptionRef | str) -> PerceptionSnapshot | None: ...
```
**抓屏统一收口**:现有三处重复管线 + planner 自拍全部改走 store——
| 现调用点 | 改为 | 备注 |
|---|---|---|
| `perception_node`(settle+OCR+融合,`perception.py:116-168`) | `capture_full(settle=..., kind="decision")` | settle 启发式与 OCR/融合逻辑原样搬入 store,行为不变 |
| validator 动作后轮询(`validator.py:117-140`) | 轮询保持轻量私有;**最终帧** `capture_raw(kind="post_action")` 入 store | 轮询中间帧不入 store,避免噪音;最终帧成为可引用证据(今日被就地写进 `state.latest_screenshot` 后即失名) |
| checker 终检自拍(`checker.py:620-652`) | `capture_full(kind="probe")` | 其 OCR+裁剪逻辑与 perception 重复,删除 |
| planner 首帧自拍(`planner.py:211-236`) | `capture_full(kind="decision")` | planner 先于 perception 运行是常规路径,必须显式支持 |
由此**每一帧都自动进入环形缓存与 DataEngine**,异步消费者(Checker 检查点)持
`PerceptionRef`/`image_name` 而非活对象——缓存被挤掉也能从 DataEngine 重建,
天然解决"异步任务引用过期感知"的悬挂问题。
### 5.2 通道改造
- `perception_node`:`capture_full` → `put()` → update 只含
`{"perception": ref, "injected_instruction": ..., "user_stop_requested": ...}`。
删除 `ctx.latest_perception_data`(B3)与 `operator_raw_data` 过通道(B6:
b64 不再随 `astream` values 每超步复制)。
- 消费者一律 `ctx.perception.get(state.perception)` 或 `latest()`:
- **Operator**:快照直取 `minimal_list`/`fused_elements`,不再自算;
- **Validator 内工具**(object_detection 兜底 `object_detection_tool.py:93` 今日读
validator 就地写的 `latest_screenshot`):改读 `latest()`——validator 每次动作后
`capture_raw` 入 store,时序语义与今日一致;
- **Diagnoser/Committee/Explorer/FA**:同一入口,删除各自的文件重读与 getattr 链;
- **Outputter**:今日读最终 State 的 `operator_raw_data`(最后决策帧)。改为
`latest()`(最后 post_action 帧,比决策帧更新,属增益);若需保持逐字节兼容,
改读 `get(final_state.perception)` 即决策帧——落地时二选一并记录;
- 幽灵键 `ui_tree` 的 4 个读取点改读 `snapshot.fused_elements`(B1 终态修复;
Phase 0 先直改 `latest_ui_hierarchy` 止血)。
### 5.3 TurnWorkspace:给 P2 平面正名(修 B4 的正确姿势)
审计结论:`indexed_points`/`indexed_elements` 是**回合内可变工作集**——
explorer 增量 append 候选(`explorer_tool.py:311-315`),validator/FA 在动作后
整体重推导(`action_executor.py:173-174`);且全部消费都发生在**产生它的节点内部**
(operator 在返回前就把索引翻译成坐标;validator 侧自给自足)。跨节点传递从未
被需要——B4 之所以没被发现,正因为如此。
因此它们**不属于冻结快照,也不属于图通道**,属于节点局部工作区:
```python
class TurnWorkspace(BaseModel):
"""节点局部的可变工作集。经 InjectedState 对同节点工具可见;
不进 update、不跨节点、每节点从快照重播种。"""
model_config = ConfigDict(extra="forbid")
indexed_points: list[list[int]] = []
indexed_elements: list[dict] = []
@classmethod
def seed_from(cls, snap: PerceptionSnapshot) -> "TurnWorkspace": ...
def reseed(self, snap: PerceptionSnapshot) -> None: ... # validator/FA 重观察
def extend_candidates(self, cands: list[dict]) -> None: ... # explorer 增量
```
- `state.workspace` 为声明字段但**永不出现在任何节点的 update 里**(visibility
manifest 强制),每个需要它的节点入口自行 `seed_from(store.get(state.perception))`。
- 隐式契约("恰好同一实例")就此变成显式契约("声明的节点局部容器 + 三个具名操作")。
- `state.indexed_points`/`indexed_elements`/就地写 `latest_screenshot`/`operator_raw_thinking`
等全部 P2 突变点随之收编(thinking 就地写是给工具内 `record_step` 读的,同为
节点内通信,归入 workspace 或显式传参,落地时按触点少者取舍)。
---
## 6. 方案 D:记忆内核统一(Flash 压缩上提)
### 6.1 一个运行时,两份摘要契约
两个摘要器的差异是**结构性的**,不能合并语义:
- Flash 的摘要是**证据替身**——消息流保留了模型全部推理原文,压缩器只摘图,
摘要因此必须是纯客观的视觉转换描述,掺入意图即是污染;
- Pro 的摘要是**整步胶囊**——`build_plan_and_history` 丢弃旧步骤的全部思考,
一行摘要+精确动作+Validator 结果就是该步的全部遗留,因此必须压缩意图/策略/
进度(这正是姊妹文档 §8.2 坚持 `summarizer.md` 为语义唯一权威的原因)。
因此统一的是**机制层**,分离的是**语义层**。新建
`artemis/memory/step_memory.py`:
- **共享运行时 `StepMemoryService`**(合并两边的调度骨架):
- 键:`step_id`(Flash 的 `tool_call_id` 保留为别名映射;`dispatch` 已有
`data_engine_step_id` 参数,接线即可);
- 调度:零阻塞 dispatch + **有界**重试 + 显式降级态(替换 Flash 的无界重试与
Pro 的 3 次退避;此为有意的行为变更,见 §8)+ `flush(timeout)` 收尾语义
(Pro 目前 run 结束时未完成摘要行为未定义,直接补上);
- 落点:`data_engine.update_step_summary`;状态机(pending/ready/failed)与
版本化按姊妹文档 §7.1;lossless-pending 原则由消费侧共同遵守;
- 素材来源:pre/post 图直接以 `PerceptionRef` 引用(方案 C 的红利:不再在
dispatch 参数里搬运 bytes,`summarizer.py:92-100` 的 `_step_inputs` 图字节
缓存与手动置 None 回收逻辑消失)。
- **两份摘要契约(lens),各 profile 只跑自己的**:
- `VisualTransitionLens`(Flash):输入 pre/post 截图(动作红标叠加)+ 动作 +
Controller 结果;产出客观视觉转换,`flash_summarizer.md` 禁判定词契约不变;
- `StepCapsuleLens`(Pro):输入按姊妹文档 §8.1(决策图 + 可选后图 + 动作 +
Validator 结果 + FA 记录 + 当步思考 + 少量近期摘要);产出意图/策略/进度
胶囊,`summarizer.md` 为语义权威,视觉仅作接地、仍禁自行判定成败
(姊妹文档 §4.5 事实优先级)。
- 单 profile 单 lens,`update_step_summary` 单字段无冲突;未来若 Pro 需同时保留
视觉转换记录,按姊妹文档 §7.1 版本化元数据加 lens 标记,不在本期范围。
- Pro 图中 `SummarizerNode` 退化为 `dispatch(StepCapsuleLens)` 一行调用;
Flash `runner.py` 同理换成 `dispatch(VisualTransitionLens)`。
### 6.2 两个渲染器,一张策略表
渲染器保持各自形态(合理的方言差异,不强行同构):
- `compress_flash_messages`:消息列表原地压缩,摘要后端换成共享运行时;
- `build_plan_and_history`:编译式渲染,无改动即消费到视觉接地摘要。
把 6 个调用点散落的硬编码 kwargs 收拢为**每 agent 一条声明**,新建
`artemis/memory/context_policy.py`:
```python
CONTEXT_POLICIES: dict[str, ContextPolicy] = {
"operator": ContextPolicy(strategy="strict_milestone", last_n_detailed=1, recent_window=3, chronological_last=True),
"planner": ContextPolicy(strategy="strict_milestone", last_n_detailed=2, recent_window=5, chronological_last=True),
"failure_analyzer": ContextPolicy(strategy="strict_milestone", last_n_detailed=1, recent_window=3, for_failure_analyzer=True),
"checker": ContextPolicy(strategy="milestone_whitelist"),
"diagnoser": ContextPolicy(strategy="milestone_whitelist", window_from_config="history_window_steps"),
"committee": ContextPolicy(strategy="milestone_whitelist", last_n_detailed=3),
"outputter": ContextPolicy(strategy="full", last_n_detailed=0),
"summarizer": ContextPolicy(strategy="sliding_window", last_n_detailed=1, min_summaries=10),
}
```
这张表与 3.2 的 READS/WRITES manifest 互为镜像:一个声明"看 State 的哪些字段",
一个声明"看历史的哪个投影"。合起来就是"按 agent 划分可见域"的完整答案,
也是今后调优各 agent 输入质量的唯一改动点。收拢时**逐调用点对拍**:策略表渲染
输出与现 kwargs 输出必须一致(对拍测试进 CI)。
### 6.3 与姊妹文档的配置收敛
姊妹文档 §14 主张 Pro 单独 `agent.pro.memory`、不复用 Flash `step_summarizer`。
本方案在语义层同意(lens 分离),仅在**运行时层**共享:
```jsonc
"agent": {
"memory": { // 共享运行时:重试/flush/并发 + 策略表覆盖
"runtime": { "max_concurrency": 1, "retry_limit": 3, "flush_timeout_s": 30 },
"policies": { "operator": { "recent_window": 3 } }
},
"flash": { "step_summarizer": { /* VisualTransitionLens:模型、开关(现有键保持兼容) */ } },
"pro": { "memory": { /* StepCapsuleLens 模型/输入项 + token 预算等,per 姊妹文档 §14 */ } }
}
```
姊妹文档的 L3 历史分块、`recall_history`、token 预算不变,全部建立在同一个
`StepMemoryService` 产物之上。
---
## 7. 分阶段落地(已按依赖重排,消除 v1 的顺序隐患)
**Phase 0 — 修 bug(不动架构,可独立合入,零行为风险)** ✅ 已实施 2026-08-31
1. B1 止血:4 处 `ui_tree` → `latest_ui_hierarchy`(终态在 Phase 2 改快照)。✅
2. B2:`graph.py` 驳回反馈改走现有 `check_feedback` 通道(条目带 `[planner]`
前缀,弃写无人读的 validator_messages),`CheckFeedbackPromptComponent`
提示语按前缀中性化(标题保持 "Verification Findings" 不变,兼容既有断言)。✅
3. B3:删 `ctx.latest_perception_data` 写入与过期注释。✅
4. B4 止血:operator update 补回 `indexed_elements`(消除两键不对称;终态在
Phase 2 收编进 workspace)。✅
5. B5:`_active_driver` → `PrivateAttr`;新增 `State.initial()` 工厂统一初始状态
构造(sdk `_get_graph_state` 与 engine 双 runner 三处收拢单源);
`ExecutionContext.artemis_context` 显式声明;`graph_runner`/`reactive_runner`
真机路径缺 ctx 时显式 RuntimeError(替代静默构造空 ctx 后期炸)。
(task_goal / failure_context 两项经复核为误报,见 §1.2 v2.1 更正。)✅
验证:目标子集 99/99 通过;全量 `tests/unit + tests/tools` 1072 通过、
5 失败均为真实 LLM 联网测试的 API key 环境问题(与改动无关)。
**Phase 1 — State 显式化(仅动经台账核销的死键与信封)** ✅ 已实施 2026-08-31
实施要点(与下述计划的差异):信封迁移最终形态为**工具直接返回 ToolMessage**
(承载 content/status/tool_call_id,未传 state 时仍返回纯字符串),未引入独立的
ToolOutcome 类型——ToolMessage 已是 LangChain 原生形态,语义完全覆盖;
`_process_plan_write` 的拒绝路径返回 status="success" 的 ToolMessage 以逐字节保持
旧信封行为(SystemMessage 无 status 属性 → 旧路径视为 success)。visibility
manifest 已接线至全部 7 个节点入口(`strict_state`,默认关闭零开销),
契约测试 tests/unit/graph/test_state_contract.py 11 项覆盖往返/forbid/代理/清单。
验证:tests/unit 全量 1067 通过 0 失败;tests/tools 仅余 5 个既有 API key
环境失败(与 Phase 0 前完全一致)。测试改写含 test_operator.py 12 处
asanitize 断言 → 返回值断言、15 个工具测试文件 Command→ToolMessage。
- 删真死键:`messages`(同步改 `sdk/agent.py:1182-1187`、`:1321-1325`、
`graph_runner.py:57`、`_get_graph_state`)、`remaining_steps`、`focused_app_info`、
`device_date`、`subgoal_plan`、`operator_tactical_plan`、`complete_subgoals_by_ids`
(同步改 `operator.py:767` 与 `_get_graph_state`)、`current_agent`(scratchpad
两处读改固定 `"operator"` 或显式传参)、`operator_replan_reason`(committee 去引用)。
- **不删**:`short_term_memory`(活跃;移除并入姊妹文档 Phase 1 落地时执行)、
感知相关键(归宿在 Phase 2)。
- 信封迁移:14 个工具写入点 → `ToolOutcome` 直返;`operator.py:388-391` 与
`tool_wrapper.py:175-176` 拆信封逻辑简化;删 `validator_messages` 通道与
`asanitize_update`。
- `check_feedback` → `operator_feedback` 更名(operator prompt + diagnoser 两读者
同步)。
- `extra="forbid"` 上 State + `State(**values)` 往返测试(`sdk/agent.py:696` 路径);
visibility manifest + StateView 测试强制(先按附录 B 现状放宽,再收紧)。
**Phase 2 — PerceptionStore + TurnWorkspace**
- store/快照/引用/环形缓存;四个抓屏点收口(§5.1 表);消费者逐个切换;
- 删 `operator_raw_data`、`latest_ui_hierarchy`、`latest_screenshot`、
`indexed_points`、`indexed_elements`(State 通道侧);新增 `perception` ref 与
`workspace`;
- outputter 帧选择决策(post_action vs 决策帧)落地并记录。
**Phase 3 — ctx 分解**
`CheckpointLedger` / `PlanGovernor` / `BackgroundTaskGroup`;`run_outcome` 显式化;
`extra="forbid"` 上 ArtemisContext / DeviceContext / ExecutionSetup / AppLaunchResult。
**Phase 4 — 记忆内核**
`StepMemoryService` 统一运行时、双 lens 分立语义(Pro 侧 `StepCapsuleLens` 即
姊妹文档 Phase 2 的实现载体);策略表收拢(逐点对拍);配置收敛。之后按姊妹文档
继续 L3 分块与 recall。
每阶段独立可回滚。Phase 0/1 以台账+现有单测护航;Phase 2 起加对拍测试。
---
## 8. 验收
**不损伤(最低要求)**
- 附录 B 台账全表核销:每个字段/属性的每个消费者都有对应改动与测试;CI 中
visibility manifest 测试保证幽灵读为零。
- `State(**values)` 往返(`astream` values 流)在 `forbid` 下全绿。
- Flash:成功路径下压缩器输出与现版**逐字节对拍一致**;重试上界与 flush 属有意
变更,单独测试覆盖(失败注入下有界退出 + 降级态可见)。
- Pro:`build_plan_and_history` 策略表收拢后与现 6 处调用点输出逐点对拍一致。
- 性能红线沿用姊妹文档 §18(P50/P95 输入与延迟回归 ≤3–5%,关键路径零摘要等待)。
**优雅处理所有信息(目标)**
- 全部 `extra="forbid"`;抓屏管线唯一实现;图通道序列化不再含任何 b64
(对照:当前 `operator_raw_data` 每超步随 values 流复制)。
- Planner 驳回后,Operator 与 Diagnoser 的下一 prompt 可见驳回理由(新增回归测试)。
- validator 的 post-action 帧从"就地写路径后失名"变为可引用证据(store + DataEngine)。
- Pro 长任务(>30 步)历史出现视觉接地摘要;每个 agent 的输入面由 manifest +
ContextPolicy 两张表完整声明。
---
## 附录 A:agent → State 读/写实证清单(2026-08-31 工作区)
(visibility manifest 初始值;来源为本次全仓扫描。)
| 节点 | 读 | 写 |
|---|---|---|
| perception | structured_decisions | latest_screenshot, latest_ui_hierarchy, operator_raw_data, injected_instruction, user_stop_requested |
| planner | latest_screenshot, initial_goal | (无通道写;就地突变 latest_screenshot) |
| operator | operator_raw_data, latest_ui_hierarchy, indexed_elements, subagent_calls, current_step_id, initial_goal, check_feedback, short_term_memory, operator_tool_limit_exceeded, injected_instruction | structured_decisions, operator_raw_thinking, operator_native_thinking, short_term_memory, indexed_points, complete_subgoals_by_ids, current_step_id, subagent_calls, operator_tool_limit_exceeded |
| execution_check | operator_raw_data, structured_decisions, operator_*_thinking, user_stop_requested, initial_goal | checker_success, validator_messages, structured_decisions, current_step_id, check_feedback, subagent_calls |
| validator | structured_decisions, latest_screenshot, current_step_id, operator_raw_data, operator_*_thinking, indexed_points, indexed_elements | last_execution_result(+就地突变 latest_screenshot, indexed_*) |
| summarizer | current_step_id, structured_decisions, operator_*_thinking, last_execution_result | (无;异步写 DataEngine) |
| exit_settlement | user_stop_requested, initial_goal | exit_settlement_route, check_feedback, run_outcome |
| checker | (不读 State) | (不写 State;ctx.assert_halt + ledger) |
| diagnoser | initial_goal, check_feedback, latest_screenshot, latest_ui_hierarchy | (无) |
| outputter | initial_goal, operator_raw_data | (无) |
| explorer 工具 | operator_raw_data, latest_screenshot, latest_ui_hierarchy, indexed_* | (就地 append indexed_*) |
| failure_analyzer | latest_ui_hierarchy, initial_goal | (就地重推导 indexed_*) |
| committee 工具 | latest_screenshot, initial_goal | (无) |
| video/object_detector/log/history_analyzer | (不读或仅透传;object_detection 兜底读 latest_screenshot) | validator_messages(信封用法) |
## 附录 B:信息处置台账("不损伤"核销表)
### B.1 State 字段
| 字段 | 消费者(证据) | 处置 | 阶段 | 同步改动点 |
|---|---|---|---|---|
| initial_goal | 多节点读 | 保留 | — | — |
| injected_instruction | perception 写 / operator prompt 读 | 保留 | — | — |
| user_stop_requested | perception 写 / gate、checkpoints、plan 守卫读 | 保留 | — | — |
| checker_success | execution_check 写 / 两处 gate 读 | 保留 | — | — |
| check_feedback | graph.py:198,230,367 写 / prompts.py:243、diagnoser.py:178 读 | 更名 operator_feedback,扩容纳 planner 驳回 | P0(接入)+P1(更名) | 写 3 处、读 2 处、组件提示语 |
| run_outcome | graph.py:367 写 / sdk/agent.py:720 读 | 保留;ctx 侧动态挂载显式化 | P3 | sdk/agent.py:721 |
| exit_settlement_route | exit_settlement 写 / gate 读 | 保留 | — | — |
| current_step_id / structured_decisions / operator_*_thinking / last_execution_result / subagent_calls / operator_tool_limit_exceeded | 活跃读写对(附录 A) | 保留 | — | — |
| short_term_memory | operator.py:743-765 写 / prompts.py:331 读 | **保留**;移除权属姊妹文档 §3.3 | — | — |
| messages | 写:无(Pro);读:sdk/agent.py:1182(空转)、:1321(空转);graph_runner.py:57 写(坏路径) | 删除 | P1 | 读 2 处、写 1 处、_get_graph_state |
| validator_messages | 14 工具信封写 + graph.py:195 跨回合写;通道 0 读 | 信封→ToolOutcome;:195→operator_feedback;删通道 | P0(:195)+P1(信封) | 14 工具 + tool_wrapper:175 + operator:388 + graph:526,595 |
| remaining_steps | 仅 init(sdk:1201);上限实际由 recursion_limit 承载(sdk:674) | 删除 | P1 | _get_graph_state |
| focused_app_info / device_date | 仅 init(sdk:1197-1198) | 删除 | P1 | _get_graph_state |
| subgoal_plan / operator_tactical_plan | 仅 init / 零引用 | 删除 | P1 | _get_graph_state |
| complete_subgoals_by_ids | operator:767 写 []、init、0 读 | 删除 | P1 | operator:767、_get_graph_state |
| current_agent | scratchpad:166,260 等读(恒回退 "operator"),0 写 | 删除 | P1 | scratchpad 固定值或显式传参 |
| operator_replan_reason | committee_tool:203 读(恒 None),0 写 | 删除 | P1 | committee 去引用 |
| latest_ui_hierarchy | perception 写 / operator:614、diagnoser:193、FA:419、explorer:1595 读 | → snapshot.fused_elements | P2 | 4 读者切 store |
| latest_screenshot | perception/planner/validator/flash 写 / validator:98、diagnoser:186、committee:138、explorer:268、object_detection:93 读 | → snapshot + capture 收口 | P2 | §5.1 表全部触点 |
| operator_raw_data | perception 写 / operator:610、validator:618、explorer:290、outputter:139 读 | → PerceptionRef + store | P2 | 4 读者切 store;outputter 帧选择决策 |
| indexed_points / indexed_elements | 节点内写读(operator/validator/FA/explorer/action_executor,附录 A) | → TurnWorkspace(节点局部) | P2 | §5.3 全部触点 |
| (幽灵)ui_tree | 4 读 0 写 | P0 止血→latest_ui_hierarchy ✅;P2 终态 | P0/P2 | operator:995、tool_declarations:477、action_executor:401、exec_tools:548 |
| (更正)failure_context | engine/BaseAgent 栈自有协议,`.get()` 有 isinstance 防护 | 误报,不动 | — | — |
| (更正)task_goal | `ExecutionContextState.task_goal` 存在(core/state.py:48) | 误报,不动 | — | — |
| (幽灵)metadata | sdk/types/task.py:223 对 finalize 快照 dict.get(恒 None) | 无害;随 finalize 清理顺手删 | P1 | 1 处 |
### B.2 ArtemisContext 属性
| 属性 | 消费者 | 处置 | 阶段 |
|---|---|---|---|
| latest_perception_data | 0 读(HEAD 版 checker 曾读,工作区已删) | 删除写入 + 注释 | P0 |
| run_outcome(动态) | sdk:1107 读 → trace 后缀 | 显式字段 | P3 |
| _active_driver | drivers/factory:75-78、tools/base:104、mock:55、4 处测试 | → PrivateAttr | P0 |
| pending_checkpoints / checkpoint_tasks / checkpoint_attempt_seq / checkpoint_repairs / final_check_attempts / assert_halt | graph/checkpoints.py 族 | → CheckpointLedger | P3 |
| task_plan_content_before / last_validated_plan / pending_validated_plan / planner_task | graph.py:_process_plan_write / execution_check | → PlanGovernor | P3 |
| background_tasks / background_jobs / grace_period | ctx.__aexit__、summarizer:66、diagnoser:234 | → BackgroundTaskGroup | P3 |
| 其余声明字段 | 基础设施 | 保留(分组) | P3 |
| (engine 侧)ExecutionContext.artemis_context | graph_runner:48、reactive_runner:57 读,0 写 | 显式接线或显式报错 | P0 |