DSH / Atlas
2026-08-06implementedarchitecture

Agent-scoped events dispatch a single payload object

Agent 作用域事件 dispatch 单个 payload 对象

Agent-scoped events historically took positional arguments: a leading `agent` subject, event-specific fields, and a trailing `next` for waterfall/serial events. Adding a field or retiring a context type (as with `PreStepContext` and `RequestFailureContext`) rewrote every listener and emitter across packages, and the contract stayed spread across the parameter list instead of one named payload.

English

Problem

Agent-scoped events historically took positional arguments: a leading agent subject, event-specific fields, and a trailing next for waterfall/serial events. Adding a field or retiring a context type (as with PreStepContext and RequestFailureContext) rewrote every listener and emitter across packages, and the contract stayed spread across the parameter list instead of one named payload.

Decision

Every agent-scoped event takes exactly one payload object as its first argument. The payload always carries the subject (agent), the event's fields, and the cancellation signal when the event has one; next remains the last argument of waterfall/serial events. The affected events are the twelve agent/* events, agent-loop/config-start-failed (the only one without a subject), and goal/changed.

PreStepContext and RequestFailureContext are retired; their fields live directly in the agent/pre-step and agent/request-error payloads.

Dispatch is fused: agentEvents(ctx, agent) (and the one-shot emitAgentEvent) injects the subject so the scope carrier key and the payload's agent cannot diverge, and the injected subject wins even over a structurally acceptable payload that happens to carry an agent field. ReactLoopAgent builds its dispatcher once in the constructor and routes every emit, serial, and waterfall through it, so hot-path dispatches allocate nothing.

Alternatives considered

Keep positional signatures. Adding a field or retiring a context type would keep rewriting every listener and emitter, and the contract would stay spread across the parameter list instead of one named payload.

Hand-build the subject at each dispatch site. The loop's intermediate design called ctx.waterfall(this.carrier, …) with a manually constructed { agent: this, … } payload; it avoided per-dispatch allocation but duplicated the subject injection and let the scope key and the payload subject diverge. The fused dispatcher is the single injection point for every dispatch mode.

Consequences

Listener signatures name the full payload once, so extending a payload or retiring a context type is a one-shape change across all listeners and emitters. The subject/scope coupling is enforced by the dispatcher for every dispatch mode, and the loop's hot paths stay allocation-free.

中文

问题

Agent 作用域事件历来采用位置参数:开头的 agent 主体、事件专属字段,以及末尾用于 waterfall(瀑布式事件)/serial 事件的 next。新增字段或退役上下文类型(如 PreStepContextRequestFailureContext)都会迫使跨包重写每个监听器和 emitter,约定也一直分散在参数列表中,而不是集中在一个具名 payload 中。

决策

每个 agent 作用域事件都将恰好一个 payload 对象作为其第一个参数。payload 始终携带主体(agent)、事件的字段,以及事件有取消信号时的取消 signalnext 仍然是 waterfall/serial 事件的最后一个参数。受影响的事件是十二个 agent/* 事件、agent-loop/config-start-failed(唯一没有主体的事件)以及 goal/changed

PreStepContextRequestFailureContext 已退役;它们的字段直接存在于 agent/pre-stepagent/request-error 的 payload 中。

dispatch 是融合的:agentEvents(ctx, agent)(以及一次性 emitAgentEvent)注入主体,使作用域载体键与 payload 的 agent 不可能分叉;即使某个结构上可接受的 payload 恰好携带 agent 字段,注入的主体仍然优先。ReactLoopAgent 在构造函数中构建一次 dispatcher,并将每个 emit、serial 和 waterfall 都经由它路由,因此热路径上的 dispatch 不产生任何分配。

考虑过的替代方案

保留位置签名。 新增字段或退役上下文类型依旧会重写每个监听器和 emitter,约定也会继续分散在参数列表中,而不是集中在一个具名 payload 中。

在每个 dispatch 位置手工构造主体。 loop 的中间设计调用 ctx.waterfall(this.carrier, …),传入手工构造的 { agent: this, … } payload;它避免了每次 dispatch 的分配,却重复了主体注入,并让作用域键与 payload 主体分叉。融合的 dispatcher 是每种 dispatch 模式的唯一注入点。

后果

监听器签名一次性命名完整 payload,因此扩展 payload 或退役上下文类型,对所有监听器和 emitter 都是一次形状变更。主体/作用域耦合由 dispatcher 在每种 dispatch 模式下强制执行,且 loop 的热路径保持零分配。