托管代理现在受 Harness 约束

下午四点,一位开发者的 Claude Code 会话在 Billing API 上开启了一个工作会话,要给套餐变更加上按比例计费。二十分钟后,那个会话还在工作,一个使用 backend-fixer 配置文件的 Archyl 定时运行领到了一张关于发票舍入的工单,而这张工单同样落在 Billing API 里。

直到这周之前,这个定时运行是这样做的。它开启自己的工作会话,托管运行本来就会这样做。预检关卡返回 warn,原因是 1 target element(s) are being worked on by other active sessions — coordinate before changing them。这个判定出现在运行的事件流里。然后运行照样启动了,因为没有任何东西会根据判定采取行动,也没有人在盯着一个定时运行、可以与之协调。

我宁愿直说:对托管运行来说,这层包裹大多只是装饰。关卡被显示出来,然后被忽略。代理改动的文件从来没有和它的会话关联起来,所以记忆落在了任务描述碰巧提到的那些元素上。会话结束时只有一行摘要,下一个代理几乎学不到什么。

这周情况变了。Harness 现在会引导托管运行,托管运行和本地编码代理在同一个模型上相互协调。

运行代理正在变成容易的那部分

这个领域里最大的三家,现在都提供了托管的代理运行环境。Anthropic 的文档把 Claude Managed Agents 描述为"一个预先构建、可配置、运行在托管基础设施上的代理 harness",提供云端或自托管的沙箱、持久会话,以及按 cron 定时的运行。OpenAI 的 Agents API 于 9 月 10 日以公开测试版发布,在 OpenAI 的基础设施上运行代理循环,并负责"编排、长时间运行的会话和上下文管理"。同一天发布的 Cursor Projects 在它自己的云端机器上放一个协调者代理,由它把任务委派给子代理,可以按计划运行,也可以跟着你的 pull request 走。

这些都是好产品,而且指向同一个方向:沙箱、工具循环、会话和定时任务,正在变成从列表里挑一个就行的东西。

运行时自己没法知道的,是你的系统。发票代码属于哪个 container。谁负责它。它的使用方所依赖的契约。让某条边界成为有意为之的那项决策。还有在没人盯着时最要紧的那部分:此刻,哪个其他代理,包括不是由这个运行时启动的代理,正在处理它的哪一部分。

这些知识不可能放在某个运行时里,因为你的代理并不都跑在同一个运行时里。有的跑在笔记本上,有的跑在 CI 里,有的跑在托管的 worker 里。所以我们的思路是:你的运行时,我们的架构。不管是什么在运行代理,它要遵守的模型都是同一个。

Archyl 自己的托管代理运行于 4 月上线,是这些运行时中的一个。这周,它们开始真正像其中一员那样行事。你也可以实时观察并引导一次运行。

运行现在可以被拒绝,并被告知原因

协作需要主动开启。每个代理配置文件在 协作 下有一项新设置 尊重其他代理的工作,默认关闭。开启后,运行会以独占方式开启工作会话,接下来发生什么由关卡判定决定。

如果另一个代理持有相同元素的租约,运行就会被拒绝,不管那个代理是某人笔记本上的 Claude Code,还是另一个托管运行。运行会在克隆任何东西之前结束,关卡事件显示 执行被拒绝,原因里写明是谁在那里、在做什么。以上面那个下午为例:

session denied by preflight gate: "Billing API" is being worked on by
claude-code/vincent (add proration to plan changes)

格式是真实的,任务只是示例。早上查看这个运行的人,确切知道该去找谁。

如果关卡只是给出警告,比如某条 error 级别的护栏适用于这个任务,运行既不会启动,也不会失败。它会停在 等待批准 状态,不占用你组织的并发运行名额。运行页面会列出原因,并提供 批准并开始 和 取消执行。

批准不是给旧判定盖章。它会重新评估关卡,所以如果在运行等待期间有本地代理拿到了这些元素的租约,已批准的运行仍然会被拒绝。24 小时内无人批准的运行会被取消。

为什么等待中的运行什么都不持有

运行在等待时,它的会话会被丢弃。发出警告的会话被取消,租约随之释放,批准时再开启一个新的会话。保留租约听起来更稳妥,实际上更糟:一个始终没人批准的运行,会让其他所有代理在一整天里都碰不了这些元素,还告诉每一个代理那里有东西在工作,而实际上什么都没有。

这延续了我们在 8 月划下的一条线。租约是咨询式的,不是锁。独占是配置文件主动要求的,而不是平台强加的;一个等待人来批准的运行,不占据任何东西。

Guard 搬进了 worker

会话管的是意图。Guard 管的是写下了什么。本地代理从 8 月起就以 Claude Code 钩子的形式用上了它,现在托管运行在 worker 内部也有了同样的检查。

当项目关联了代码仓库时,代理发起的每一次 write_file 和 edit_file 都会在变更生效前与项目的合规规则比对。critical 级别的违规会拒绝写入,代理会读到是哪条规则、为什么:

