Skip to content

Commit 46194b1

Browse files
author
Sebastian Boehler
committed
feat(observability): add ToolObserver hook for structured tool tracing
Add a lightweight observer pattern to ToolRegistry that fires after every tool execution. This enables external observability integrations (Weave, OpenTelemetry, custom metrics) without modifying individual tools. Changes: - pkg/tools/registry.go: Add ToolObserver func type, observer field to ToolRegistry, SetObserver() method, and fire observer in ExecuteWithContext after every tool call with (name, args, result, durationMs) - pkg/agent/loop.go: Add SetToolObserver() to AgentLoop, propagating the observer to all agent tool registries - cmd/picoclaw/cmd_agent.go: Wire observer in agentCmd when env var PICOCLAW_WEAVE_OBSERVE=1 — emits WEAVE_TOOL_EVENT: JSON lines to stderr for downstream consumption by observability backends The observer is a zero-cost abstraction when not set (nil check before call). The WEAVE_TOOL_EVENT: line format is: WEAVE_TOOL_EVENT:{"tool":"exec","duration_ms":42,"is_error":false,"args":{...}}
1 parent cb0c870 commit 46194b1

File tree

3 files changed

+57
-2
lines changed

3 files changed

+57
-2
lines changed

cmd/picoclaw/cmd_agent.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package main
66
import (
77
"bufio"
88
"context"
9+
"encoding/json"
910
"fmt"
1011
"io"
1112
"os"
@@ -18,6 +19,7 @@ import (
1819
"github.com/sipeed/picoclaw/pkg/bus"
1920
"github.com/sipeed/picoclaw/pkg/logger"
2021
"github.com/sipeed/picoclaw/pkg/providers"
22+
"github.com/sipeed/picoclaw/pkg/tools"
2123
)
2224

2325
func agentCmd() {
@@ -72,6 +74,28 @@ func agentCmd() {
7274
msgBus := bus.NewMessageBus()
7375
agentLoop := agent.NewAgentLoop(cfg, msgBus, provider)
7476

77+
// If observability mode is enabled, emit structured tool events to stderr.
78+
// Python gateway reads these lines and logs them to Weave.
79+
if os.Getenv("PICOCLAW_WEAVE_OBSERVE") == "1" {
80+
agentLoop.SetToolObserver(func(name string, args map[string]interface{}, result *tools.ToolResult, durationMs int64) {
81+
evt, err := json.Marshal(struct {
82+
Tool string `json:"tool"`
83+
DurationMs int64 `json:"duration_ms"`
84+
IsError bool `json:"is_error"`
85+
Args map[string]interface{} `json:"args"`
86+
}{
87+
Tool: name,
88+
DurationMs: durationMs,
89+
IsError: result.IsError,
90+
Args: args,
91+
})
92+
if err != nil {
93+
return
94+
}
95+
fmt.Fprintf(os.Stderr, "WEAVE_TOOL_EVENT:%s\n", evt)
96+
})
97+
}
98+
7599
// Print agent startup info (only for interactive mode)
76100
startupInfo := agentLoop.GetStartupInfo()
77101
logger.InfoCF("agent", "Agent initialized",

pkg/agent/loop.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,16 @@ func (al *AgentLoop) RegisterTool(tool tools.Tool) {
208208
}
209209
}
210210

211+
// SetToolObserver registers a ToolObserver on every agent's tool registry.
212+
// The observer is called after every tool execution with name, args, result, and duration.
213+
func (al *AgentLoop) SetToolObserver(obs tools.ToolObserver) {
214+
for _, agentID := range al.registry.ListAgentIDs() {
215+
if agent, ok := al.registry.GetAgent(agentID); ok {
216+
agent.Tools.SetObserver(obs)
217+
}
218+
}
219+
}
220+
211221
func (al *AgentLoop) SetChannelManager(cm *channels.Manager) {
212222
al.channelManager = cm
213223
}

pkg/tools/registry.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,15 @@ import (
1010
"github.com/sipeed/picoclaw/pkg/providers"
1111
)
1212

13+
// ToolObserver is called after every tool execution with structured event data.
14+
// Implement this to add observability (tracing, metrics, logging) without
15+
// modifying individual tools.
16+
type ToolObserver func(name string, args map[string]interface{}, result *ToolResult, durationMs int64)
17+
1318
type ToolRegistry struct {
14-
tools map[string]Tool
15-
mu sync.RWMutex
19+
tools map[string]Tool
20+
mu sync.RWMutex
21+
observer ToolObserver
1622
}
1723

1824
func NewToolRegistry() *ToolRegistry {
@@ -21,6 +27,13 @@ func NewToolRegistry() *ToolRegistry {
2127
}
2228
}
2329

30+
// SetObserver registers a callback invoked after every tool execution.
31+
func (r *ToolRegistry) SetObserver(obs ToolObserver) {
32+
r.mu.Lock()
33+
defer r.mu.Unlock()
34+
r.observer = obs
35+
}
36+
2437
func (r *ToolRegistry) Register(tool Tool) {
2538
r.mu.Lock()
2639
defer r.mu.Unlock()
@@ -81,6 +94,14 @@ func (r *ToolRegistry) ExecuteWithContext(
8194
result := tool.Execute(ctx, args)
8295
duration := time.Since(start)
8396

97+
// Fire observer if registered
98+
r.mu.RLock()
99+
obs := r.observer
100+
r.mu.RUnlock()
101+
if obs != nil {
102+
obs(name, args, result, duration.Milliseconds())
103+
}
104+
84105
// Log based on result type
85106
if result.IsError {
86107
logger.ErrorCF("tool", "Tool execution failed",

0 commit comments

Comments
 (0)