Introduce two-gate checker settlement, explicit graph visibility and checkpoints, structured LLM reliability, centralized action specifications, and corresponding tests and design documentation.
37 KiB
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. 设计原则
- 上下文是编译出来的,不是累积出来的。 Pro 已经如此;把它升格为正式模型, Flash 的消息列表只是同一份记忆的另一种渲染。
- 一份情景记忆,多种视图。 DataEngine 是唯一事实源;每个 agent 看到的是按其 角色声明的"最高价值密度视图"。agent 的智能上限由输入质量决定。
- 可见域靠声明与类型约束。
extra="forbid"拦截幽灵写与非法构造;幽灵读 (getattr带默认值不会因 forbid 报错)由节点 READS/WRITES manifest 的测试期 代理拦截。两者缺一不可。 - 重物不过通道。 截图 b64 不进 LangGraph 通道;通道里只放引用,重物放进有属主、 有生命周期的 store。
- 就地突变不消灭,而是正名。 P2 平面的全部用途经审计都是"节点内工具通信"—— 给它一个声明的、节点局部的载体(TurnWorkspace),而不是假装它不存在。
- 后台增强不可成为依赖。(继承姊妹文档 §4.6)摘要未就绪必有无损降级。
- 不损伤:删除必先核销。 每个字段/属性/通道的处置都对照附录 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 不再随astreamvalues 每超步复制)。- 消费者一律
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止血)。
- Operator:快照直取
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)一行调用; Flashrunner.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
- B1 止血:4 处
ui_tree→latest_ui_hierarchy(终态在 Phase 2 改快照)。✅ - B2:
graph.py驳回反馈改走现有check_feedback通道(条目带[planner]前缀,弃写无人读的 validator_messages),CheckFeedbackPromptComponent提示语按前缀中性化(标题保持 "Verification Findings" 不变,兼容既有断言)。✅ - B3:删
ctx.latest_perception_data写入与过期注释。✅ - B4 止血:operator update 补回
indexed_elements(消除两键不对称;终态在 Phase 2 收编进 workspace)。✅ - 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 通道侧);新增perceptionref 与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)往返(astreamvalues 流)在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 |