Conversation
Introduce a ToolHook trait and HookRegistry for customizing behavior at pre-execution, post-execution, and post-failure points in the tool lifecycle. Hooks can block calls, modify input/output, add context for the model, request retries on failure, or stop the query loop. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
This PR adds a clean, well-tested hook system for the tool execution lifecycle in crates/agent. The design — ToolHook trait, HookRegistry, HookScope, and the three lifecycle points (pre/post/failure) — is solid and the test coverage is thorough. All 71 tests pass. The clippy error in chain.rs is pre-existing and not introduced by this PR.
Two suggestions worth addressing before the hooks are wired into the actual session/chain execution path:
1. run_post_hooks doesn't short-circuit on StopLoop (inline comment on hooks.rs:254)
Pre-hooks stop on Block; post-hooks don't stop on StopLoop. This asymmetry means hooks registered after a StopLoop hook still run, and any accumulated_context gathered before the stop is silently dropped. The right fix depends on intended semantics — either short-circuit like Block does, or document the behavior explicitly.
2. Retry path skips post-hooks (inline comment on tool_exec.rs:163)
On FailureHookAction::Retry, the retry result is pushed without running post-hooks. If the retry succeeds, post-hooks never process the output. Worth documenting if intentional.
References:
Reviewed by PR / Review
| } | ||
|
|
||
| if stop_loop { | ||
| PostHookAction::StopLoop(current_output) |
There was a problem hiding this comment.
[SUGGESTION]
Priority: Code Quality / Correctness
run_post_hooks doesn't short-circuit after a StopLoop action — the loop continues and subsequent hooks keep processing current_output. This is asymmetric with run_pre_hooks, which returns early on Block.
The consequence is two-fold:
- A hook registered after the
StopLoophook can still modify the output (undocumented, surprising to callers). - Any
accumulated_contextcollected before theStopLoopis silently dropped (line 253–254 discards it). If hook A adds context and hook B later emitsStopLoop, the context disappears with no indication.
If the intended semantics are "first hook to request a stop wins and owns the output", the fix is a break (or return early) when StopLoop is encountered:
PostHookAction::StopLoop(output) => {
return PostHookAction::StopLoop(output); // short-circuit like Block does
}If hooks after StopLoop should still run (observable side-effects), that should be documented and the silent context-drop should be preserved deliberately with a comment explaining why.
| } | ||
| crate::hooks::FailureHookAction::Retry(retry_input) => { | ||
| // Single retry with modified input | ||
| let retry_call = ToolCall { |
There was a problem hiding this comment.
[SUGGESTION]
Priority: Correctness
The retry path executes the retry call and pushes the result directly, without running post-hooks (success or failure) on it:
FailureHookAction::Retry(retry_input) => {
let retry_call = ToolCall { ... };
let retry_result = executor(retry_call).await;
results.push(retry_result); // no post-hooks applied
}If the retry succeeds, post-hooks never see the output. If it fails again, failure hooks don't get a second chance (one retry, silently drops). Whether this is intentional should be documented. If it is intentional (keep it simple, single retry, raw result), a comment explaining the decision would prevent future confusion.
|
Orchestrator Evaluation: Rejected The PR has merge conflicts (mergeable: No) and CI is failing, which are immediate blockers. Beyond that, the implementation is solid in terms of what it delivers — a well-designed hook trait, registry, scope filtering, and integration with the existing tool execution pipeline — but there are notable gaps:
Feedback for agent:
Additional code-level feedback:
|
Summary
ToolHooktrait andHookRegistrytocrates/agent/for customizing tool execution at three lifecycle points: pre-execution, post-execution, and post-failureexecute_tool_calls_with_hooks()that wraps the existingexecute_tool_calls()with hook support, falling back to the fast path when no hooks are registeredHookScopefor filtering hooks to specific tools (All, Tools, Except)Hook capabilities
PreToolUsePostToolUsePostToolUseFailureUse cases
rm -rf /)Test plan
cargo check --workspace)Closes #669
🤖 Generated with Claude Code