feat(gateway): tag agent:start payload with turn trigger + interrupt_depth - #37291
Closed
Couiz wants to merge 2 commits into
Closed
feat(gateway): tag agent:start payload with turn trigger + interrupt_depth#37291Couiz wants to merge 2 commits into
Couiz wants to merge 2 commits into
Conversation
The main message dispatch emits the `agent:start` hook before running the agent, but the interrupt/drain follow-up path in `_run_agent` promoted a queued message straight into a recursive `_run_agent` call without emitting `agent:start`. Every hook listening on `agent:start` (SessionStart-style integrations, activity loggers, visualizers) silently missed interrupt/queue follow-up turns — an event-emission gap, not a hook bug. Emit `agent:start` on the drain path right before the recursive `_run_agent`, mirroring the main-dispatch payload (platform, user_id, chat_id, session_id, message[:500]) but built from the follow-up turn's source (`next_source`) and the final, already-transcribed text (`next_message`) — so voice follow-ups carry the transcript, not the raw audio placeholder. The emit sits after every discard guard (draining, interrupt depth-cap, stale /goal continuation, transcription→None) so it fires exactly once per turn, only when the follow-up actually proceeds to the agent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…depth
Both agent:start emit sites — the main inbound dispatch and the
interrupt/drain follow-up path in _run_agent — previously emitted an
identical payload shape, so hooks (voice-echo, activity loggers,
visualizers) could not tell a fresh user turn from an interrupt-driven
follow-up turn.
Add two discriminator fields to the agent:start payload at both sites:
* trigger — a string, not a bool, so future turn kinds like "goal" or
"schedule" can be added without breaking the contract: "message" on the
main dispatch, "interrupt" on the drain follow-up. Hooks read it
backward-compatibly as context.get("trigger", "message").
* interrupt_depth — an int: 0 on the main dispatch (a fresh turn is never
an interrupt); _interrupt_depth + 1 on the drain path, matching the
depth handed to the recursive _run_agent call (first interrupt -> 1,
interrupt-of-an-interrupt -> 2, ...).
Both payloads stay shape-consistent (7 keys); no existing key changes. The
drain emit still sits after every discard guard, so it fires once per turn
only when the follow-up actually proceeds to the agent.
Depends on NousResearch#37269.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jun 4, 2026
Author
|
Consolidated into #39126 — the three changes (agent:start on drain, trigger/depth tagging, agent:end on drain) are one coherent concern, and the agent:end fix's payload carries the trigger/interrupt_depth tagging, so they aren't cleanly separable. A single PR off fresh main reviews better than three stacked PRs across diverging bases. Closing in favor of #39126. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
agent:startis emitted at two sites ingateway/run.py— the main inbound dispatch and the interrupt/drain follow-up path in_run_agent— and both previously emitted an identical payload shape. Hooks (voice-echo, activity loggers, visualizers) therefore could not distinguish a fresh user turn from an interrupt-driven follow-up turn.This adds two discriminator fields to the
agent:startpayload at both sites:trigger— a string (not a bool, so future turn kinds like"goal"/"schedule"extend the contract without breaking it):"message"on the main dispatch,"interrupt"on the drain follow-up. Hooks read it backward-compatibly viacontext.get("trigger", "message").interrupt_depth— an int:0on the main dispatch (a fresh turn is never an interrupt);_interrupt_depth + 1on the drain path, matching the depth handed to the recursive_run_agentcall (first interrupt → 1, interrupt-of-an-interrupt → 2, …).Both payloads stay shape-consistent (7 keys); no existing key changes. The drain emit still sits after every discard guard (stale
/goal, transcription→None), so it fires once per turn only when the follow-up actually proceeds to the agent.Tests
test_drain_emits_agent_start.py: updated the exact-dict assertion to the new shape; added a first-level (depth == 1) and a nested (_interrupt_depth=1 → depth == 2) case driving the real_run_agentdrain path. The existing no-emit-on-discard tests are unchanged and still pass.test_agent_start_trigger.py(new): the main-dispatch emit lives ~630 lines deep behind session-store/DB/env I/O, so rather than mock that world this statically inspects the actual dict literal handed tohooks.emit("agent:start", …)and pinstrigger="message"/interrupt_depth=0.Notes
Depends on #37269 (the drain-emit fix this builds on). The first commit in this PR is that fix; it will drop out once #37269 merges.