blocked by Archyl Guard: 1 critical architecture violation(s) in
backend/internal/adapter/http/handlers/invoice.go. Change the content so it
conforms, then write again:
- [critical] No direct database access from HTTP handlers — move the query behind a service

代理把查询挪走,然后重新写入。high 级别的违规会让写入带着警告通过。如果检查本身失败或超时,写入照常通过:Guard 永远不会因为自己的错误阻塞代理,因为一个会搞坏它所治理的工作的治理检查,最后只会被关掉。

每次检查还会带上会话 ID,在代理工作时把文件归属到这次运行的会话上。这一点再往后两节会很关键。

代理知道自己的会话,但不拥有它

运行的提示词现在会写明它的会话,并告诉代理这个会话由平台开启、也将由平台关闭,"所以没有需要调用的会话工具"。代理不能开第二个会话,也不能提前结束这个会话。生命周期属于平台,只有平台知道运行什么时候结束。

代理真正拥有的,是它对这次工作的交代。在结束前,它调用一次 report_outcome,附上一份诚实的摘要、值得写成 ADR 的决策、针对未完成工作的后续事项,以及它所依据的简报记忆。这个工具的描述要求它把情境性的发现排除在决策之外,因为决策会被重放给之后的会话,而一条调试笔记不应该约束任何人。

成果落在工作真正落下的地方

运行结束时,Archyl 首先把这次运行改动的文件归属到会话上。这修补了三个缺口里最不起眼的那个。会话租用的元素是从任务描述里推断出来的,而任务描述只是猜测。改动的文件才是真实发生的事。现在,记忆会写到真实工作触及的元素上,而不是工单里提到的所有东西上。

然后,它关闭会话,把摘要作为记忆保存到这些元素上,并给代理声明自己用过的记忆记上一笔,这正是记忆排序用来学习的信号。

只有运行成功时才会记录决策。决策会成为项目记忆,并开启一份 Architecture Change Request(架构变更请求)草稿,由人来评审模型该如何跟上。失败的运行会保留摘要和后续事项,这正是下一次尝试所需要的,但不记录任何决策。没有完成的工作,没有什么可以站得住的。

运行页面在 工作会话成果 卡片里展示这一切:摘要、决策、后续事项、触及的元素、引用的记忆,以及指向 Change Request 的链接。被其他会话持有的元素会标记为 已被另一个工作会话占用,这样一个在别人租约内改了代码的运行就不会悄无声息地过去。

两类代理,一个模型

这正是很多代理,一个架构想要触及的那块拼图:从不共享进程的代理之间的协调,而且是双向的。

托管运行以 managed-agent/<profile name> 的身份开启会话。把本文开头的场景倒过来:定时运行正在 Billing API 上工作,这时一位开发者在上面启动了 Claude Code。现在,Claude Code 拿到的简报会把这个运行列为冲突:

- **Conflicts** (someone else is already working here):
  - Billing API — held by managed-agent/backend-fixer: fix rounding on prorated invoices

本地代理会被告知在修改该元素前先协调,Fleet 控制台会同时显示两个会话,而无论哪一方结束,都会留下另一方下次会读到的记忆。

本周同时上线

简单说几句,因为其余部分都建立在它们之上。配置文件现在带有内置技能和工具白名单,所以一个只读的审阅者可以被限制为只能使用 read_file、list_* 和 get_*。worker 心跳会发现已经死掉的运行并回收它们,每个运行的 API 密钥会在运行结束时吊销,失败的运行也仍然会把工作发布成一个草稿 pull request。运行始终在 Archyl 自己的 worker 中执行;启用 BYO 时,使用你组织的 AI 提供商。

它做不到的事

协作只能看到使用 Harness 的代理。 租约之所以存在,是因为某个代理开启了会话。不开会话就直接编辑仓库的代理,对关卡来说是不可见的,一个尊重其他代理工作的运行会在它旁边照常启动。

worker 不会运行你的测试。 它有工作区文件工具(读取、写入、编辑、列出、grep),没有 shell。它不能构建项目,也不能跑测试套件,所以一张干净的成果卡片意味着写入通过了 Guard,而不是代码能编译。这件事仍然由 CI 来做。

代理的决策是未经审阅的。 在有人审阅 Change Request 之前,成果卡片里的决策只是代理的一面之词。这次审阅才是重点,不是走过场。

Guard 需要有可以对照的东西。 没有关联仓库,就没有写入检查;合规规则集为空时,所有写入都会通过。

从哪里开始

挑出那个按计划运行、针对的代码人们也会手工修改的配置文件,在 协作 下开启 尊重其他代理的工作。其他配置文件保持原样。

如果它拒绝了某次运行,原因里会写明与之重叠的代理和任务。在此之前,这种重叠会悄悄通过,没有任何人知道。


托管代理运行、工作会话、Guard 和记忆都是 archyl 的一部分。上面提到的每一项设置都在托管代理运行文档里,本地这一侧见 Harness 指南。相关阅读:很多代理,一个架构、编码代理的工作会话、给 AI 编码代理的 memory,以及托管代理运行的发布。