Files
Artemis-Eapil/docs/plans/context-visibility-redesign.md
T
somew1nd cbe8f4f0c6 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.
2026-08-31 20:42:24 -07:00

37 KiB
Raw Blame History

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"

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 可见域:节点声明读/写集,测试期强制

每个节点类声明清单,运行期零开销,测试期强制:

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":

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 数据模型

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 之所以没被发现,正因为如此。

因此它们不属于冻结快照,也不属于图通道,属于节点局部工作区:

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:

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 分离),仅在运行时层共享:

"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