Observability for Durable Agent Harnesses
Agent Runs shows each session of a durable agent in the Edgenivo dashboard, with its turns, every model and tool call, timing, and token usage. durable-harness sends it a trace with agentRuns(), the same integration eve uses. This page applies to the guides for Pi, with either variant, fx, OpenCode, Claude Code, and Codex.
Here's a Pi session in Agent Runs. Select a span to see its timing, tokens, input, and output, and open the Conversation and Metadata tabs:
A Pi session in Agent Runs, with made-up values. Its second model call was interrupted when its function died, and ran again on a new function.
Agent Runs usage is billed at Always-on Tracing rates during beta.
durable-harnessrecords traces with@vercel/agent-runsand sends them with OpenTelemetry's SDK. It loads them only for Agent Runs. Install the versions it's tested with:npm i @vercel/[email protected] @opentelemetry/[email protected] @opentelemetry/[email protected] @opentelemetry/[email protected]Without them,
agentRuns()throws with this list. Agent Runs needs Node.js 24. The rest ofdurable-harnessruns on Node.js 22 or later.Declare the integration in
agent-runs.ts:agent-runs.tsimport { agentRuns } from 'durable-harness/instrumentation/otel'; export default agentRuns();A public session's trace records its model and tool inputs and outputs, and any other session's records only metadata, such as its models, tools, timing, and token usage. To change that, see Control what Agent Runs receives.
Importing the module makes no network requests, and it doesn't replace your app's global OpenTelemetry provider.
Call
register()once per process, before the agent runs a turn. On Next.js, call it frominstrumentation.ts. With Edgenivo Functions inapi/, call it in the module that makes your agent, so every function that runs turns registers it:instrumentation.tsexport async function register() { if (process.env.NEXT_RUNTIME !== 'nodejs') return; const { default: runs } = await import('./agent-runs'); runs.register(); }lib/agent.tsimport { pi } from 'durable-harness/pi'; import runs from '../agent-runs.js'; runs.register(); // once per process, before the agent runs a turn export const agent = pi({ model: 'anthropic/claude-sonnet-5.5' });For an fx agent, also pass
agentRunsHooks()fromdurable-harness/fxtocreateFxAgent, and make sureregister()runs before you create the agent. libfx checks for Agent Runs once, when you create the agent, so an agent created beforeregister()records only metadata, even for public sessions.On Next.js,
instrumentation.tsalready runs before your modules. With Edgenivo Functions inapi/, callregister()abovecreateFxAgent:lib/agent.tsimport { createFxAgent } from 'libfx'; import { agentRunsHooks } from 'durable-harness/fx'; import { tools } from './tools'; // your tools, each with idempotent set export const agent = createFxAgent({ model: 'anthropic/claude-sonnet-5.5', tools, hooks: agentRunsHooks() });lib/agent.tsimport { createFxAgent } from 'libfx'; import { agentRunsHooks } from 'durable-harness/fx'; import runs from '../agent-runs.js'; import { tools } from './tools.js'; // your tools, each with idempotent set runs.register(); // before the agent is created export const agent = createFxAgent({ model: 'anthropic/claude-sonnet-5.5', tools, hooks: agentRunsHooks() });For an fx agent with its own World, as in
durability: world(sessions), pass that World toagentRunsHooks({ world: sessions })too.Edgenivo collects traces only for an environment with a sampling rule. Open Settings → Tracing for your project:
- Select Add Sampling Rule.
- Choose All Environments, Production, or Preview.
- Set Rate to
100%, so a trace holds every span of its session. - Select Save.
You can also manage sampling rules with
vercel traces config, in Edgenivo CLI 62.5 or later. Without a sampling rule, Agent Runs still lists each session, but without its trace, model, or token usage.vercel deploySend a message to the chat route, then open Observability → Agent Runs and select the session.
The list shows each session's status and token usage. A session whose turn has finished shows as Waiting, ready for its next message. The session's page shows the model, timing, model and tool call spans, and token usage. By default, authenticated sessions have an unknown audience, so their traces don't record titles, messages, replies, or tool inputs and outputs. To capture content intentionally, see Control what Agent Runs receives.
Each session is one trace, across every function that drives it:
- Each turn is an
invoke_agentspan, with anagent.stepspan for each step and the model and tool calls nested in it. A turn that continues in a later function, after a crash or when its function runs out of time, stays one turn. - Model calls carry token usage, including cached input when the provider reports it, and cost when the provider supplies it.
- A turn that resumes after a crash reads its finished calls back from the log, so they don't appear twice. A function that finds nothing to do adds no spans.
A session's audience decides what its trace records. A public session's trace records its title and its model and tool inputs and outputs. A private or unknown session's trace records only metadata, and the content it leaves out is never recorded, saved, or exported. A session's audience comes from:
-
The chat route. A route with
authorize: 'public'makes its sessions public. Any other route leaves them unknown. -
The agent. Set
traceAudienceto'public'or'private'to decide for every session of the agent:lib/agent.tsexport const agent = pi({ model: 'anthropic/claude-sonnet-5.5', traceAudience: 'private' }); -
The storage, with
durable-harness/pi-durable. PasstraceAudiencetoopenWorldStoragewhen it starts a session. The session keeps it, including in the turns the queue route resumes. On a session that exists, it applies to that storage only.resumeWith(open, { traceAudience })sets it for the turns the queue route drives in a session that has none. -
The fx agent.
agentRunsHooks({ audience })sets it for every session of that agent. -
The integration.
agentRuns({ audience })sets the audience of sessions that have none.
To change what an audience records, pass capture to agentRuns(). It gets a session's audience and returns { emit, recordInputs, recordOutputs }. This one keeps the default for public and private sessions, and sends no trace for a session whose audience is unknown:
import { agentRuns } from 'durable-harness/instrumentation/otel';
export default agentRuns({
capture: (audience) => ({
emit: audience !== 'unknown', // no trace at all
recordInputs: audience === 'public',
recordOutputs: audience === 'public',
}),
});To send the same traces to another OpenTelemetry destination, pass its span processors as spanProcessors. They get the same spans, with the same content. A tracing failure never fails a turn. It's logged as a warning, or passed to onError. In a script or a test, call forceFlush() before the process exits.
- Claude Code and Codex run in separate processes in a Sandbox. OpenCode runs an embedded server in your function. Model request and response capture for these harnesses has size limits; larger content is left out or truncated.
- A subagent's calls don't show as a run of their own.
- Agent Runs is shared with eve, and some of its views may not list
durable-harnesssessions yet, even when their traces are recorded. Open the session from the table of runs.
Was this helpful?