feat(observability): add ToolObserver hook for structured tool tracing#490
Open
SebastianBoehler wants to merge 1 commit intosipeed:mainfrom
Open
feat(observability): add ToolObserver hook for structured tool tracing#490SebastianBoehler wants to merge 1 commit intosipeed:mainfrom
SebastianBoehler wants to merge 1 commit intosipeed:mainfrom
Conversation
nikolasdehor
approved these changes
Feb 19, 2026
nikolasdehor
left a comment
There was a problem hiding this comment.
The observer pattern is well-designed — zero-cost nil check, thread-safe via RLock, clean API. Two notes:
-
Unrelated changes: Removes OneBot transcriber wiring and a duplicate Name field. These should be separate commits or documented in the PR description.
-
Fragile JSON construction: The event JSON is built via
fmt.Sprintfwith%qand raw%s. If argsJSON is malformed, the outer JSON breaks. Safer to usejson.Marshalfor the entire event struct.
LGTM for the core observer pattern.
SebastianBoehler
pushed a commit
to SebastianBoehler/picoclaw
that referenced
this pull request
Feb 19, 2026
- Use json.Marshal for entire WEAVE_TOOL_EVENT struct instead of fragile fmt.Sprintf with %q/%s interpolation; prevents broken JSON if args contain special characters - Restore accidentally removed OneBot transcriber wiring in gatewayCmd - Document removal of duplicate ToolCall.Name field (canonical name lives in Function.Name only)
SebastianBoehler
pushed a commit
to SebastianBoehler/picoclaw
that referenced
this pull request
Feb 19, 2026
…ests
## Problem
When agents call external APIs (e.g. Handelsregister, lead discovery,
internal services), they were forced to use exec+curl. This has two
critical drawbacks:
1. The ToolObserver fires with tool="exec" and args={"command": "curl ..."},
making the actual API call invisible in Weave traces — you see a shell
command string, not a structured HTTP event.
2. Models with weaker instruction-following (e.g. glm-5) misread curl
examples as pseudo-syntax and emit literal strings like
"POST http://..." as shell commands, causing immediate failures.
## Solution
Add http_fetch as a first-class native Go tool alongside web_fetch.
Every http_fetch call surfaces in Weave traces as:
tool="http_fetch", args={url, method, body, headers}
This makes API calls to internal services fully observable — reviewers
can see exactly which endpoint was called, with what payload, and
whether it succeeded, without parsing shell command strings.
## Changes
pkg/tools/http_fetch.go (new)
- HttpFetchTool struct with Name/Description/Parameters/Execute
- Supports GET, POST, PUT, PATCH, DELETE
- Defaults Content-Type to application/json when body is present
- Pretty-prints JSON responses for LLM readability
- Returns IsError=true for HTTP 4xx/5xx so ToolObserver marks them
- 512KB response cap (configurable via NewHttpFetchTool)
- 30s timeout, max 5 redirects
pkg/agent/loop.go
- Register tools.NewHttpFetchTool(512*1024) alongside NewWebFetchTool
## Relationship to PR sipeed#490
PR sipeed#490 added the ToolObserver hook that fires after every tool call.
http_fetch is the first tool specifically designed to exploit that hook:
because it is a named native tool rather than a shell wrapper, every
HTTP API call now appears as a distinct, structured trace event instead
of being buried inside an opaque exec invocation.
lxowalle
reviewed
Feb 22, 2026
Collaborator
lxowalle
left a comment
There was a problem hiding this comment.
- Please submit only one feature per PR.
- The http_fetch and web_fetch tools conflict.
5 tasks
4d6b3c9 to
46194b1
Compare
Author
|
Removed conflicting features |
This was referenced Feb 27, 2026
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":{...}}
46194b1 to
cdc3127
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a lightweight observer pattern to
ToolRegistrythat fires after every tool execution. This enables external observability integrations (W&B Weave, OpenTelemetry, custom metrics) without modifying individual tools.Changes
pkg/tools/registry.goToolObserverfunc type:func(name string, args map[string]interface{}, result *ToolResult, durationMs int64)observerfield toToolRegistrySetObserver()methodExecuteWithContextafter every tool callpkg/agent/loop.goSetToolObserver()toAgentLoop— propagates observer to all agent tool registriescmd/picoclaw/main.goagentCmdwhenPICOCLAW_WEAVE_OBSERVE=1WEAVE_TOOL_EVENT:JSON lines to stderr for downstream consumptionDesign
The observer is a zero-cost abstraction when not set — a single nil check before the call. No allocations, no goroutines.
The
WEAVE_TOOL_EVENT:line format emitted to stderr:This is intentionally transport-agnostic — any process reading the agent's stderr can parse these lines. The W&B Weave integration reads them from the agent subprocess and logs structured traces per run, but the hook itself has no Weave dependency.
Usage
Or via env var for the built-in stderr emitter:
PICOCLAW_WEAVE_OBSERVE=1 picoclaw agent -m "your prompt"