记忆、学习与派发验证 深度解析
拆解 agent-chief 如何把偏好变成记忆,把可执行任务变成可验证结果。
导入链与契约
这个子页面聚焦 memory learning。关键证据包括 memory/store.py:20;memory/store.py:34;core/schema.py:68;dispatch/acceptance.py:41;dispatch/acceptance.py:68;core/brain.py:299。这些引用不是源码索引,而是帮助定位边界:入口在哪里、数据对象在哪里、失败时由谁兜底。
▸MemoryStore
15class MemoryStore: 16 def __init__(self, state: State, embedder: Embedder = DEFAULT_EMBEDDER): 17 self.state = state 18 self.embedder = embedder 19 20 async def curate( 21 self, text: str, *, topic: str, origin_event_id: str | None, now: datetime 22 ) -> MemoryItem: 23 item = MemoryItem( 24 id=f"mem_{now:%Y%m%d}_{secrets.token_hex(3)}", 25 origin_event_id=origin_event_id, 26 text=text, 27 topic=topic, 28 embedding=self.embedder.embed(text), 29 created_at=now, 30 ) 31 await self.state.save_memory(item) 32 return item 33 34 async def associate(self, text: str, *, now: datetime) -> list[MemoryItem]: 35 """Top-3 active memories with cosine > 0.78; updates hit stats (SPEC §4.2).""" 36 query = self.embedder.embed(text) 37 scored = [ 38 (cosine(query, item.embedding), item) 39 for item in await self.state.list_memory() 40 ] 41 hits = sorted( 42 (pair for pair in scored if pair[0] > ASSOCIATION_THRESHOLD), 43 key=lambda pair: pair[0], 44 reverse=True, 45 )[:TOP_K] 46 results = [] 47 for _sim, item in hits: 48 item.hit_count += 1 49 item.last_hit_at = now 50 await self.state.save_memory(item) 51 results.append(item) 52 return results 53 54 async def expire(self, *, now: datetime) -> int: 55 """Move items past their ttl_days into the archive table (SPEC §3).""" 56 moved = 0 57 for item in await self.state.list_memory(): 58 if now - item.created_at > timedelta(days=item.ttl_days): 59 await self.state.save_memory(item, archive=True) 60 await self.state.delete_memory(item.id) 61 moved += 1 62 return moved
核心实现,逐段拆
▸Task 契约
Task 模型在 core/schema.py:68 到 core/schema.py:78 定义 goal、executor、acceptance、acceptance_cmd、status 和 attempts。dispatch 的可验证性从这个数据结构开始。
68class Task(BaseModel): 69 id: str 70 origin_event_id: str 71 goal: str 72 executor: Executor 73 acceptance: str 74 acceptance_cmd: str | None = None 75 status: Literal["pending", "running", "done", "failed", "rejected"] = "pending" 76 result_summary: str | None = None 77 attempts: int = 0 78
▸acceptance 验证
dispatch_and_verify 在 dispatch/acceptance.py:57 到 dispatch/acceptance.py:88 最多运行两次,仍失败就 rejected 并请求人类接手。这个路径让自动化有刹车。
30async def verify( 31 task: Task, *, ask: AskFn | None = None, exec_fn: Callable = _default_exec 32) -> VerifyResult: 33 """Check a completed task against its acceptance criteria.""" 34 if task.acceptance_cmd: 35 argv = shlex.split(task.acceptance_cmd) 36 code, _out, err = await exec_fn(argv, None) 37 if code == 0: 38 return VerifyResult(True, "acceptance_cmd exit 0") 39 return VerifyResult(False, f"acceptance_cmd exit {code}: {err.strip()[:120]}") 40 41 if ask is None: 42 # No verifier available: never bless unverified work. 43 return VerifyResult(False, "no verifier configured; refusing to trust the claim") 44 45 prompt = verify_prompt( 46 acceptance=task.acceptance, result=task.result_summary or "(no result)" 47 ) 48 raw = await ask(prompt) 49 try: 50 data = json.loads(extract_json(raw)) 51 return VerifyResult(data.get("verdict") == "pass", data.get("reason", "")) 52 except Exception: 53 logger.warning("verifier returned garbage; treating as fail: %r", raw[:80]) 54 return VerifyResult(False, "verifier output unparseable") 55 56 57async def dispatch_and_verify( 58 state: State, 59 task: Task, 60 executor: Executor, 61 *, 62 ask: AskFn | None = None, 63 exec_fn: Callable = _default_exec, 64) -> tuple[Task, str | None]: 65 """Run + verify with one retry (attempts max 2, SPEC §3); on final failure 66 mark the task rejected and return a message asking the human.""" 67 last_reason = "" 68 while task.attempts < 2: 69 task = await run_task(state, task, executor) 70 if task.status == "done": 71 result = await verify(task, ask=ask, exec_fn=exec_fn) 72 if result.passed: 73 return task, None 74 last_reason = result.reason 75 task.status = "failed" 76 await state.save_task(task) 77 else: 78 last_reason = task.result_summary or "executor failed" 79 80 task.status = "rejected" 81 await state.save_task(task) 82 ask_human = ( 83 f"I tried twice and couldn't finish this myself: {task.goal}\n" 84 f"Last attempt: {task.result_summary}\n" 85 f"Why it didn't pass: {last_reason}\n" 86 f"Can you take a look?" 87 ) 88 return task, ask_human
关键流程
流程上,memory learning 并不是一个孤立函数。它会和 memory/store.py:20、memory/store.py:34、core/schema.py:68 共同形成一个闭环:先产生可解释中间状态,再把 route、score、cost 或 rejected reason 存回系统。
边界 / 失败模式
▸失败不是异常,而是产品路径
边界处理在源码里是显式产品逻辑:dispatch/acceptance.py:41 负责保守降级,dispatch/acceptance.py:68 负责避免重复或噪声,core/brain.py:299 负责留下可追踪状态。这让 agent-chief 的可信度来自失败路径,而不是只来自 happy path。
- judge 不可用时不得盲目 interrupt。
- 历史相似事件只能在阈值内复用,不应跳过新奇事件。
- 派发结果必须验证,不能信任“done”的自述。