feat(hooks): add three observability events around runtime transitions#2542
Merged
dgageot merged 3 commits intodocker:mainfrom Apr 27, 2026
Merged
Conversation
Fires whenever the runtime moves the active agent to a new one:
- transfer_task - delegating to a sub-agent
- transfer_task_return - returning to the caller after a
transferred task completes
- handoff - handing off the conversation to
another agent in the handoffs list
Until now this transition was visible only via the runtime
AgentSwitching event, which forces consumers to subscribe to the
per-session event channel and parse a (start, from, to) triple.
The hook makes the transition accessible via standard hook config
for audit, transcript, and metrics pipelines.
Three new typed Input fields carry the transition: FromAgent,
ToAgent, and AgentSwitchKind. AgentSwitchKind is a stable
classifier suitable for switch statements; the constants
agentSwitchKind* document the contract from the runtime side.
The hook is dispatched on the source agent's executor (the agent
doing the delegation/handoff), so an agent's audit policy follows
the agent itself rather than depending on which target it points
at. Existing AgentSwitching event consumers are unaffected.
Assisted-By: docker-agent
Fires when the user explicitly approves the runtime to continue
past its configured max_iterations limit. The runtime already
supports user-driven resume via the resumeChan / ResumeTypeApprove
path, but the resumption was visible only as a runtime-internal
behaviour (the iteration counter quietly extended). The hook makes
the event accessible for:
- alerting on extended-runtime sessions ("agent X has resumed
past its iteration cap N times this hour")
- billing/quota pipelines that meter resumes separately from
initial budget
- audit transcripts that distinguish "agent stopped at N" from
"agent stopped at N, user resumed, then stopped at N+10"
Two new typed Input fields, PreviousMaxIterations and
NewMaxIterations, carry the granted runtime so audit pipelines can
compute the delta directly without reconstructing it from the
iteration counter.
The hook fires alongside the existing AgentSwitching / Notification
machinery in the resume branch, so consumers that already track
ResumeTypeApprove via other channels see no behaviour change.
Assisted-By: docker-agent
Fires after the runtime's tool approval chain (yolo / permissions /
readonly / ask) resolves a verdict for a tool call, BEFORE the call
is executed (allow) or its error response is recorded (deny /
canceled). Until now this verdict was implicit \u2014 reconstructible
only by correlating ToolCall, ToolCallConfirmation, ToolCallResponse,
and HookBlocked events from the runtime channel. The hook gives
audit pipelines a single, structured "who approved what" record.
Two new typed Input fields:
- ApprovalDecision: "allow" | "deny" | "canceled"
- ApprovalSource: stable classifier for which step decided
(yolo, session_permissions_allow,
session_permissions_deny, team_permissions_allow,
team_permissions_deny, readonly_hint,
user_approved, user_approved_session,
user_approved_tool, user_rejected,
context_canceled)
Constants live on the runtime side as Approval{Decision,Source}*
so the contract between executeWithApproval and the hook protocol
is discoverable from one place. allowSourceFor / denySourceFor map
the existing permissionChecker.source labels onto the public
classifiers; unknown labels default to team_permissions to avoid
silent misclassification on future label changes.
The hook is fired at every return path of executeWithApproval and
askUserForConfirmation, so a single hook gets exactly one record
per tool call regardless of which step decided. Existing event
consumers see no change.
Assisted-By: docker-agent
gtardif
approved these changes
Apr 27, 2026
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.
Three small, additive observability events covering runtime transitions
that today are visible only via the per-session event channel. Each
gives audit / transcript / metrics / billing pipelines a structured,
typed entry point without forcing them to subscribe to the channel
and reconstruct state from the existing event stream.
Mechanics (per commit)
Each commit follows the now-established one-event template and is
deliberately minimal:
existing `dispatchHook` helper
`notification_message` string-parsing)
event-channel emissions, so consumers that already listen to
`AgentSwitching` / `MaxIterationsReached` / `ToolCallConfirmation` +
`HookBlocked` see no behaviour change
For `on_tool_approval_decision`, the source classifier is fired at
every return path of `executeWithApproval` and `askUserForConfirmation`
so a single hook gets exactly one record per tool call regardless of
which step decided. Stable string constants live on the runtime side
(`ApprovalSource*`, `ApprovalDecision*`, `agentSwitchKind*`) so the
contract between the runtime and the hook protocol is discoverable
and a typo trips a compile error.
Tests
A `recordingBuiltin` helper (introduced in the first commit, reused
by the next two) registers itself on the runtime's private
`hooksRegistry` post-construction. The pattern (`runtimeWithRecorded*`)
keeps the assertions close to a real call path without exposing a
`WithHooksRegistry` option that production callers would reach for.
Each event has its own `pkg/runtime/on_*_test.go` covering:
iteration limits all forwarded verbatim)
configured, since the executor lookup short-circuits)
`denySourceFor` mapping is pinned, including the
unknown-label-defaults-to-team-permissions safety behaviour.
`mise test` and `mise lint` are both clean.
Commits