Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@
}
},
"tools": {
"enable_notifications": false,
"web": {
"brave": {
"enabled": false,
Expand Down
22 changes: 22 additions & 0 deletions pkg/agent/loop.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions pkg/config/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ func DefaultConfig() *Config {
Port: 18790,
},
Tools: ToolsConfig{
EnableNotifications: false,
Web: WebToolsConfig{
Proxy: "",
Brave: BraveConfig{
Expand Down
6 changes: 6 additions & 0 deletions pkg/tools/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand Down
8 changes: 8 additions & 0 deletions pkg/tools/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"time"

"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/utils"
)

type ExecTool struct {
Expand Down Expand Up @@ -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 {
Expand Down
14 changes: 14 additions & 0 deletions pkg/tools/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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(`<script[\s\S]*?</script>`)
result := re.ReplaceAllLiteralString(htmlContent, "")
Expand Down