feat(tool) show tool usage via channel

This commit is contained in:
afjcjsbx 2026-02-25 19:41:07 +01:00
parent 094d65916d
commit eca4ede77e
7 changed files with 57 additions and 4 deletions

View file

@ -203,6 +203,7 @@
} }
}, },
"tools": { "tools": {
"enable_notifications": false,
"web": { "web": {
"brave": { "brave": {
"enabled": false, "enabled": false,

View file

@ -668,6 +668,28 @@ func (al *AgentLoop) runLLMIteration(
"iteration": iteration, "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 // Create async callback for tools that implement AsyncTool
// NOTE: Following openclaw's design, async tools do NOT send results directly to users. // 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 // Instead, they notify the agent via PublishInbound, and the agent decides

View file

@ -468,6 +468,7 @@ type ExecConfig struct {
} }
type ToolsConfig struct { type ToolsConfig struct {
EnableNotifications bool `json:"enable_notifications" env:"PICOCLAW_TOOLS_ENABLE_NOTIFICATIONS"`
Web WebToolsConfig `json:"web"` Web WebToolsConfig `json:"web"`
Cron CronToolsConfig `json:"cron"` Cron CronToolsConfig `json:"cron"`
Exec ExecConfig `json:"exec"` Exec ExecConfig `json:"exec"`

View file

@ -276,6 +276,7 @@ func DefaultConfig() *Config {
Port: 18790, Port: 18790,
}, },
Tools: ToolsConfig{ Tools: ToolsConfig{
EnableNotifications: false,
Web: WebToolsConfig{ Web: WebToolsConfig{
Proxy: "", Proxy: "",
Brave: BraveConfig{ Brave: BraveConfig{

View file

@ -17,6 +17,12 @@ type ContextualTool interface {
SetContext(channel, chatID string) 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. // 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. // When an async tool finishes its work, it calls this callback with the result.
// //

View file

@ -14,6 +14,7 @@ import (
"time" "time"
"github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/utils"
) )
type ExecTool struct { 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 { func (t *ExecTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
command, ok := args["command"].(string) command, ok := args["command"].(string)
if !ok { if !ok {

View file

@ -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 { func (t *WebSearchTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
query, ok := args["query"].(string) query, ok := args["query"].(string)
if !ok { 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 { func (t *WebFetchTool) extractText(htmlContent string) string {
re := regexp.MustCompile(`<script[\s\S]*?</script>`) re := regexp.MustCompile(`<script[\s\S]*?</script>`)
result := re.ReplaceAllLiteralString(htmlContent, "") result := re.ReplaceAllLiteralString(htmlContent, "")