diff --git a/config/config.example.json b/config/config.example.json index 9575039f8..a8f638094 100644 --- a/config/config.example.json +++ b/config/config.example.json @@ -203,6 +203,7 @@ } }, "tools": { + "enable_notifications": false, "web": { "brave": { "enabled": false, diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 5558f7c0e..ef2dd70ed 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -668,6 +668,28 @@ func (al *AgentLoop) runLLMIteration( "iteration": iteration, }) + // notify tool use on channel + if al.cfg.Tools.EnableNotifications && !constants.IsInternalChannel(opts.Channel) { + var notification string + + if tool, ok := agent.Tools.Get(tc.Name); ok { + // check the tool implements the interface + if notifier, isNotifier := tool.(tools.NotificationFormatter); isNotifier { + notification = notifier.FormatNotification(tc.Arguments) + } else if tc.Name != "message" { + notification = fmt.Sprintf("🛠️ Tool use: `%s`", tc.Name) + } + } + + if notification != "" { + al.bus.PublishOutbound(bus.OutboundMessage{ + Channel: opts.Channel, + ChatID: opts.ChatID, + Content: notification, + }) + } + } + // Create async callback for tools that implement AsyncTool // NOTE: Following openclaw's design, async tools do NOT send results directly to users. // Instead, they notify the agent via PublishInbound, and the agent decides diff --git a/pkg/config/config.go b/pkg/config/config.go index 6f76614cf..b228379e2 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -468,10 +468,11 @@ type ExecConfig struct { } type ToolsConfig struct { - Web WebToolsConfig `json:"web"` - Cron CronToolsConfig `json:"cron"` - Exec ExecConfig `json:"exec"` - Skills SkillsToolsConfig `json:"skills"` + EnableNotifications bool `json:"enable_notifications" env:"PICOCLAW_TOOLS_ENABLE_NOTIFICATIONS"` + Web WebToolsConfig `json:"web"` + Cron CronToolsConfig `json:"cron"` + Exec ExecConfig `json:"exec"` + Skills SkillsToolsConfig `json:"skills"` } type SkillsToolsConfig struct { diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index cc6de9399..9e7e07340 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -276,6 +276,7 @@ func DefaultConfig() *Config { Port: 18790, }, Tools: ToolsConfig{ + EnableNotifications: false, Web: WebToolsConfig{ Proxy: "", Brave: BraveConfig{ diff --git a/pkg/tools/base.go b/pkg/tools/base.go index 770d8cb04..f8b8e4499 100644 --- a/pkg/tools/base.go +++ b/pkg/tools/base.go @@ -17,6 +17,12 @@ type ContextualTool interface { SetContext(channel, chatID string) } +// NotificationFormatter allows a tool to generate a custom +// message to be shown to the user before execution. +type NotificationFormatter interface { + FormatNotification(args map[string]any) string +} + // AsyncCallback is a function type that async tools use to notify completion. // When an async tool finishes its work, it calls this callback with the result. // diff --git a/pkg/tools/shell.go b/pkg/tools/shell.go index ad1664b5b..789af663c 100644 --- a/pkg/tools/shell.go +++ b/pkg/tools/shell.go @@ -14,6 +14,7 @@ import ( "time" "github.com/sipeed/picoclaw/pkg/config" + "github.com/sipeed/picoclaw/pkg/utils" ) type ExecTool struct { @@ -134,6 +135,13 @@ func (t *ExecTool) Parameters() map[string]any { } } +func (t *ExecTool) FormatNotification(args map[string]any) string { + if cmd, ok := args["command"].(string); ok { + return fmt.Sprintf("🛠️ Exec: `%s`", utils.Truncate(cmd, 500)) + } + return "🛠️ Shell command execution in progress" +} + func (t *ExecTool) Execute(ctx context.Context, args map[string]any) *ToolResult { command, ok := args["command"].(string) if !ok { diff --git a/pkg/tools/web.go b/pkg/tools/web.go index 44df28215..97aecc2de 100644 --- a/pkg/tools/web.go +++ b/pkg/tools/web.go @@ -471,6 +471,13 @@ func (t *WebSearchTool) Parameters() map[string]any { } } +func (t *WebSearchTool) FormatNotification(args map[string]any) string { + if query, ok := args["query"].(string); ok { + return fmt.Sprintf("🛠️ Web search: \"%s\"", query) + } + return "🛠️ Web search in progress" +} + func (t *WebSearchTool) Execute(ctx context.Context, args map[string]any) *ToolResult { query, ok := args["query"].(string) if !ok { @@ -653,6 +660,13 @@ func (t *WebFetchTool) Execute(ctx context.Context, args map[string]any) *ToolRe } } +func (t *WebFetchTool) FormatNotification(args map[string]any) string { + if u, ok := args["url"].(string); ok { + return fmt.Sprintf("🛠️ Fetching page: \"%s\"", u) + } + return "🛠️ Fetching web page in progress" +} + func (t *WebFetchTool) extractText(htmlContent string) string { re := regexp.MustCompile(``) result := re.ReplaceAllLiteralString(htmlContent, "")