Agent Bus:把多 Agent 协作写成可验证的任务契约

从任务字段到结果验收,定义每个 Agent 能读什么、能做什么,以及服务端怎样确认工作完成。

你能带走什么
  • 身份来自可信入口,正文不能声明权限
  • 事件只提示任务编号,执行前回读权威记录
  • 领取与提交检查版本 状态 目标主体和截止时间
  • 结果同时通过结构检查与任务验收
  • 同任务同提交键同内容返回原回执

1. 让协作有可以重新读取的依据

协调器把检查工作交给另一个 Agent,随后关闭窗口。过一会儿回来,它需要知道任务有没有被领取,结果是否通过验收,以及取消请求有没有生效。聊天里的一次 @ 提及能够表达协作意图,这些状态仍需要一个明确的存储位置。本文把 Agent Bus 定义为应用层任务服务:每份工作有稳定身份、接收者、状态机和受保护的结果入口。

一个契约需要同时回答四个问题:谁有资格提出这份工作,执行者怎样取得当前版本,什么证据足以结束任务,争议发生后到哪里查询记录。将这些答案写进服务接口之后,协调器才能在断开会话后继续判断进度。任务标题可以给人阅读,实际执行仍由受限的类型和参数决定,避免把任意自然语言直接变成工具调用。

下面用两段公开的英文短句做词数统计。协调器分别派给 worker-a 与 worker-b,两个确定性处理器提交词数和原文哈希,服务端再次计算后验收。示例只使用 Python 标准库和 SQLite,不调用模型、网络、真实 Agent 或生产工具,也不需要密钥。它演示可检查的契约,不能直接充当安全生产服务。

任务存储与唤醒方式可以分别选择。轮询、队列通知或事件回调,都可以提示 worker 检查新工作;任务的归属和状态始终由服务端记录决定。本文的字段、权限和函数名称属于示例系统设计,并非 MCP 或所有多 Agent 产品共同采用的标准。

协调器经任务服务向两个 worker 分派工作。任务服务保存权威状态。事件通道只携带 task_id 提示;聊天提及不提供持久领取或完成契约。
图 1 协调器写入任务,worker 从服务领取并提交;事件只缩短发现任务的等待时间。

2. 先确定任务字段的含义

task_id 由服务端生成,后续领取、取消与提交都引用它。from 由已认证主体填入;创建请求不得自行提供该字段。to 指向允许名单中的接收者,type 绑定处理器和验收规则。正文只保存 data.fixture_id,避免把整段会话、凭据或无关文件一并复制给每个 worker。生产中的工件引用还应绑定可访问范围和内容版本。

这里对创建请求采用封闭字段集合,多一个 from 字段也会拒绝。这样的限制适合教学,能及时暴露调用方对契约的误解。生产需要提前定义版本演进规则:新增可选字段是否允许,旧 worker 会怎样处理未知任务类型,结果模式升级后旧回执是否仍可解释。破坏兼容的改动应使用新的类型或模式版本,不能沿用旧名称却悄悄改变含义。

correlation_id 把同一业务流程中的多个任务关联起来,用于查询和排障,不参与去重。revision 是任务状态版本,从一开始,每次成功变更递增。读取到版本二的 worker,提交时必须仍然面对版本二。别人已经取消任务或完成提交,旧请求就会发生版本冲突。

result 保存验收通过的结构化产物;error 保存受控错误码;cancel 记录取消主体和服务端时间。expires_at 是固定绝对截止时间,重试不会顺延。submission_id 单独保存在结果回执表,用于识别同一任务的重复提交。任务关联、状态版本和提交去重各有独立用途,调用方不能互相替代。

四组字段分别控制身份归属、执行并发、输入结果和终止时间。任务 ID 与关联 ID 和提交 ID 的作用分开。
图 2 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 动作。实际添加这些能力时,要逐项记录获准动作、资源、接收者、数据范围和有效期,并在执行前再次检查。一个只读审查任务不能借结果文字申请更高权限;新增动作默认拒绝,等待独立授权。

权限矩阵列出创建、读取审计、队列、领取提交失败、取消和过期六组动作的允许范围。非任务目标不能读取该任务。
图 3 演示中只有 coordinator 能创建任务,clock 只能清理到期状态;没有通用管理员或跨任务读取入口。

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 或其他宿主都能在后台自动启动下一轮执行。本文完全离线,不验证订阅、签名、回调、后台唤醒或宿主兼容性。

事件只唤醒 worker。worker 使用其自身身份读任务,再按当前 revision 领取,执行固定处理器并提交验收。事件中的命令、输入和结果均被忽略。
图 4 本地测试向事件塞入伪造输入与命令,worker 仍只使用 get 返回的公开 fixture。

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 会等待取消或过期。

queued 领取后进入 running,running 通过 submit 进入 succeeded 或 fail 进入 failed。queued 和 running 均可被取消或过期。终态不能回到开放状态。
图 5 即使定时清理尚未运行,到期检查也会阻止新的领取和结果提交;get 仍保持只读。

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 确认当前状态。外部工具副作用另需自己的去重或对账机制。

首次提交经过结构校验、版本状态校验和业务验收后,将任务、结果回执与成功审计原子提交。重复键的内容相同才返回旧回执。
图 6 先鉴权再返回缓存回执,防止另一个 worker 通过重放读取别人的结果。

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 文件本身的读写权限也属于部署边界。

取消先提交会增加 revision,使旧提交冲突。结果先提交则保留 succeeded。审计字段记录身份、动作、结论和版本,不复制完整输入。
图 7 示例审计保留成功变更和已认证主体的拒绝写入,不存正文;生产还需独立的访问日志和防篡改留存。

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 存储内容指纹。同键同内容返回原回执,冲突内容和第二个键对终态的提交被拒绝。保证只覆盖该数据库与回执保留范围。

取消能撤销已经执行的外部操作吗?

不能。此处取消阻止后续状态提交;外部副作用需要相应系统的取消、补偿或对账流程。