> ## Documentation Index
> Fetch the complete documentation index at: https://docs.verlon.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent Gates

> Trace every run of your agent — every LLM call, tool execution, and sub-agent — and control each call site's model remotely

## What are Agent Gates?

An agent gate is the identity of one agent in Verlon, the way a standard gate is the identity of one LLM call. It answers two questions:

* **What did my agent do?** Every run of your agent becomes a **trace**: a grouped timeline of its LLM calls, tool executions, and sub-agents, with cost, latency, and coverage accounting.
* **What should each call site run on?** Your agent's LLM call sites become **tasks** you can control from the dashboard: pin a model per call site or delegate a call site to an existing gate — without redeploying code.

Verlon sits in the request path, so LLM calls are captured automatically. Tools and run boundaries take one line each.

### When to use an agent gate

| Scenario                                                    | Gate type     |
| ----------------------------------------------------------- | ------------- |
| Single LLM call (summarization endpoint, one-shot API)      | Standard gate |
| A chatbot — one call per turn, grouped into conversations   | Agent gate    |
| A loop or workflow that makes several LLM calls per run     | Agent gate    |
| Agents that call tools you want on the timeline             | Agent gate    |
| Different models for different call sites, managed remotely | Agent gate    |

A chatbot's traces are legitimately thin — one generation per turn — and that's fine: the session grouping (one row per conversation, turns in order) is what the agent gate buys you there.

## Vocabulary

Agent gates use standard observability terms with their standard meanings:

| Term           | Meaning                                                                                                                                         |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| **Agent gate** | The registered identity of one agent — home of its tasks, traces, and sessions.                                                                 |
| **Task**       | A named LLM call site inside the agent (`classify`, `respond`, …). The unit of remote control.                                                  |
| **Trace**      | One bounded run of the agent, end to end — including sub-agents, which appear as nested spans in the *same* trace. W3C trace ids.               |
| **Span**       | One step inside a trace: an `agent` span, a `generation` (LLM call), or a `tool` execution.                                                     |
| **Session**    | A conversation label on traces — *your* id (chat id, ticket number). Sessions never start, end, or expire; they're a grouping key, not a state. |

Every span also carries **provenance**: `Observed` (captured at the gateway — ground truth), `Reported` (measured by the SDK), or `OTel` (ingested from your own OpenTelemetry instrumentation). Tool calls that only appear in proxied traffic show as `Evidenced` — facts without fabricated measurements.

## Quickstart

<Steps>
  <Step title="Create an agent gate">
    Dashboard → Agent Gates → **Create Agent Gate**. Only a name is required —
    no model configuration. Calls run on whatever model your code requests
    until you pin models per task.
  </Step>

  <Step title="Wrap your agent's entry point">
    One trace per run. Everything inside the scope lands on the same timeline.

    ```typescript theme={null}
    import { Verlon } from '@verlon-ai/sdk';

    const verlon = new Verlon({ apiKey: process.env.VERLON_API_KEY });
    const agent = verlon.agent(process.env.AGENT_GATE_ID);
    const respond = agent.task('respond'); // one task per LLM call site

    export function handleMessage(chatId: string, message: string) {
      return agent.trace({ conversationId: chatId }, async () => {
        const reply = await respond.chat({
          data: { messages: [{ role: 'user', content: message }] },
        });
        return reply.content;
      });
    }
    ```
  </Step>

  <Step title="Open the gate">
    The gate's **Traces** tab shows every run; click one for the timeline. The
    **Tasks** tab shows your call sites, ready to control.
  </Step>
</Steps>

## Using native provider SDKs

If your agent is written with the OpenAI or Anthropic SDK, keep it. Spread `task.clientOptions()` into the client constructor — it carries the Verlon base URL, a trace-aware `fetch`, and the call site's identity headers:

```typescript theme={null}
import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai';
import { Verlon } from '@verlon-ai/sdk';

const verlon = new Verlon({ apiKey: process.env.VERLON_API_KEY });
const agent = verlon.agent(process.env.AGENT_GATE_ID);

const classify = agent.task('classify');
const respond = agent.task('respond');

// One client per call site — then your agent code is untouched vanilla SDK usage.
// The provider hint sets the right base path for each SDK's URL conventions.
const classifier = new Anthropic({
  apiKey: process.env.VERLON_API_KEY,
  ...classify.clientOptions('anthropic'),
});
const responder = new OpenAI({
  apiKey: process.env.VERLON_API_KEY,
  ...respond.clientOptions('openai'),
});
```

