Skip to main content

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

ScenarioGate type
Single LLM call (summarization endpoint, one-shot API)Standard gate
A chatbot — one call per turn, grouped into conversationsAgent gate
A loop or workflow that makes several LLM calls per runAgent gate
Agents that call tools you want on the timelineAgent gate
Different models for different call sites, managed remotelyAgent 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:
TermMeaning
Agent gateThe registered identity of one agent — home of its tasks, traces, and sessions.
TaskA named LLM call site inside the agent (classify, respond, …). The unit of remote control.
TraceOne bounded run of the agent, end to end — including sub-agents, which appear as nested spans in the same trace. W3C trace ids.
SpanOne step inside a trace: an agent span, a generation (LLM call), or a tool execution.
SessionA 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

1

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.
2

Wrap your agent's entry point

One trace per run. Everything inside the scope lands on the same timeline.
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;
  });
}
3

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.

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:
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:
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:
// chatId is created ONCE per conversation and reused for every turn
agent.trace({ conversationId: chatId }, () => handleTurn(message));
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.
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:
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.
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.
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.
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.

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:
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:
// 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:
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:
v1v2
verlon.generateSessionId() + sessionId per callagent.trace({ conversationId }, fn) — ambient within the scope
x-verlon-session-id as the run identifiertraceparent (automatic via instrumentFetch / clientOptions)
verlon.endSession(id)Not needed — a trace ends when its scope exits
Sub-gates attached to an agent gateTasks with a gate binding (dashboard → Tasks → Delegate to a gate)
Modes (observability / orchestrated), model poolsRemoved — 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.