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
24 changes: 24 additions & 0 deletions cmd/picoclaw/cmd_agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package main
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"os"
Expand All @@ -18,6 +19,7 @@ import (
"github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/logger"
"github.com/sipeed/picoclaw/pkg/providers"
"github.com/sipeed/picoclaw/pkg/tools"
)

func agentCmd() {
Expand Down Expand Up @@ -72,6 +74,28 @@ func agentCmd() {
msgBus := bus.NewMessageBus()
agentLoop := agent.NewAgentLoop(cfg, msgBus, provider)

// If observability mode is enabled, emit structured tool events to stderr.
// Python gateway reads these lines and logs them to Weave.
if os.Getenv("PICOCLAW_WEAVE_OBSERVE") == "1" {
agentLoop.SetToolObserver(func(name string, args map[string]interface{}, result *tools.ToolResult, durationMs int64) {
evt, err := json.Marshal(struct {
Tool string `json:"tool"`
DurationMs int64 `json:"duration_ms"`
IsError bool `json:"is_error"`
Args map[string]interface{} `json:"args"`
}{
Tool: name,
DurationMs: durationMs,
IsError: result.IsError,
Args: args,
})
if err != nil {
return
}
fmt.Fprintf(os.Stderr, "WEAVE_TOOL_EVENT:%s\n", evt)
})
}

// Print agent startup info (only for interactive mode)
startupInfo := agentLoop.GetStartupInfo()
logger.InfoCF("agent", "Agent initialized",
Expand Down
10 changes: 10 additions & 0 deletions pkg/agent/loop.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,16 @@ func (al *AgentLoop) RegisterTool(tool tools.Tool) {
}
}

// SetToolObserver registers a ToolObserver on every agent's tool registry.
// The observer is called after every tool execution with name, args, result, and duration.
func (al *AgentLoop) SetToolObserver(obs tools.ToolObserver) {
for _, agentID := range al.registry.ListAgentIDs() {
if agent, ok := al.registry.GetAgent(agentID); ok {
agent.Tools.SetObserver(obs)
}
}
}

func (al *AgentLoop) SetChannelManager(cm *channels.Manager) {
al.channelManager = cm
}
Expand Down
25 changes: 23 additions & 2 deletions pkg/tools/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@ import (
"github.com/sipeed/picoclaw/pkg/providers"
)

// ToolObserver is called after every tool execution with structured event data.
// Implement this to add observability (tracing, metrics, logging) without
// modifying individual tools.
type ToolObserver func(name string, args map[string]interface{}, result *ToolResult, durationMs int64)

type ToolRegistry struct {
tools map[string]Tool
mu sync.RWMutex
tools map[string]Tool
mu sync.RWMutex
observer ToolObserver
}

func NewToolRegistry() *ToolRegistry {
Expand All @@ -21,6 +27,13 @@ func NewToolRegistry() *ToolRegistry {
}
}

// SetObserver registers a callback invoked after every tool execution.
func (r *ToolRegistry) SetObserver(obs ToolObserver) {
r.mu.Lock()
defer r.mu.Unlock()
r.observer = obs
}

func (r *ToolRegistry) Register(tool Tool) {
r.mu.Lock()
defer r.mu.Unlock()
Expand Down Expand Up @@ -81,6 +94,14 @@ func (r *ToolRegistry) ExecuteWithContext(
result := tool.Execute(ctx, args)
duration := time.Since(start)

// Fire observer if registered
r.mu.RLock()
obs := r.observer
r.mu.RUnlock()
if obs != nil {
obs(name, args, result, duration.Milliseconds())
}

// Log based on result type
if result.IsError {
logger.ErrorCF("tool", "Tool execution failed",
Expand Down