Skip to content

Observability — metrics & Prometheus

You can’t run an agent in production you can’t see. toolnexus instruments the loop once and gives you two outputs from it — both opt-in, both zero-cost when unused.

Pass a sink and it receives a readable, semantic record at each significant point: one llm event per model call, one tool event per tool call, and one terminal run event per run/ask. Forward it to statsd, structured logs, or OpenTelemetry — the library holds no opinion.

const client = createClient({
baseUrl, style: "openai", model,
onMetric: (ev) => {
if (ev.event === "run") console.log(`run: ${ev.turns} turns, ${ev.totalTokens} tokens, ${ev.ms}ms`)
},
})

The three event shapes: { event: "llm", model, status, ms, promptTokens, completionTokens }, { event: "tool", tool, source, isError, ms, pending? }, and { event: "run", model, turns, toolCalls, totalTokens, ms, error? } — names idiomatic per port (snake_case in Python and Elixir; Clojure emits plain maps keyed :event / :prompt_tokens / :total_tokens).

The examples above build a client directly. When you run sub-agents the runtime builds that client for you, so onMetric is set on the runtime — covering every agent — or on an individual agent, which replaces the runtime-wide sink for that agent alone. Setting it per agent is how you attribute events to the agent that produced them.

The events are identical on both paths: same names, same fields. The runtime forwards the sink verbatim and adds, renames, drops, buffers, reorders and aggregates nothing.

// every agent in this runtime reports to one sink…
const rt = new agents.AgentRuntime({ registry, onMetric: ev => track(ev) })
// …or give one agent its own, and it wins for that agent only
const researcher = agents.agent("researcher", {
does: "digs through long documents",
onMetric: ev => track({ ...ev, agent: "researcher" }),
})

hooks resolves the same way and independently — an agent may override one and inherit the other. See Sub-agents → hooks & metrics.

The same events feed a tiny in-memory registry that renders the Prometheus text exposition format — no third-party dependency. Mount it at GET /metrics:

// in your HTTP handler:
res.setHeader("Content-Type", "text/plain")
res.end(client.metrics()) // client.Metrics() in Go / Java / C#

It exposes toolnexus_llm_requests_total, toolnexus_llm_tokens_total, toolnexus_tool_calls_total, plus the toolnexus_llm_request_duration_seconds and toolnexus_tool_duration_seconds histograms.

A suspension is never counted as a tool error: its tool event carries isError: false and a pending: true marker, so error-rate metrics and circuit-breakers don’t trip on a human wait (see Suspension).