Introduce two-gate checker settlement, explicit graph visibility and checkpoints, structured LLM reliability, centralized action specifications, and corresponding tests and design documentation.
587 lines
37 KiB
Markdown
587 lines
37 KiB
Markdown
# 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 |
|