Files
deepseek-harness/.agents/notes/implemented/architecture/2026-08-06-agent-event-payload-objects.md
T
_Kerman 262d044642 fix(agent): route loop dispatches through prebuilt fused dispatcher
Address review feedback on PR #1738:
- ReactLoopAgent builds its AgentEventDispatch once in the constructor and
  routes every emit/serial/waterfall through it, so hot-path dispatches no
  longer allocate a carrier and dispatcher per call; the public carrier
  field is gone (fused dispatcher is private).
- agentEvents accepts an optional prebuilt carrier.
- The fused payload builder spreads the payload before the injected agent
  so a structurally acceptable payload carrying an agent field can never
  override the subject.
- Regenerate doc graphs; re-record core + architecture + affected Agent
  Note translation pairs; add payload-object event contract Agent Note.
2026-08-06 13:44:23 +08:00

2.4 KiB

Agent Note: Agent-scoped events dispatch a single payload object

Status: implemented

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.