diff --git a/pkg/runtime/hooks.go b/pkg/runtime/hooks.go index 3381fe543..4b9b02455 100644 --- a/pkg/runtime/hooks.go +++ b/pkg/runtime/hooks.go @@ -104,6 +104,13 @@ func (r *LocalRuntime) dispatchHook( return nil } + // Auto-fill AgentName for events whose Input builder omits it (tool + // events via toolexec.NewHooksInput), mirroring Cwd auto-fill in + // Executor.Dispatch. Caller-set values win. + if input != nil && input.AgentName == "" { + input.AgentName = a.Name() + } + started := time.Now() if events != nil { events.Emit(HookStarted(event, input.SessionID, a.Name())) diff --git a/pkg/runtime/pre_tool_use_approval_test.go b/pkg/runtime/pre_tool_use_approval_test.go index 2beaff6f6..30f8766d5 100644 --- a/pkg/runtime/pre_tool_use_approval_test.go +++ b/pkg/runtime/pre_tool_use_approval_test.go @@ -260,3 +260,41 @@ func TestPreToolUseHook_PermissionsAllowShortCircuitsHook(t *testing.T) { assert.True(t, *executed, "deterministic Allow must short-circuit the hook (cost / latency win)") } + +// TestPreToolUseHook_ReceivesAgentName verifies dispatchHook auto-fills +// Input.AgentName for tool events, whose toolexec.NewHooksInput builder +// can't set it — matching every other event type. +func TestPreToolUseHook_ReceivesAgentName(t *testing.T) { + t.Parallel() + + executed := new(bool) + agentTools := recordingTool("the_tool", executed) + hookName := "test_agentname_" + t.Name() + + prov := &mockProvider{id: "test/mock-model", stream: &mockStream{}} + root := agent.New("root", "instr", + agent.WithModel(prov), + agent.WithToolSets(newStubToolSet(nil, agentTools, nil)), + agent.WithHooks(preToolUseHooksConfig(hookName)), + ) + rt, err := NewLocalRuntime(team.New(team.WithAgents(root)), + WithSessionCompaction(false), WithModelStore(mockModelStore{})) + require.NoError(t, err) + + var gotAgentName string + require.NoError(t, rt.hooksRegistry.RegisterBuiltin(hookName, func(_ context.Context, in *hooks.Input, _ []string) (*hooks.Output, error) { + if in != nil && in.HookEventName == hooks.EventPreToolUse { + gotAgentName = in.AgentName + } + return &hooks.Output{HookSpecificOutput: &hooks.HookSpecificOutput{ + HookEventName: hooks.EventPreToolUse, + PermissionDecision: hooks.DecisionAllow, + }}, nil + })) + + sess := session.New(session.WithUserMessage("test")) + _ = runJudgedToolCall(t, rt, sess, agentTools) + + assert.Equal(t, "root", gotAgentName, + "pre_tool_use hook Input must carry the dispatching agent's name") +}