feat(agents): allow disabling subagent tool feedback

This commit is contained in:
Anton Bogdanovich 2026-05-06 18:20:18 -07:00
parent 7744e44704
commit 2efdcb7bec
6 changed files with 69 additions and 1 deletions

View file

@ -17,6 +17,7 @@
"enabled": false,
"max_args_length": 300,
"separate_messages": false,
"subagents": true,
"style": "raw"
}
}

View file

@ -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 |

View file

@ -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
}

View file

@ -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 {

View file

@ -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.

View file

@ -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")