Agent Bus:把多 Agent 协作写成可验证的任务契约
从任务字段到结果验收,定义每个 Agent 能读什么、能做什么,以及服务端怎样确认工作完成。
- 身份来自可信入口,正文不能声明权限
- 事件只提示任务编号,执行前回读权威记录
- 领取与提交检查版本 状态 目标主体和截止时间
- 结果同时通过结构检查与任务验收
- 同任务同提交键同内容返回原回执
1. 让协作有可以重新读取的依据
协调器把检查工作交给另一个 Agent,随后关闭窗口。过一会儿回来,它需要知道任务有没有被领取,结果是否通过验收,以及取消请求有没有生效。聊天里的一次 @ 提及能够表达协作意图,这些状态仍需要一个明确的存储位置。本文把 Agent Bus 定义为应用层任务服务:每份工作有稳定身份、接收者、状态机和受保护的结果入口。
一个契约需要同时回答四个问题:谁有资格提出这份工作,执行者怎样取得当前版本,什么证据足以结束任务,争议发生后到哪里查询记录。将这些答案写进服务接口之后,协调器才能在断开会话后继续判断进度。任务标题可以给人阅读,实际执行仍由受限的类型和参数决定,避免把任意自然语言直接变成工具调用。
下面用两段公开的英文短句做词数统计。协调器分别派给 worker-a 与 worker-b,两个确定性处理器提交词数和原文哈希,服务端再次计算后验收。示例只使用 Python 标准库和 SQLite,不调用模型、网络、真实 Agent 或生产工具,也不需要密钥。它演示可检查的契约,不能直接充当安全生产服务。
任务存储与唤醒方式可以分别选择。轮询、队列通知或事件回调,都可以提示 worker 检查新工作;任务的归属和状态始终由服务端记录决定。本文的字段、权限和函数名称属于示例系统设计,并非 MCP 或所有多 Agent 产品共同采用的标准。
2. 先确定任务字段的含义
task_id 由服务端生成,后续领取、取消与提交都引用它。from 由已认证主体填入;创建请求不得自行提供该字段。to 指向允许名单中的接收者,type 绑定处理器和验收规则。正文只保存 data.fixture_id,避免把整段会话、凭据或无关文件一并复制给每个 worker。生产中的工件引用还应绑定可访问范围和内容版本。
这里对创建请求采用封闭字段集合,多一个 from 字段也会拒绝。这样的限制适合教学,能及时暴露调用方对契约的误解。生产需要提前定义版本演进规则:新增可选字段是否允许,旧 worker 会怎样处理未知任务类型,结果模式升级后旧回执是否仍可解释。破坏兼容的改动应使用新的类型或模式版本,不能沿用旧名称却悄悄改变含义。
correlation_id 把同一业务流程中的多个任务关联起来,用于查询和排障,不参与去重。revision 是任务状态版本,从一开始,每次成功变更递增。读取到版本二的 worker,提交时必须仍然面对版本二。别人已经取消任务或完成提交,旧请求就会发生版本冲突。
result 保存验收通过的结构化产物;error 保存受控错误码;cancel 记录取消主体和服务端时间。expires_at 是固定绝对截止时间,重试不会顺延。submission_id 单独保存在结果回执表,用于识别同一任务的重复提交。任务关联、状态版本和提交去重各有独立用途,调用方不能互相替代。
3. 把可执行动作写成权限清单
本例只有 coordinator 能创建 count_words 任务,目标限于两个 worker。创建者可读取、查看审计及取消自己创建的任务。目标 worker 可读取任务、查看该任务审计、查询自己的待办队列,并执行 claim、submit、fail;它不能取消、转派、创建新任务或读取另一个 worker 的任务。clock 仅可把已经到期的开放任务标记为 expired,没有通用读取接口。
读写分离也体现在接口形状上。get 返回独立解析的对象,调用者修改这个副本不会改动数据库;queue 只展示候选工作,不带隐式领取;audit 先走同一条对象级读取检查。写入口各自指定动作,没有通用的任意字段更新方法。这样可以独立测试读取泄露、越权动作和非法状态变更,不需要把所有行为塞进一个万能消息接口。
授权检查在每个服务方法内部执行。猜到 task_id 不会获得访问权;结果重放也先检查调用者是否为目标 worker,再查询旧回执。OWASP Authorization Cheat Sheet提出默认拒绝与每次请求检查权限。本例据此写出动作与资源归属条件,未实现完整的租户管理、权限撤销传播或网关。
FixtureAuth 只为离线测试预先创建几个 Actor 实例,服务按对象身份识别这些实例。任何人都能在同一 Python 进程里访问演示对象,因此它没有密码学认证,也无法隔离进程内恶意代码。真实入口必须验证会话或访问令牌,再从可信中间件注入主体;把 body.from 或请求传入的名字直接构造成 Actor,会破坏这里的信任边界。
总线接受任务不代表用户允许任意副作用。本文没有发布、付款、邮件或 shell 动作。实际添加这些能力时,要逐项记录获准动作、资源、接收者、数据范围和有效期,并在执行前再次检查。一个只读审查任务不能借结果文字申请更高权限;新增动作默认拒绝,等待独立授权。
4. 事件到达后先回读任务
Worker.on_event 只从事件取 task_id,然后用自己的会话调用 get。接收者、输入、状态及 revision 都来自返回的任务记录。事件中即使附带新的 instruction、result 或 to,也不会覆盖数据库。测试故意把另一段文本和任意命令塞进事件,最终得到的仍是原任务的五词结果。
真实事件入口还要验证来源、控制请求体大小和触发频率,防止伪造通知反复消耗查询及计算资源。回读策略只限制事件正文影响任务的方式,无法代替事件传输自身的认证。即使通知带有正确签名,签名也只证明来源和完整性,任务权限仍须由服务方法判定。这个实验没有对外监听入口,因此没有实现上述网络防护。
这一步同时处理了通知重复和延迟。已经结束的任务直接返回当前状态;过期但尚未清理的任务会在 claim 阶段被拒绝。通知丢失时,获准的 worker 可以查询自己的 queue,再按相同步骤执行。queue 是候选列表,拿到列表并未获得任务所有权。
截至 2026 年 10 月 5 日,MCP 官方组织的 Events Design Sketch页面仍标注 Draft proposal,列出了多种投递方式,并将事件载荷视为不可信数据。该草案可帮助设计事件适配器,不能证明任意 Grok、Muse 或其他宿主都能在后台自动启动下一轮执行。本文完全离线,不验证订阅、签名、回调、后台唤醒或宿主兼容性。
5. 领取与写入使用同一组条件
队列先列出派给当前 worker、尚未过期的 queued 任务。claim 在 BEGIN IMMEDIATE 事务内重新读取记录,核对目标主体、revision、queued 状态与截止时间,全部成立才写成 running。两个连接即使先读到相同版本,后进入写事务的一方也必须面对已经变化的状态。示例的双连接测试验证旧版本领取被拒绝。
本例把读取、比较和更新放在同一个 SQLite 写事务中,所以版本检查与修改之间不会被另一个写事务插入。在远程数据库中,应保留等价的原子条件更新,例如同时约束任务编号、状态、版本和拥有者,再检查受影响行数。单独 GET 一次后无条件 UPDATE,会重新打开竞争窗口。
领取冲突应让调用方重新读取,再决定停止或处理另一项任务,不能在后台不断换版本重试。演示中每个任务直接指定一个 worker 主体,同一主体的多个进程仍共享这一身份;数据库只允许一次 queued 到 running 的迁移,没有进一步分配进程级所有权。需要同主体接管、续租或长任务恢复时,应加入单独的执行尝试与 fencing 设计。
如果将此接口包装为 HTTP,可以把任务版本映射为强 ETag,并要求变更请求携带 If-Match。RFC 9110 第 13.1.1 节解释了这种条件请求对丢失更新的防护。本文没有实现 HTTP;本地 revision 参数只演示同类前置条件,仍需鉴权、状态机和业务验收。
允许的迁移很有限:queued 经 claim 进入 running;running 可进入 succeeded 或 failed;两个开放状态都可被取消或标记过期。终态不能重新领取。执行器崩溃后的租约回收、fencing、重试预算与 outbox,见 多 Agent 消息协作与重启恢复。本例不提供领取后自动恢复,进程中断留下的 running 会等待取消或过期。
6. 结果通过两层验收才提交
词数任务的结果必须恰好包含 fixture_id、words、sha256。服务先检查字段集合和精确类型,拒绝字符串、额外字段,以及 Python 中容易混入整数判断的布尔值。随后依据任务保存的 fixture_id 重新计算词数和文本哈希,比较实际值。结构正确但统计错误的结果仍会被拒绝,任务保持 running,revision 不变。
回执中的响应保存了当时确认的完整任务快照。相同提交重试时,服务可以直接返回这份已提交事实,故意不要求调用方重新取得最新版本;这条路径只对已经保存且内容完全匹配的回执成立。第一次提交仍要检查截止时间和 running 状态。将这两条路径分开,可以避免完成响应丢失后被过期检查误判为一次从未成功的操作。
真实任务的验收需要按任务类型编写。代码任务可以要求明确的提交版本、测试命令和退出状态;文件转换可检查文件是否存在、大小、格式与内容摘要。模型说已完成只能作为待验证材料。难以机械判断的结论可转交只读审查环节,审查通过后再由受控入口记录最终状态。
首次成功提交把任务终态、result、receipt 和成功审计放进同一个事务。响应丢失时,worker 复用同一个 submission_id 和相同内容重试,服务返回原回执,revision 不再增加。同键换内容会报 idempotency_conflict;换一个键重复完成已经结束的任务,也会被状态机拒绝。返回旧回执前仍要鉴权。
幂等范围限定为同一数据库中同一 task_id 与 submission_id 的保留期。示例不删除回执;数据库丢失或生产清理过早都会破坏这一保证。创建任务本身没有幂等键,因此创建响应不确定时不能盲目重试。fail 与 cancel 也没有结果回执机制,重复调用应先 get 确认当前状态。外部工具副作用另需自己的去重或对账机制。
submission_id 还必须能按 UTF-8 编码。Python 字符串和 JSON 解码结果可能包含孤立代理码点:它们能通过字符串与长度检查,却无法绑定为 SQLite TEXT 参数。示例在查询回执前将这类值拒绝为 bad_submission_id,保留任务和既有回执,并记录拒绝审计。长度上限仍按 Python 字符串字符数计算,中文与正确解码的 emoji 均可作为提交键;这里不做 Unicode 规范化,也没有新增 HTTP 解析器或生产输入网关。
7. 取消 过期与审计要能解释先后
取消与提交竞争时,以数据库写事务的实际提交顺序为准。取消先落盘,revision 递增,旧结果会被拒绝;结果先落盘,取消不能覆盖 succeeded。固定截止时间还会在每次领取和首次提交时检查,即使 clock 尚未清理到期任务,也不会继续接受工作。get 保持只读,可能读到状态仍为 queued、时间已经到期的记录,调用者要结合 expires_at 判断。
错误也要区分阶段。bad_result_schema 表示格式被拒绝,result_verification_failed 表示内容未通过验收,它们都没有把任务写成失败终态。只有目标 worker 明确调用 fail 并提交允许的错误结构,任务才成为 failed。数据库异常则回滚本次事务并向调用方暴露异常,不能把异常捕获后拼成一段成功文字。实际服务应记录关联编号,并避免把堆栈和敏感数据直接返回给模型。
clock.expire 只改变开放且到期的任务,重复清理不会增加终态版本。取消记录说明服务拒绝后续提交,不保证 worker 的计算线程瞬间停止,更不能撤销已经发送的邮件或完成的付款。本例没有接入任何外部写入,因此这类中断和补偿能力需要另外验证。
审计记录任务编号、真实主体、动作、结论、版本和服务端时间,不复制完整输入或结果正文。成功变更与任务一起提交;已认证主体的拒绝写入在回滚后记录。认证入口拒绝和只读访问日志未在示例中实现,生产还应补上速率限制、日志脱敏、防篡改存储与留存策略。SQLite 文件本身的读写权限也属于部署边界。
8. 运行实验并观察留下的记录
将本页三个完整文件分别保存为 task_bus.py、demo.py、test_task_bus.py,放在同一目录,使用 Python 3.10 或更新版本运行下列命令。测试为每个案例建立临时 SQLite 文件,演示使用独立临时目录,退出后清理;重开数据库测试会在清理前验证任务和回执仍然存在。两个模拟 worker 由脚本依次调用,不伪装成真实后台进程。
2026 年 10 月 20 日复核时,实际运行了三十三项离线测试,普通模式与 Python -O 模式均通过。覆盖项包括身份伪造、非目标读取、动作越权、版本冲突、非法状态迁移、相同和冲突结果重放、取消、过期、结构及业务验收、不信事件正文、只读返回副本和数据库重开。另用 SQLite 触发器注入 receipt 写入失败,确认任务、结果与成功审计整体回滚。新增用例检查孤立代理码点及其 JSON 转义输入、有效的中文与 emoji 提交键、字符数边界,以及无效新键不会影响已有回执。旧实现会在六个测试场景中抛出 UnicodeEncodeError;补上提交键校验后,调用方收到 bad_submission_id,任务状态和回执保持不变。两模拟 worker 的演示输出未变。测试通过的范围止于这些离线契约。
在接入真实宿主前,应拿一份公开小任务逐项核对:认证主体从哪里来,队列是否只返回获准对象,哪个动作启动执行,结果由谁验收,取消后旧提交怎样处理,以及通知缺失时如何发现未完成任务。本文未进行浏览器 GUI、生产认证、真实事件投递或宿主后台唤醒验收。七张配图沿用此前的渲染与逐张检查记录,本次未重新做浏览器目视验收。完整源代码均在下方可展开阅读。
python3 demo.py
python3 -m unittest -v
python3 -O -m unittest -v实际演示输出:
worker-a succeeded revision 3 words 5
audit: [('coordinator', 'create', 'ok', 1), ('worker-a', 'claim', 'ok', 2), ('worker-a', 'submit', 'ok', 3)]
worker-b succeeded revision 3 words 7
audit: [('coordinator', 'create', 'ok', 1), ('worker-b', 'claim', 'ok', 2), ('worker-b', 'submit', 'ok', 3)]
task_bus.py 完整源码
"""Offline contract exercise. Fixture sessions are NOT production authentication."""
from contextlib import contextmanager
from dataclasses import dataclass
import hashlib
import json
import sqlite3
import time
import uuid
FIXTURES = {
"public-a": "Reliable tasks need explicit contracts.",
"public-b": "Read the task before doing the work.",
}
WORKERS = {"worker-a", "worker-b"}
OPEN = {"queued", "running"}
class BusError(Exception):
pass
def require(condition, code):
if not condition:
raise BusError(code)
def canonical(value):
return json.dumps(value, sort_keys=True, separators=(",", ":"))
def digest(value):
return hashlib.sha256(canonical(value).encode()).hexdigest()
def answer(fixture_id):
text = FIXTURES[fixture_id]
return {"fixture_id": fixture_id, "words": len(text.split()),
"sha256": hashlib.sha256(text.encode()).hexdigest()}
@dataclass(frozen=True)
class Actor:
name: str
class FixtureAuth:
"""Trusted test harness only; do not expose this object as a login API."""
def __init__(self):
self.sessions = {name: Actor(name) for name in
["coordinator", "other", "clock", *sorted(WORKERS)]}
def identify(self, session):
for name, known in self.sessions.items():
if session is known:
return name
raise BusError("unauthenticated")
class Bus:
def __init__(self, path, auth, now=time.time):
self.auth, self.now = auth, now
self.db = sqlite3.connect(path, isolation_level=None, timeout=5)
self.db.executescript("""
CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY, body TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS receipts (
task_id TEXT, submission_id TEXT, hash TEXT, response TEXT,
PRIMARY KEY (task_id, submission_id));
CREATE TABLE IF NOT EXISTS audit (
seq INTEGER PRIMARY KEY, task_id TEXT, actor TEXT,
action TEXT, outcome TEXT, revision INTEGER, at REAL);
""")
def close(self):
self.db.close()
def _log(self, task_id, actor, action, outcome, revision=None):
self.db.execute("INSERT INTO audit VALUES(NULL,?,?,?,?,?,?)",
(task_id, actor, action, outcome, revision, self.now()))
@contextmanager
def _write(self, session, action, task_id=""):
actor = self.auth.identify(session)
self.db.execute("BEGIN IMMEDIATE")
try:
yield actor
self.db.commit()
except Exception as exc:
self.db.rollback()
if isinstance(exc, BusError):
self._log(task_id, actor, action, str(exc))
raise
def _load(self, task_id):
row = self.db.execute("SELECT body FROM tasks WHERE id=?",
(task_id,)).fetchone()
require(row is not None, "not_found")
return json.loads(row[0])
def _readable(self, task, actor):
require(actor in {task["from"], task["to"]}, "forbidden")
def _open(self, task, revision, states):
require(type(revision) is int, "bad_revision")
require(task["revision"] == revision, "version_conflict")
require(task["status"] in states, "illegal_state")
require(self.now() < task["expires_at"], "expired")
def _save(self, task, actor, action):
task["revision"] += 1
self.db.execute("UPDATE tasks SET body=? WHERE id=?",
(canonical(task), task["task_id"]))
self._log(task["task_id"], actor, action, "ok", task["revision"])
return task
def create(self, session, body):
with self._write(session, "create") as actor:
require(actor == "coordinator", "forbidden")
keys = {"to", "type", "correlation_id", "data", "expires_at"}
require(type(body) is dict and set(body) == keys, "bad_envelope")
require(type(body["to"]) is str and body["to"] in WORKERS,
"bad_target")
require(body["type"] == "count_words", "bad_type")
correlation = body["correlation_id"]
require(type(correlation) is str and 0 < len(correlation) <= 80,
"bad_correlation")
data = body["data"]
require(type(data) is dict and set(data) == {"fixture_id"},
"bad_data")
require(type(data["fixture_id"]) is str, "bad_fixture")
require(data["fixture_id"] in FIXTURES, "bad_fixture")
expiry = body["expires_at"]
require(type(expiry) in {int, float}, "bad_expiry")
require(self.now() < expiry <= self.now() + 3600, "bad_expiry")
task = json.loads(canonical(body))
task.update(task_id=str(uuid.uuid4()), **{"from": actor},
status="queued", revision=1, result=None,
error=None, cancel=None)
self.db.execute("INSERT INTO tasks VALUES(?,?)",
(task["task_id"], canonical(task)))
self._log(task["task_id"], actor, "create", "ok", 1)
return task
def get(self, session, task_id):
actor = self.auth.identify(session)
task = self._load(task_id)
self._readable(task, actor)
return task
def queue(self, session):
actor = self.auth.identify(session)
require(actor in WORKERS, "forbidden")
tasks = [json.loads(row[0]) for row in
self.db.execute("SELECT body FROM tasks ORDER BY rowid")]
return [t for t in tasks if t["to"] == actor
and t["status"] == "queued" and self.now() < t["expires_at"]]
def claim(self, session, task_id, revision):
with self._write(session, "claim", task_id) as actor:
task = self._load(task_id)
require(actor == task["to"], "forbidden")
self._open(task, revision, {"queued"})
task["status"] = "running"
return self._save(task, actor, "claim")
def submit(self, session, task_id, revision, submission_id, result):
with self._write(session, "submit", task_id) as actor:
task = self._load(task_id)
require(actor == task["to"], "forbidden")
require(type(submission_id) is str
and 0 < len(submission_id) <= 80, "bad_submission_id")
try:
submission_id.encode("utf-8")
except UnicodeEncodeError:
raise BusError("bad_submission_id") from None
require(type(result) is dict, "bad_result_schema")
require(set(result) == {"fixture_id", "words", "sha256"},
"bad_result_schema")
require(type(result["fixture_id"]) is str
and type(result["words"]) is int
and type(result["sha256"]) is str, "bad_result_schema")
fingerprint = digest(result)
receipt = self.db.execute(
"SELECT hash,response FROM receipts "
"WHERE task_id=? AND submission_id=?",
(task_id, submission_id)).fetchone()
if receipt:
require(receipt[0] == fingerprint, "idempotency_conflict")
self._log(task_id, actor, "submit", "replayed",
task["revision"])
return json.loads(receipt[1])
self._open(task, revision, {"running"})
require(result == answer(task["data"]["fixture_id"]),
"result_verification_failed")
task["status"], task["result"] = "succeeded", result
self._save(task, actor, "submit")
self.db.execute("INSERT INTO receipts VALUES(?,?,?,?)",
(task_id, submission_id, fingerprint,
canonical(task)))
return task
def fail(self, session, task_id, revision, error):
with self._write(session, "fail", task_id) as actor:
task = self._load(task_id)
require(actor == task["to"], "forbidden")
self._open(task, revision, {"running"})
require(type(error) is dict
and error == {"code": "worker_failed", "retryable": False}
and type(error.get("retryable")) is bool, "bad_error")
task["status"], task["error"] = "failed", error
return self._save(task, actor, "fail")
def cancel(self, session, task_id, revision):
with self._write(session, "cancel", task_id) as actor:
task = self._load(task_id)
require(actor == task["from"], "forbidden")
self._open(task, revision, OPEN)
task["status"] = "cancelled"
task["cancel"] = {"by": actor, "at": self.now()}
return self._save(task, actor, "cancel")
def expire(self, session):
with self._write(session, "expire") as actor:
require(actor == "clock", "forbidden")
changed = []
rows = self.db.execute("SELECT body FROM tasks").fetchall()
for row in rows:
task = json.loads(row[0])
if task["status"] in OPEN and self.now() >= task["expires_at"]:
task["status"] = "expired"
task["error"] = {"code": "deadline_elapsed",
"retryable": False}
changed.append(self._save(task, actor, "expire"))
return changed
def audit(self, session, task_id):
self.get(session, task_id)
return self.db.execute(
"SELECT actor,action,outcome,revision FROM audit "
"WHERE task_id=? ORDER BY seq", (task_id,)).fetchall()
class Worker:
def __init__(self, bus, session):
self.bus, self.session = bus, session
def on_event(self, event):
# Event content is only a hint. No instruction or result is executed.
require(type(event) is dict and type(event.get("task_id")) is str,
"bad_event")
task = self.bus.get(self.session, event["task_id"])
if task["status"] != "queued":
return task
task = self.bus.claim(self.session, task["task_id"], task["revision"])
result = answer(task["data"]["fixture_id"])
return self.bus.submit(self.session, task["task_id"], task["revision"],
"result-" + task["task_id"], result)
def poll_once(self):
return [self.on_event({"task_id": t["task_id"]})
for t in self.bus.queue(self.session)]
demo.py 完整源码
"""No model, network, credentials, or external agent calls."""
from pathlib import Path
from tempfile import TemporaryDirectory
from task_bus import Bus, FixtureAuth, Worker
def main():
with TemporaryDirectory() as tmp:
auth = FixtureAuth()
bus = Bus(Path(tmp) / "tasks.sqlite", auth, now=lambda: 1000)
coordinator = auth.sessions["coordinator"]
for target, fixture in [("worker-a", "public-a"),
("worker-b", "public-b")]:
bus.create(coordinator, {
"to": target, "type": "count_words",
"correlation_id": "public-demo", "data": {"fixture_id": fixture},
"expires_at": 1060,
})
for name in ["worker-a", "worker-b"]:
worker = Worker(bus, auth.sessions[name])
for result in worker.poll_once():
print(name, result["status"], "revision", result["revision"],
"words", result["result"]["words"])
print("audit:", bus.audit(coordinator, result["task_id"]))
bus.close()
if __name__ == "__main__":
main()
test_task_bus.py 完整源码
from pathlib import Path
from tempfile import TemporaryDirectory
import unittest
import json
from task_bus import Actor, Bus, BusError, FixtureAuth, Worker, answer
class ContractTests(unittest.TestCase):
def setUp(self):
self.tmp = TemporaryDirectory()
self.auth = FixtureAuth()
self.now = 1000
self.path = Path(self.tmp.name) / "tasks.sqlite"
self.bus = Bus(self.path, self.auth, lambda: self.now)
self.c = self.auth.sessions["coordinator"]
self.a = self.auth.sessions["worker-a"]
self.b = self.auth.sessions["worker-b"]
def tearDown(self):
self.bus.close()
self.tmp.cleanup()
def body(self, target="worker-a"):
return {"to": target, "type": "count_words", "correlation_id": "test",
"data": {"fixture_id": "public-a"}, "expires_at": 1060}
def create(self):
return self.bus.create(self.c, self.body())
def running(self):
task = self.create()
return self.bus.claim(self.a, task["task_id"], 1)
def rejected(self, code, fn, *args):
with self.assertRaisesRegex(BusError, "^" + code + "$"):
fn(*args)
def test_forged_actor(self):
self.rejected("unauthenticated", self.bus.create,
Actor("coordinator"), self.body())
def test_body_from_rejected(self):
body = self.body()
body["from"] = "coordinator"
self.rejected("bad_envelope", self.bus.create, self.c, body)
def test_bad_target_types(self):
for target in [[], {}, None, 42]:
self.rejected("bad_target", self.bus.create,
self.c, self.body(target))
def test_worker_cannot_delegate(self):
self.rejected("forbidden", self.bus.create, self.a, self.body())
def test_wrong_agent_cannot_read_or_audit(self):
task = self.create()
for operation in [self.bus.get, self.bus.audit]:
self.rejected("forbidden", operation, self.b, task["task_id"])
def test_queue_is_scoped(self):
self.create()
self.assertEqual(self.bus.queue(self.b), [])
self.assertEqual(len(self.bus.queue(self.a)), 1)
def test_wrong_agent_cannot_claim(self):
task = self.create()
self.rejected("forbidden", self.bus.claim, self.b, task["task_id"], 1)
def test_success_without_claim_rejected(self):
task = self.create()
self.rejected("illegal_state", self.bus.submit, self.a,
task["task_id"], 1, "r1", answer("public-a"))
def test_version_conflict(self):
task = self.running()
self.rejected("version_conflict", self.bus.submit, self.a,
task["task_id"], 1, "r1", answer("public-a"))
def test_only_one_claim_across_connections(self):
task = self.create()
other = Bus(self.path, self.auth, lambda: self.now)
try:
self.bus.claim(self.a, task["task_id"], 1)
self.rejected("version_conflict", other.claim,
self.a, task["task_id"], 1)
finally:
other.close()
def test_result_replay(self):
task = self.running()
args = (self.a, task["task_id"], 2, "r1", answer("public-a"))
first = self.bus.submit(*args)
self.assertEqual(self.bus.submit(*args), first)
self.assertEqual(first["revision"], 3)
def test_submission_id_rejects_surrogates(self):
task = self.running()
keys = ["\ud800", "\udfff", "receipt-\ud800-x", "\ud83d\ude00",
json.loads('"\\ud800"')]
for key in keys:
with self.subTest(key=ascii(key)):
self.rejected("bad_submission_id", self.bus.submit,
self.a, task["task_id"], 2, key,
answer("public-a"))
self.assertEqual(self.bus.get(self.a, task["task_id"]), task)
count = self.bus.db.execute(
"SELECT count(*) FROM receipts").fetchone()[0]
self.assertEqual(count, 0)
self.assertFalse(self.bus.db.in_transaction)
self.assertEqual(self.bus.audit(self.c, task["task_id"])[-1],
("worker-a", "submit", "bad_submission_id",
None))
def test_unicode_submission_id_replay(self):
task = self.running()
key = "交付-" + json.loads('"\\ud83d\\ude00"')
args = (self.a, task["task_id"], 2, key, answer("public-a"))
first = self.bus.submit(*args)
self.assertEqual(first["status"], "succeeded")
self.assertEqual(self.bus.submit(*args), first)
self.assertEqual(self.bus.db.execute(
"SELECT submission_id FROM receipts").fetchone()[0], key)
def test_invalid_submission_id_preserves_existing_receipt(self):
task = self.running()
args = (self.a, task["task_id"], 2, "valid-key", answer("public-a"))
first = self.bus.submit(*args)
self.rejected("bad_submission_id", self.bus.submit,
self.a, task["task_id"], 2, "\ud800", answer("public-a"))
self.assertEqual(self.bus.get(self.a, task["task_id"]), first)
self.assertEqual(self.bus.submit(*args), first)
self.assertEqual(self.bus.db.execute(
"SELECT count(*) FROM receipts").fetchone()[0], 1)
def test_submission_id_type_and_length_boundaries(self):
task = self.running()
for key in [None, True, 7, [], "", "x" * 81]:
with self.subTest(key=key):
self.rejected("bad_submission_id", self.bus.submit,
self.a, task["task_id"], 2, key,
answer("public-a"))
args = (self.a, task["task_id"], 2, "字" * 80, answer("public-a"))
self.assertEqual(self.bus.submit(*args)["status"], "succeeded")
def test_same_key_different_result(self):
task = self.running()
self.bus.submit(self.a, task["task_id"], 2, "r1", answer("public-a"))
changed = dict(answer("public-a"), words=99)
self.rejected("idempotency_conflict", self.bus.submit,
self.a, task["task_id"], 2, "r1", changed)
def test_second_submission_key_rejected(self):
task = self.running()
self.bus.submit(self.a, task["task_id"], 2, "r1", answer("public-a"))
self.rejected("illegal_state", self.bus.submit, self.a,
task["task_id"], 3, "r2", answer("public-a"))
def test_wrong_worker_cannot_replay_receipt(self):
task = self.running()
self.bus.submit(self.a, task["task_id"], 2, "r1", answer("public-a"))
self.rejected("forbidden", self.bus.submit, self.b,
task["task_id"], 2, "r1", answer("public-a"))
def test_string_is_not_success(self):
task = self.running()
self.rejected("bad_result_schema", self.bus.submit,
self.a, task["task_id"], 2, "r1", "done")
def test_boolean_is_not_word_count(self):
task = self.running()
result = dict(answer("public-a"), words=True)
self.rejected("bad_result_schema", self.bus.submit,
self.a, task["task_id"], 2, "r1", result)
def test_schema_valid_but_wrong_answer(self):
task = self.running()
result = dict(answer("public-a"), words=99)
self.rejected("result_verification_failed", self.bus.submit,
self.a, task["task_id"], 2, "r1", result)
self.assertEqual(self.bus.get(self.a, task["task_id"])["revision"], 2)
def test_cancel_running_rejects_late_result(self):
task = self.running()
self.bus.cancel(self.c, task["task_id"], 2)
self.rejected("version_conflict", self.bus.submit, self.a,
task["task_id"], 2, "r1", answer("public-a"))
self.rejected("illegal_state", self.bus.submit, self.a,
task["task_id"], 3, "r1", answer("public-a"))
def test_worker_cannot_cancel(self):
task = self.running()
self.rejected("forbidden", self.bus.cancel, self.a, task["task_id"], 2)
def test_expired_before_sweep_is_not_claimable(self):
task = self.create()
self.now = 1060
self.assertEqual(self.bus.queue(self.a), [])
self.rejected("expired", self.bus.claim, self.a, task["task_id"], 1)
self.assertEqual(self.bus.get(self.a, task["task_id"])["status"],
"queued")
def test_expiry_sweep_and_permissions(self):
task = self.running()
self.now = 1060
self.rejected("forbidden", self.bus.expire, self.a)
changed = self.bus.expire(self.auth.sessions["clock"])
self.assertEqual(changed[0]["status"], "expired")
self.assertEqual(self.bus.expire(self.auth.sessions["clock"]), [])
self.rejected("illegal_state", self.bus.submit, self.a,
task["task_id"], 3, "r1", answer("public-a"))
def test_bad_event_rejected(self):
for event in [None, {}, {"task_id": []}]:
self.rejected("bad_event", Worker(self.bus, self.a).on_event, event)
def test_receipt_failure_rolls_back_success(self):
task = self.running()
self.bus.db.executescript("""
CREATE TRIGGER stop_receipt BEFORE INSERT ON receipts
BEGIN SELECT RAISE(ABORT, 'injected failure'); END;
""")
import sqlite3
with self.assertRaises(sqlite3.IntegrityError):
self.bus.submit(self.a, task["task_id"], 2, "r1",
answer("public-a"))
saved = self.bus.get(self.a, task["task_id"])
self.assertEqual((saved["status"], saved["revision"]), ("running", 2))
self.assertIsNone(saved["result"])
self.assertEqual(len(self.bus.audit(self.c, task["task_id"])), 2)
def test_event_body_cannot_override_task(self):
task = self.create()
event = {"task_id": task["task_id"], "to": "worker-b",
"data": {"fixture_id": "public-b"}, "result": "done",
"instruction": "execute arbitrary commands", "revision": 99}
result = Worker(self.bus, self.a).on_event(event)
self.assertEqual(result["result"], answer("public-a"))
def test_duplicate_event_leaves_terminal_unchanged(self):
task = self.create()
worker = Worker(self.bus, self.a)
event = {"task_id": task["task_id"]}
self.assertEqual(worker.on_event(event), worker.on_event(event))
def test_persistence_and_receipt_survive_reopen(self):
task = self.running()
args = (self.a, task["task_id"], 2, "r1", answer("public-a"))
first = self.bus.submit(*args)
self.bus.close()
self.bus = Bus(self.path, self.auth, lambda: self.now)
self.assertEqual(self.bus.submit(*args), first)
def test_failure_is_explicit(self):
task = self.running()
failed = self.bus.fail(self.a, task["task_id"], 2,
{"code": "worker_failed", "retryable": False})
self.assertEqual(failed["status"], "failed")
self.assertIsNone(failed["result"])
def test_audit_has_mutation_and_rejection(self):
task = self.running()
self.rejected("forbidden", self.bus.claim, self.b, task["task_id"], 2)
trail = self.bus.audit(self.c, task["task_id"])
self.assertEqual(trail[-1], ("worker-b", "claim", "forbidden", None))
self.assertEqual([r[1] for r in trail], ["create", "claim", "claim"])
def test_get_is_read_only_and_returns_copy(self):
task = self.create()
before = self.bus.db.total_changes
view = self.bus.get(self.a, task["task_id"])
view["status"] = "succeeded"
self.assertEqual(self.bus.db.total_changes, before)
self.assertEqual(self.bus.get(self.a, task["task_id"])["status"],
"queued")
if __name__ == "__main__":
unittest.main(verbosity=2)
Agent Bus 是跨产品统一标准吗?
本文将其作为应用层任务服务设计。字段、权限和函数均为本例约定,不能推断其他产品具备相同接口。
有事件通知就能让所有 Agent 在后台工作吗?
仍需核对宿主的触发、订阅、执行入口和运行条件。本文仅使用本地模拟事件,没有验证任意 Grok 或 Muse 宿主的后台唤醒。
示例能用于生产认证吗?
不能。FixtureAuth 只注入离线测试主体,没有密码学认证,也不能隔离同一进程内的恶意代码。
重复提交为什么不会重复完成?
结果回执按 task_id 与 submission_id 存储内容指纹。同键同内容返回原回执,冲突内容和第二个键对终态的提交被拒绝。保证只覆盖该数据库与回执保留范围。
取消能撤销已经执行的外部操作吗?
不能。此处取消阻止后续状态提交;外部副作用需要相应系统的取消、补偿或对账流程。