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 (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -18,6 +19,7 @@ import (
|
||||||
"github.com/sipeed/picoclaw/pkg/bus"
|
"github.com/sipeed/picoclaw/pkg/bus"
|
||||||
"github.com/sipeed/picoclaw/pkg/logger"
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
"github.com/sipeed/picoclaw/pkg/providers"
|
"github.com/sipeed/picoclaw/pkg/providers"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
func agentCmd() {
|
func agentCmd() {
|
||||||
|
|
@ -72,6 +74,28 @@ func agentCmd() {
|
||||||
msgBus := bus.NewMessageBus()
|
msgBus := bus.NewMessageBus()
|
||||||
agentLoop := agent.NewAgentLoop(cfg, msgBus, provider)
|
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)
|
// Print agent startup info (only for interactive mode)
|
||||||
startupInfo := agentLoop.GetStartupInfo()
|
startupInfo := agentLoop.GetStartupInfo()
|
||||||
logger.InfoCF("agent", "Agent initialized",
|
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) {
|
func (al *AgentLoop) SetChannelManager(cm *channels.Manager) {
|
||||||
al.channelManager = cm
|
al.channelManager = cm
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,15 @@ import (
|
||||||
"github.com/sipeed/picoclaw/pkg/providers"
|
"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 {
|
type ToolRegistry struct {
|
||||||
tools map[string]Tool
|
tools map[string]Tool
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
|
observer ToolObserver
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewToolRegistry() *ToolRegistry {
|
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) {
|
func (r *ToolRegistry) Register(tool Tool) {
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
|
|
@ -81,6 +94,14 @@ func (r *ToolRegistry) ExecuteWithContext(
|
||||||
result := tool.Execute(ctx, args)
|
result := tool.Execute(ctx, args)
|
||||||
duration := time.Since(start)
|
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
|
// Log based on result type
|
||||||
if result.IsError {
|
if result.IsError {
|
||||||
logger.ErrorCF("tool", "Tool execution failed",
|
logger.ErrorCF("tool", "Tool execution failed",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue