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":{...}}
This commit is contained in:
parent
cb0c8703fb
commit
cdc312726b
3 changed files with 57 additions and 2 deletions
|
|
@ -6,6 +6,7 @@ package main
|
|||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
|
@ -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() {
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
observer ToolObserver
|
||||
}
|
||||
|
||||
func NewToolRegistry() *ToolRegistry {
|
||||
|
|
@ -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()
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue