From 2efdcb7bec391a88e17b35069c08a5f7f940310e Mon Sep 17 00:00:00 2001 From: Anton Bogdanovich <27antonb@gmail.com> Date: Wed, 6 May 2026 18:20:18 -0700 Subject: [PATCH] feat(agents): allow disabling subagent tool feedback --- config/config.example.json | 1 + docs/operations/debug.md | 3 +++ pkg/agent/agent_test.go | 23 +++++++++++++++++++++++ pkg/agent/agent_utils.go | 9 ++++++++- pkg/config/config.go | 8 ++++++++ pkg/config/config_test.go | 26 ++++++++++++++++++++++++++ 6 files changed, 69 insertions(+), 1 deletion(-) diff --git a/config/config.example.json b/config/config.example.json index 5d0ff3683..a0d252980 100644 --- a/config/config.example.json +++ b/config/config.example.json @@ -17,6 +17,7 @@ "enabled": false, "max_args_length": 300, "separate_messages": false, + "subagents": true, "style": "raw" } } diff --git a/docs/operations/debug.md b/docs/operations/debug.md index aeeb38c33..9304f9a2b 100644 --- a/docs/operations/debug.md +++ b/docs/operations/debug.md @@ -67,6 +67,7 @@ Debug logs are server-side only. If you want the agent to send a visible notific "enabled": true, "max_args_length": 300, "separate_messages": true, + "subagents": true, "style": "raw" } } @@ -90,6 +91,7 @@ Set `style` to `working_summary` to use a compact, non-argument progress message "tool_feedback": { "enabled": true, "separate_messages": false, + "subagents": true, "style": "working_summary" } } @@ -121,6 +123,7 @@ The `working_summary` style intentionally does not show raw tool arguments, expl |---|---|---|---| | `enabled` | bool | `false` | Send a chat notification for each tool call | | `separate_messages` | bool | `false` | Keep every tool feedback update as a separate chat message instead of reusing a single placeholder/progress message | +| `subagents` | bool | `true` | Also publish visible tool feedback for subagent turns when `enabled` is true | | `max_args_length` | int | `300` | Maximum characters of the serialised arguments included in the notification | | `style` | string | `raw` | Feedback format. Use `raw` for the original tool/explanation/argument preview, or `working_summary` for compact progress lines without raw arguments | diff --git a/pkg/agent/agent_test.go b/pkg/agent/agent_test.go index ebb66e19b..bdb4ef3dc 100644 --- a/pkg/agent/agent_test.go +++ b/pkg/agent/agent_test.go @@ -2093,6 +2093,29 @@ func TestToolFeedbackArgsPreview_UsesJSONAndTruncates(t *testing.T) { } } +func TestShouldPublishToolFeedback_DisablesSubagentFeedback(t *testing.T) { + subagents := false + cfg := config.DefaultConfig() + cfg.Agents.Defaults.ToolFeedback = config.ToolFeedbackConfig{ + Enabled: true, + Subagents: &subagents, + } + + if shouldPublishToolFeedback(cfg, &turnState{ + channel: "telegram", + sessionKey: "subturn-1", + }) { + t.Fatal("shouldPublishToolFeedback() = true for disabled subagent feedback, want false") + } + + if !shouldPublishToolFeedback(cfg, &turnState{ + channel: "telegram", + sessionKey: "chat-1", + }) { + t.Fatal("shouldPublishToolFeedback() = false for main turn, want true") + } +} + type picoInterleavedContentProvider struct { calls int } diff --git a/pkg/agent/agent_utils.go b/pkg/agent/agent_utils.go index 4fad83c0a..442a4a3fc 100644 --- a/pkg/agent/agent_utils.go +++ b/pkg/agent/agent_utils.go @@ -178,7 +178,14 @@ func shouldPublishToolFeedback(cfg *config.Config, ts *turnState) bool { if ts == nil || ts.channel == "" || ts.opts.SuppressToolFeedback { return false } - return cfg != nil && cfg.Agents.Defaults.IsToolFeedbackEnabled() + if cfg == nil || !cfg.Agents.Defaults.IsToolFeedbackEnabled() { + return false + } + if strings.HasPrefix(strings.TrimSpace(ts.sessionKey), "subturn-") && + !cfg.Agents.Defaults.IsSubagentToolFeedbackEnabled() { + return false + } + return true } func toolFeedbackTitleForTurn(ts *turnState) string { diff --git a/pkg/config/config.go b/pkg/config/config.go index 463c77a29..dc66291d8 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -252,6 +252,7 @@ type ToolFeedbackConfig struct { Enabled bool `json:"enabled" env:"PICOCLAW_AGENTS_DEFAULTS_TOOL_FEEDBACK_ENABLED"` MaxArgsLength int `json:"max_args_length" env:"PICOCLAW_AGENTS_DEFAULTS_TOOL_FEEDBACK_MAX_ARGS_LENGTH"` SeparateMessages bool `json:"separate_messages" env:"PICOCLAW_AGENTS_DEFAULTS_TOOL_FEEDBACK_SEPARATE_MESSAGES"` + Subagents *bool `json:"subagents,omitempty"` Style string `json:"style,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_TOOL_FEEDBACK_STYLE"` } @@ -305,6 +306,13 @@ func (d *AgentDefaults) IsToolFeedbackEnabled() bool { return d.ToolFeedback.Enabled } +// IsSubagentToolFeedbackEnabled returns true when subagent turns should publish +// visible tool feedback. It defaults to true for backward compatibility when +// tool_feedback itself is enabled. +func (d *AgentDefaults) IsSubagentToolFeedbackEnabled() bool { + return d.ToolFeedback.Subagents == nil || *d.ToolFeedback.Subagents +} + // IsToolFeedbackSeparateMessagesEnabled returns true when each tool feedback // update should be sent as its own chat message instead of editing a single // in-place progress message. diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index b0e952488..70f731bf6 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -787,6 +787,9 @@ func TestDefaultConfig_ToolFeedbackDisabled(t *testing.T) { if cfg.Agents.Defaults.ToolFeedback.Enabled { t.Fatal("DefaultConfig().Agents.Defaults.ToolFeedback.Enabled should be false") } + if !cfg.Agents.Defaults.IsSubagentToolFeedbackEnabled() { + t.Fatal("DefaultConfig().Agents.Defaults.IsSubagentToolFeedbackEnabled() should default to true") + } if cfg.Agents.Defaults.ToolFeedback.SeparateMessages { t.Fatal("DefaultConfig().Agents.Defaults.ToolFeedback.SeparateMessages should be false") } @@ -813,6 +816,9 @@ func TestLoadConfig_ToolFeedbackDefaultsFalseWhenUnset(t *testing.T) { if cfg.Agents.Defaults.ToolFeedback.Enabled { t.Fatal("agents.defaults.tool_feedback.enabled should remain false when unset in config file") } + if !cfg.Agents.Defaults.IsSubagentToolFeedbackEnabled() { + t.Fatal("agents.defaults.tool_feedback.subagents should default to true when unset") + } if cfg.Agents.Defaults.ToolFeedback.SeparateMessages { t.Fatal("agents.defaults.tool_feedback.separate_messages should remain false when unset in config file") } @@ -841,6 +847,26 @@ func TestLoadConfig_ToolFeedbackStyle(t *testing.T) { } } +func TestLoadConfig_ToolFeedbackSubagentsFalse(t *testing.T) { + dir := t.TempDir() + configPath := filepath.Join(dir, "config.json") + if err := os.WriteFile( + configPath, + []byte(`{"version":1,"agents":{"defaults":{"tool_feedback":{"enabled":true,"subagents":false}}}}`), + 0o600, + ); err != nil { + t.Fatalf("WriteFile() error: %v", err) + } + + cfg, err := LoadConfig(configPath) + if err != nil { + t.Fatalf("LoadConfig() error: %v", err) + } + if cfg.Agents.Defaults.IsSubagentToolFeedbackEnabled() { + t.Fatal("agents.defaults.tool_feedback.subagents = true, want false") + } +} + func TestLoadConfig_WebPreferNativeDefaultsTrueWhenUnset(t *testing.T) { dir := t.TempDir() configPath := filepath.Join(dir, "config.json")