Inside a trace scope, every call these clients make joins the run automatically — no per-call code.

## Tools on the timeline

Two capture modes, and you usually get the first one for free:

**Evidenced (zero setup).** When the model requests a tool and your loop feeds the result back, both directions pass through Verlon. The timeline reconstructs the execution — name, arguments, result — marked `Evidenced` because the execution itself wasn't observed (so no made-up durations).

**Reported (one line per tool).** Wrap a function and every execution becomes a measured tool span — exact duration, error capture, visibility for tools the model never sees:

```typescript theme={null}
const lookupOrder = verlon.tool('lookup_order', async (orderId: string) => {
  return db.orders.find(orderId);
});
```

Wrapped tools behave identically outside a trace scope (pure pass-through), record errors and rethrow them unchanged, and support a `redact` option for sensitive arguments.

## Sessions: multi-turn conversations

`conversationId` is exactly what the name says — the id of the **conversation**, not the message. Each user message becomes one trace; the shared conversation id is what groups those traces into a session:

```typescript theme={null}
// chatId is created ONCE per conversation and reused for every turn
agent.trace({ conversationId: chatId }, () => handleTurn(message));
```

<Warning>
  Don't generate a fresh `conversationId` per message (`crypto.randomUUID()` inside
  the message handler is the classic mistake). That creates a one-trace session
  for every message and grouping never happens. Pass the id your app already
  has for the conversation — the chat id, thread id, or ticket number. If your
  Sessions tab shows exactly one trace in every session, this is why.
</Warning>

A session is only a label — a chat resumed after a month just keeps grouping, with no lifecycle to manage. The gate's **Sessions** tab shows one row per label with trace counts and cumulative cost. Click a session to read the conversation forward, turn by turn.

### In the trace, or beside it?

A trace is the work that produced one run's output. When a run triggers side work that doesn't shape its output — generating a chat title, indexing the exchange, firing a notification — give that work its **own trace in the same conversation, linked to the run that caused it**:

```typescript theme={null}
let turnParent = '';
await agent.trace({ conversationId: chatId }, async (t) => {
  turnParent = t.traceparent();
  return respondToUser(message);
});

// Triggered side work: a sibling trace in the same session,
// linked back to the turn that spawned it.
await agent.trace(
  { conversationId: chatId, links: [{ traceparent: turnParent }] },
  () => generateTitle(chatId)
);
```

The rule: **if the work contributes to the run's output, it's a span inside the trace; if the run merely triggers it, it's a sibling trace with a link.** Keeping side work out of the run's trace keeps the run's duration and cost honest; the link keeps the causation visible — the run's page shows "spawned →", the side work's page shows "triggered by →", and the session view draws the edge between the siblings.

A link is a reference, never parentage: durations, costs, and coverage never aggregate across it (to *join* a trace instead, pass `traceparent`). Links to runs that were never observed are kept and shown as "triggered by an unobserved run" — same honesty rule as coverage. If your own OpenTelemetry instrumentation emits span links, they're ingested as the same edges automatically.

## Tasks: remote control per call site

Tasks are how the dashboard controls your agent's models. Three states:

1. **Unconfigured (default).** A task with no config is pass-through: the call runs whatever model your code requested. Control is never required.
2. **Pinned.** Set a model on the task and every call through it runs that model — changed from the dashboard, live, no redeploy.
3. **Delegated.** Bind the task to an existing standard gate and calls inherit that gate's model, parameters, and managed instructions. Tune "how to summarize" once on a `summarize` gate; every agent that delegates to it follows.

<Note>
  A model must resolve from *somewhere* on every call. The chain, in
  order: **task pin or delegation → the model your code requested → the
  gate's own model**. An agent gate has no model of its own, so omit
  `model` in `task.chat()` calls when the dashboard should decide, and
  pass one when the code should own the choice (native provider SDKs
  always pass one — that's the pass-through value a pin overrides). If
  nothing in the chain resolves, the call fails with a clear 400 —
  hardcoding a model into the gate to avoid that error defeats the
  point of per-task control.
</Note>

You never have to pre-register tasks. Call sites seen in traffic appear on the **Tasks** tab automatically as claimable rows — the loop is *see traffic → name it → control it*. Typo'd names are flagged when they sit next to a similarly-named configured task.

<Note>
  The SDK validates your declared task names against the gate when the first
  trace starts and warns on mismatches (set `strictTasks: true` in the client
  config to throw instead). A typo'd task name is never an error at request
  time — the call passes through and the name shows up as unclaimed traffic.
</Note>

## Sub-agents

An agent calling another agent nests naturally — open the sub-agent's trace scope inside the parent's and it becomes an agent span in the *same* trace:

```typescript theme={null}
const researcher = verlon.agent(RESEARCHER_GATE_ID);

await orchestrator.trace({ conversationId }, async () => {
  // ...orchestrator calls...
  await researcher.trace(async () => {
    // these calls nest under the researcher's agent span
  });
});
```

Recursive agents are safe: each invocation is a new span, so a run always renders as a finite tree.

## Crossing process boundaries

Trace context propagates automatically within a process. Across processes or services, pass the standard W3C `traceparent` and resume:

```typescript theme={null}
// producer
queue.send({ job, traceparent: t.traceparent() });

// consumer — continues the SAME trace
agent.trace({ traceparent: job.traceparent }, () => processJob(job));
```

If your services already propagate `traceparent` (most tracing middleware does), Verlon rides it as-is.

## OpenTelemetry ingestion

Already instrumented? Point any OTLP exporter at Verlon and your spans land in the same traces:

```bash theme={null}
OTEL_EXPORTER_OTLP_ENDPOINT=https://api.verlon.ai/otlp
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer $VERLON_API_KEY"
```

The endpoint accepts OTLP/HTTP (protobuf and JSON) and normalizes the common GenAI dialects — the official OTel `gen_ai` conventions, OpenInference, and OpenLLMetry — so framework instrumentation (Vercel AI SDK telemetry, LangChain instrumentors, the OpenAI Agents SDK via an OTel processor) works without changes. Attributes we don't recognize are preserved verbatim on the span.

## Cost alerts

Per-gate thresholds under **Settings → Cost alerts**, both alert-only — a fired alert notifies you and never interrupts a run:

* **Per-trace cost** — one run exceeded the threshold (fires once per trace).
* **Per-session cumulative cost** — one conversation's traces add up past the threshold (fires at most once per day per session).

## Coverage: what the timeline doesn't know

Agent gates never present a partial picture as a complete one. Each trace shows a coverage strip: the share of wall-clock covered by observed work (computed correctly under parallelism), unobserved gaps labeled inline, evidenced-only executions counted, and **verified span loss** — the SDK counts anything it had to drop, and sequence gaps are detected server-side, so "0 lost" is a checked claim.

Two boundary rules make the math predictable:

* **The trace's wall-clock is the `trace()` scope's lifetime** — scope entry to scope exit, not first span to last span. Work you do inside the scope that isn't instrumented (a database write before the first model call, post-processing after the last) shows up as an unobserved gap. That's deliberate: the gap is the honest record that something happened the timeline can't account for. Wrap it in `verlon.tool()` if you want it measured, or move it outside the scope if it isn't part of the run.
* **The scope itself doesn't count as observation.** Coverage measures generations and tools, so a trace whose LLM calls bypassed Verlon reads as 0% covered — not 100% because the run boundary technically spans everything.

## Migrating from v1 agent gates

The v1 session system (SDK 1.x) was replaced end to end. If you integrated against it:

| v1                                                    | v2                                                                     |
| ----------------------------------------------------- | ---------------------------------------------------------------------- |
| `verlon.generateSessionId()` + `sessionId` per call   | `agent.trace({ conversationId }, fn)` — ambient within the scope       |
| `x-verlon-session-id` as the run identifier           | `traceparent` (automatic via `instrumentFetch` / `clientOptions`)      |
| `verlon.endSession(id)`                               | Not needed — a trace ends when its scope exits                         |
| Sub-gates attached to an agent gate                   | Tasks with a gate binding (dashboard → Tasks → Delegate to a gate)     |
| Modes (`observability` / `orchestrated`), model pools | Removed — everything observes; per-task control replaces orchestration |

`x-verlon-session-id` still works as a legacy alias for `x-verlon-conversation-id` — both carry the session *label* (your conversation id) rather than identifying a run.
