Skip to content
Docs
 

Observability for Durable Agent Harnesses

Agent Runs are available in Beta on all plans

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:

020s40s1m1m 20s
pi
6.13s
agent.step
1.58s
chat anthropic/claude-opus-5.5-fast
1.57s
agent.step
27ms
write_file
26ms
agent.step
1ms
list_files
1ms
agent.step
1ms
read_file
1ms
agent.step
2.13s
chat anthropic/claude-opus-5.5-fast
720ms
chat anthropic/claude-opus-5.5-fast
1.18s
agent.step
1ms
edit_file
1ms
agent.step
0ms
read_file
0ms
agent.step
1.34s
chat anthropic/claude-opus-5.5-fast
1.33s
pi
4.21s
agent.step
1.65s
chat anthropic/claude-opus-5.5-fast
1.64s
agent.step
2ms
edit_file
2ms
agent.step
1.2s
chat anthropic/claude-opus-5.5-fast
1.2s
agent.step
1ms
read_file
1ms
agent.step
1.32s
chat anthropic/claude-opus-5.5-fast
1.31s

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.

  1. durable-harness records traces with @vercel/agent-runs and sends them with OpenTelemetry's SDK. It loads them only for Agent Runs. Install the versions it's tested with:

    npm i @vercel/agent-runs@0.0.1 @opentelemetry/sdk-trace-base@2.2.0 @opentelemetry/resources@2.2.0 @opentelemetry/otlp-transformer@0.208.0

    Without them, agentRuns() throws with this list. Agent Runs needs Node.js 24. The rest of durable-harness runs on Node.js 22 or later.

  2. Declare the integration in agent-runs.ts:

    agent-runs.ts
    import { 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.

  3. Call register() once per process, before the agent runs a turn. On Next.js, call it from instrumentation.ts. With Edgenivo Functions in api/, call it in the module that makes your agent, so every function that runs turns registers it:

    instrumentation.ts
    export async function register() {
      if (process.env.NEXT_RUNTIME !== 'nodejs') return;
      const { default: runs } = await import('./agent-runs');
      runs.register();
    }
    lib/agent.ts
    import { 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() from durable-harness/fx to createFxAgent, and make sure register() runs before you create the agent. libfx checks for Agent Runs once, when you create the agent, so an agent created before register() records only metadata, even for public sessions.

    On Next.js, instrumentation.ts already runs before your modules. With Edgenivo Functions in api/, call register() above createFxAgent:

    lib/agent.ts
    import { 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.ts
    import { 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 to agentRunsHooks({ world: sessions }) too.

  4. Edgenivo collects traces only for an environment with a sampling rule. Open Settings → Tracing for your project:

    1. Select Add Sampling Rule.
    2. Choose All Environments, Production, or Preview.
    3. Set Rate to 100%, so a trace holds every span of its session.
    4. 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.

  5. vercel deploy

    Send 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_agent span, with an agent.step span 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 traceAudience to 'public' or 'private' to decide for every session of the agent:

    lib/agent.ts
    export const agent = pi({ model: 'anthropic/claude-sonnet-5.5', traceAudience: 'private' });
  • The storage, with durable-harness/pi-durable. Pass traceAudience to openWorldStorage when 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:

agent-runs.ts
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',
  }),
});

A public audience shows the session's model and tool content to every project member who can read traces. If your agent processes personal, sensitive, or regulated data, you may need to disclose this capture under applicable laws and in your privacy materials.

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-harness sessions yet, even when their traces are recorded. Open the session from the table of runs.

Was this helpful?

supported.