From 7673b626b3d23025e820e87ea7630e2fad5b7237 Mon Sep 17 00:00:00 2001 From: Mauro Date: Thu, 19 Mar 2026 11:08:50 +0100 Subject: [PATCH] feat(tool): debug tool usage via channels (#1332) * feat(tool): debug usage via channel * set defaults * fix conflicts --- config/config.example.json | 6 +++- docs/debug.md | 66 ++++++++++++++++++++++++++++++++++++++ pkg/agent/loop.go | 16 +++++++++ pkg/config/config.go | 54 ++++++++++++++++++++++--------- pkg/config/defaults.go | 4 +++ 5 files changed, 129 insertions(+), 17 deletions(-) diff --git a/config/config.example.json b/config/config.example.json index c214f26fa..6df0a6293 100644 --- a/config/config.example.json +++ b/config/config.example.json @@ -8,7 +8,11 @@ "temperature": 0.7, "max_tool_iterations": 20, "summarize_message_threshold": 20, - "summarize_token_percent": 75 + "summarize_token_percent": 75, + "tool_feedback": { + "enabled": false, + "max_args_length": 300 + } } }, "model_list": [ diff --git a/docs/debug.md b/docs/debug.md index 7e28a15f2..b9e776f0f 100644 --- a/docs/debug.md +++ b/docs/debug.md @@ -31,3 +31,69 @@ When this flag is active, the global truncation function is disabled. This is ex * Verifying the exact syntax of the messages sent to the provider. * Reading the complete output of tools like `exec`, `web_fetch`, or `read_file`. * Debugging the session history saved in memory. + +## Tool Call Visibility in Debug Logs + +When debug mode is active, the agent emits structured log entries at each stage of the tool execution lifecycle. These entries carry a `component=agent` label and use `INFO` or `DEBUG` level depending on the amount of detail: + +| Log message | Level | Key fields | Description | +|---|---|---|---| +| `LLM requested tool calls` | INFO | `tools`, `count`, `iteration` | List of tool names the model decided to call | +| `Tool call: ()` | INFO | `tool`, `iteration` | The tool name and a preview of its arguments (truncated to 200 chars) | +| `Sent tool result to user` | DEBUG | `tool`, `content_len` | Fired when a tool result is forwarded to the chat channel | +| `TTL tick after tool execution` | DEBUG | `agent_id`, `iteration` | MCP tool-discovery TTL decrement after each tool round | +| `Async tool completed, publishing result` | INFO | `tool`, `content_len`, `channel` | Only for tools that run asynchronously in the background | + +### Reading a tool call log entry + +A typical synchronous tool call produces two consecutive lines in the console: + +``` +[...] [INFO] agent: LLM requested tool calls {tools=[web_search], count=1, iteration=1} +[...] [INFO] agent: Tool call: web_search({"query":"picoclaw release notes"}) {tool=web_search, iteration=1} +``` + +The arguments preview is hard-capped at **200 characters** in the logs regardless of the `--no-truncate` flag, because it belongs to the `INFO`-level path. Use `--no-truncate` together with `--debug` to see the full `tools_json` field emitted by the `Full LLM request` DEBUG entry, which contains every tool definition sent to the model. + +## Real-Time Tool Feedback in Chat (tool_feedback) + +Debug logs are server-side only. If you want the agent to send a visible notification directly into the chat channel every time it executes a tool—useful when sharing the bot with other users or for transparency—enable the `tool_feedback` feature in `config.json`: + +```json +{ + "agents": { + "defaults": { + "tool_feedback": { + "enabled": true, + "max_args_length": 300 + } + } + } +} +``` + +When `enabled` is `true`, every tool call sends a short message to the chat before the tool result is returned to the model. The message looks like: + +```bash +🔧 `web_search` +{"query": "picoclaw release notes"} +``` + + +### Options + +| Field | Type | Default | Description | +|---|---|---|---| +| `enabled` | bool | `false` | Send a chat notification for each tool call | +| `max_args_length` | int | `300` | Maximum characters of the serialised arguments included in the notification | + +### Environment variables + +Both fields can also be set via environment variables: + +```bash +PICOCLAW_AGENTS_DEFAULTS_TOOL_FEEDBACK_ENABLED=true +PICOCLAW_AGENTS_DEFAULTS_TOOL_FEEDBACK_MAX_ARGS_LENGTH=300 +``` + +> **Note:** `tool_feedback` is independent of `--debug` mode. It works in production and does not require the gateway to be started with any special flag. diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index a6eccc3fe..edb0994c2 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -1322,6 +1322,22 @@ func (al *AgentLoop) runLLMIteration( "iteration": iteration, }) + // Send tool feedback to chat channel if enabled + if al.cfg.Agents.Defaults.IsToolFeedbackEnabled() && opts.Channel != "" { + feedbackPreview := utils.Truncate( + string(argsJSON), + al.cfg.Agents.Defaults.GetToolFeedbackMaxArgsLength(), + ) + feedbackMsg := fmt.Sprintf("\U0001f527 `%s`\n```\n%s\n```", tc.Name, feedbackPreview) + fbCtx, fbCancel := context.WithTimeout(ctx, 3*time.Second) + _ = al.bus.PublishOutbound(fbCtx, bus.OutboundMessage{ + Channel: opts.Channel, + ChatID: opts.ChatID, + Content: feedbackMsg, + }) + fbCancel() + } + // Create async callback for tools that implement AsyncExecutor. // When the background work completes, this publishes the result // as an inbound system message so processSystemMessage routes it diff --git a/pkg/config/config.go b/pkg/config/config.go index 78b3aa487..947af14a6 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -219,23 +219,32 @@ type RoutingConfig struct { Threshold float64 `json:"threshold"` // complexity score in [0,1]; score >= threshold → primary model } +// ToolFeedbackConfig controls whether tool execution details are sent to the +// chat channel as real-time feedback messages. When enabled, every tool call +// produces a short notification with the tool name and its parameters. +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"` +} + type AgentDefaults struct { - Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"` - RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"` - AllowReadOutsideWorkspace bool `json:"allow_read_outside_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_ALLOW_READ_OUTSIDE_WORKSPACE"` - Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"` - ModelName string `json:"model_name" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL_NAME"` - Model string `json:"model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"` // Deprecated: use model_name instead - ModelFallbacks []string `json:"model_fallbacks,omitempty"` - ImageModel string `json:"image_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_IMAGE_MODEL"` - ImageModelFallbacks []string `json:"image_model_fallbacks,omitempty"` - MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"` - Temperature *float64 `json:"temperature,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"` - MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"` - SummarizeMessageThreshold int `json:"summarize_message_threshold" env:"PICOCLAW_AGENTS_DEFAULTS_SUMMARIZE_MESSAGE_THRESHOLD"` - SummarizeTokenPercent int `json:"summarize_token_percent" env:"PICOCLAW_AGENTS_DEFAULTS_SUMMARIZE_TOKEN_PERCENT"` - MaxMediaSize int `json:"max_media_size,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_MEDIA_SIZE"` - Routing *RoutingConfig `json:"routing,omitempty"` + Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"` + RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"` + AllowReadOutsideWorkspace bool `json:"allow_read_outside_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_ALLOW_READ_OUTSIDE_WORKSPACE"` + Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"` + ModelName string `json:"model_name" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL_NAME"` + Model string `json:"model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"` // Deprecated: use model_name instead + ModelFallbacks []string `json:"model_fallbacks,omitempty"` + ImageModel string `json:"image_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_IMAGE_MODEL"` + ImageModelFallbacks []string `json:"image_model_fallbacks,omitempty"` + MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"` + Temperature *float64 `json:"temperature,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"` + MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"` + SummarizeMessageThreshold int `json:"summarize_message_threshold" env:"PICOCLAW_AGENTS_DEFAULTS_SUMMARIZE_MESSAGE_THRESHOLD"` + SummarizeTokenPercent int `json:"summarize_token_percent" env:"PICOCLAW_AGENTS_DEFAULTS_SUMMARIZE_TOKEN_PERCENT"` + MaxMediaSize int `json:"max_media_size,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_MEDIA_SIZE"` + Routing *RoutingConfig `json:"routing,omitempty"` + ToolFeedback ToolFeedbackConfig `json:"tool_feedback,omitempty"` } const DefaultMaxMediaSize = 20 * 1024 * 1024 // 20 MB @@ -247,6 +256,19 @@ func (d *AgentDefaults) GetMaxMediaSize() int { return DefaultMaxMediaSize } +// GetToolFeedbackMaxArgsLength returns the max args preview length for tool feedback messages. +func (d *AgentDefaults) GetToolFeedbackMaxArgsLength() int { + if d.ToolFeedback.MaxArgsLength > 0 { + return d.ToolFeedback.MaxArgsLength + } + return 300 +} + +// IsToolFeedbackEnabled returns true when tool feedback messages should be sent to the chat. +func (d *AgentDefaults) IsToolFeedbackEnabled() bool { + return d.ToolFeedback.Enabled +} + // GetModelName returns the effective model name for the agent defaults. // It prefers the new "model_name" field but falls back to "model" for backward compatibility. func (d *AgentDefaults) GetModelName() string { diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index 5841504aa..4038696c4 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -35,6 +35,10 @@ func DefaultConfig() *Config { MaxToolIterations: 50, SummarizeMessageThreshold: 20, SummarizeTokenPercent: 75, + ToolFeedback: ToolFeedbackConfig{ + Enabled: true, + MaxArgsLength: 300, + }, }, }, Bindings: []AgentBinding{},