DSH / Atlas
2026-06-20implementedsimplification

Keep one public stop primitive

保留单一公开停止原语

The public `Agent` handle exposed two overlapping ways to stop in-flight work: step-only `abort()` and queue-aware `cancel()`. The former preserved queued input while the latter originally only exposed its broad default, which clears queued and steering work while aborting the active turn. `cancel(cause, { keepInbox: true })` now covers the production Web stop policy without exposing the private turn holder; ACP reta

English

Problem

The public Agent handle exposed two overlapping ways to stop in-flight work: step-only abort() and queue-aware cancel(). The former preserved queued input while the latter originally only exposed its broad default, which clears queued and steering work while aborting the active turn. cancel(cause, { keepInbox: true }) now covers the production Web stop policy without exposing the private turn holder; ACP retains broad cancellation, while lifecycle owners tear down agents through AgentHandle.dispose(). No production caller needs a bare step-only abort.

The behavioral distinction is real, but no shipping code needs a separate narrower verb. AgentLoop owns one private cancellation holder for the whole turn. cancel(cause, options?) carries an explicit typed user or parent cause; its broad default drops pending input, while keepInbox preserves pending work for later turns. Disposal remains a separate lifecycle interruption. The complete ownership and propagation contract lives in the explicit turn cancellation note.

The extra surface area made the loop carry a public verb that was mostly a teardown internal. An options-bearing cancel() expresses caller policy without exposing a second holder-shaped operation.

Decision

cancel() is the only public stop primitive on Agent. Lifecycle owners use AgentHandle.dispose() to stop and unregister an agent; non-owners use broad cancel() to abandon current and queued work or keepInbox to abort the active turn while retaining pending work. The implementation keeps a private turn cancellation holder, but it is not part of the plugin-facing Agent contract. The Web stop decision is the production keepInbox consumer.

whenIdle() is retained as the public quiescence-observation primitive (resolve once the agent settles out of running, resolve immediately when already idle, await the loop exit when disposed). It is not a stop verb; it is how a non-owner observes the stop completing without disposing the agent. Its live consumers are ACP and agent tests that await settlement through this public contract (packages/acp/acp/tests, packages/core/agent-loop/tests); the production ACP bridge owns its agents and tears them down through AgentHandle.dispose(), so packages/acp/acp/src itself has no whenIdle() call.

Public abort() is absent, and the disposer remains async and waits for the loop to stop. Tests exercise cancellation through the public typed cause and explicit signal APIs rather than reaching into the holder.

Alternatives considered

Removing whenIdle() too — the original proposal's shape, reversed on validating the premise against the code: it is a load-bearing quiescence primitive that safely handles waiter settlement and replacement-turn races, and pushing consumers onto hand-observed runningidle transitions is exactly the brittle path the defensive patterns warn against.

Verification

Agent exposes no public abort() while cancel(), whenIdle(), and steer() remain; ACP cancellation calls broad cancel(), Web stop calls cancel(..., { keepInbox: true }), and teardown awaits quiescence through handle disposal. whenIdle() resolves on quiescence for non-owner observers, and the suites cover cancellation and disposal as the two supported stop paths.

Consequences

A plugin can abort the active turn while preserving queued prompts through keepInbox, but it cannot abort only one model/tool step while leaving that turn running. A step-only use case would need a named consumer and a narrower contract; exposing the private loop mechanic remains unjustified.

Related

This Agent Note only removes the redundant stop verb. Mid-turn steering remains an intentional message path; quiescence observation remains via whenIdle(). The resulting delivery surface is followup(), steer(), and inject(); stopping and observation remain with cancel() and whenIdle().

中文

问题

公共 Agent handle 暴露了两种相互重叠的在途工作停止方式:仅针对步骤的 abort() 和感知队列的 cancel()。前者保留已排队输入,后者原本只暴露广义默认行为,该行为会清除已排队和 steering(中途引导)工作,同时中止活动轮次。cancel(cause, { keepInbox: true }) 现在无需暴露私有轮次 holder 即可覆盖生产环境的 Web 停止策略;ACP(Agent Client Protocol)保留广义取消,生命周期拥有者则通过 AgentHandle.dispose() 拆除 agent(智能体)。没有生产调用方需要一个裸的、仅针对步骤的 abort。

行为差异确实存在,但实际交付的代码不需要独立的更窄动词。AgentLoop 为整个轮次拥有一个私有取消 holder。cancel(cause, options?) 携带显式且类型化的 userparent 原因;其广义默认行为丢弃待处理输入,keepInbox 则为后续轮次保留待处理工作。dispose(资源释放)仍是单独的生命周期中断。完整的归属与传播约定位于显式轮次取消 Agent Note

多余的公开接口使循环承载了一个本质上属于内部拆卸的公开动词。带选项的 cancel() 可以表达调用方策略,而无需暴露第二个 holder 形态的操作。

决策

cancel()Agent 上唯一的公共停止原语。生命周期拥有者使用 AgentHandle.dispose() 停止并注销 agent;非拥有者使用广义 cancel() 放弃当前和已排队工作,或使用 keepInbox 中止活动轮次并保留待处理工作。实现保留一个私有轮次取消 holder,但它不属于面向插件的 Agent 约定。Web 停止决策是生产环境中的 keepInbox 消费方。

whenIdle() 保留为公开的完全停稳观测原语(agent 退出 running 状态并完全停稳后 resolve,已处于 idle 时立即 resolve,dispose 后等待循环退出)。它不是停止动词;它是非所有者在不 dispose agent 的前提下观测停止完成的方式。它的活跃消费方是 ACP 和通过此公开约定等待结算的 agent 测试(packages/acp/acp/testspackages/core/agent-loop/tests);生产环境的 ACP 桥接层拥有其 agent 并通过 AgentHandle.dispose() 销毁它们,因此 packages/acp/acp/src 本身没有 whenIdle() 调用。

公共 abort() 已不存在,disposer 仍为异步并等待循环停止。测试通过公共类型化原因和显式 signal API 验证取消,而不会伸入 holder 内部。

曾考虑的替代方案

同时移除 whenIdle():最初提案的形态,在对照代码验证前提后被推翻:它是承重的完全停稳原语,能安全处理等待者结算与替换轮次竞态,迫使消费方手动观测 runningidle 转换正是防御性模式所警告的脆弱路径。

验证

Agent 不再暴露公开的 abort(),而 cancel()whenIdle()steer() 保留;ACP 取消调用广义 cancel(),Web 停止调用 cancel(..., { keepInbox: true }),拆卸则通过 handle 的 dispose 等待完全停稳。whenIdle() 在完全停稳时为非所有者观测者 resolve;测试套件覆盖取消和 dispose 这两条受支持的停止路径。

后果

插件可以通过 keepInbox 在保留已排队提示词的同时中止活动轮次,但不能只中止某一个模型/工具步骤而让该轮次继续运行。仅步骤用例需要具名消费方和更窄约定;暴露私有循环机制仍缺乏正当理由。

相关

本 Agent Note 只移除冗余的停止动词。轮次中途 steering 仍是一条有意保留的消息路径;完全停稳观察仍通过 whenIdle() 完成。最终的消息投递接口包括 followup()steer()inject();停止与观察仍通过 cancel()whenIdle() 完成。