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

# Instrumenting your agent

> The three instrumentation modes of @glassray/tracing, the llm/tool/span helpers, sessions, usage capture, and error recording.

The SDK gives you three ways to open a trace. All three produce the same thing — a trace with nested spans — so pick whichever fits the shape of your code. Within a trace, the `t.llm` / `t.tool` / `t.span` helpers record the steps.

## Mode 1 — callback-scoped (the default path)

Wrap a run in `glassray.trace(name, meta?, fn)`. The callback receives a trace handle `t`; the return value becomes the trace's output, and a throw is recorded as an error (the trace still sends).

```ts theme={null}
const out = await glassray.trace(
  "handle-ticket",
  { customer: "acme-corp", sessionId: conversationId },
  async (t) => {
    const plan = await t.llm("plan", { model: "claude-opus-4-8", provider: "anthropic" }, () =>
      anthropic.messages.create(req),
    );
    const docs = await t.tool("search-kb", () => searchKb(plan));
    return await t.llm("answer", { model: "claude-opus-4-8" }, () => answer(docs));
  },
);
```

The optional `meta` object takes `{ customer?, sessionId?, flow?, traceId?, environment? }` — see [Metadata](/sdk-metadata) for what each field does. `environment` is accepted for backward compatibility but **ignored since 0.1.3** (the ingest key selects the project).

## Mode 2 — wrap once, trace every call

For an agent with a single entry function, wrap it once and every invocation becomes a trace:

```ts theme={null}
const handleTicket = glassray.wrap(handleTicketImpl, { name: "handle-ticket", kind: "agent" });
```

Arguments are captured as the trace's input and the return value as its output (best-effort, subject to the [privacy controls](/sdk-privacy)).

## Mode 3 — manual lifecycle

When a run doesn't fit inside one callback — batch pipelines, event-driven steps — drive the handles yourself:

```ts theme={null}
const t = glassray.startTrace("batch-run", { customer: "acme-corp" });

const s = t.startSpan("classify", { kind: "llm" });
s.setInput(prompt);
s.setOutput(completion);
s.setUsage({ inputTokens, outputTokens });
s.end();

t.end({ output: summary });
```

Span setters: `s.setInput(x)`, `s.setOutput(y)`, `s.setUsage({ inputTokens, outputTokens })`, `s.setError(err)`, `s.end()`. The trace sends when `t.end()` is called.

## Recording steps

Inside any trace, three helpers cover the step kinds Glassray understands:

```ts theme={null}
// LLM call — model is required, provider and explicit input are optional
await t.llm("plan", { model: "claude-opus-4-8", provider: "anthropic", input: messages }, () =>
  anthropic.messages.create(req),
);

// Tool call — with or without an explicit input
await t.tool("search-kb", () => searchKb(query));
await t.tool("search-kb", { input: query }, () => searchKb(query));

// Anything else — retrieval, sub-workflows, custom steps
await t.span("rerank", { kind: "retriever" }, () => rerank(docs));
```

Each helper runs your function, captures its return value as the span's output, records timing, and marks the span errored if it throws (the exception passes through untouched).

**Nesting follows call structure.** Spans opened inside another helper's callback become its children; parallel `t.tool()` calls (`Promise.all`) become siblings. Spans still open when the root settles are auto-closed and flagged, so a forgotten `end()` can't wedge a trace.

### Usage capture

For `t.llm`, token usage is extracted automatically from the two common response shapes — Anthropic (`usage.input_tokens` / `output_tokens`) and OpenAI (`usage.prompt_tokens` / `completion_tokens`). For anything else, set it explicitly:

```ts theme={null}
s.setUsage({ inputTokens: 812, outputTokens: 240 });
```

### Errors

A throw anywhere — in the root callback or any helper — is recorded on the corresponding span and rethrown unchanged. You never need to try/catch for Glassray's benefit. To record an error without throwing, use `s.setError(err)` in manual mode.

### Escape hatches

Automatic capture reads arguments and return values. When those aren't the interesting payload, set trace or span I/O explicitly — explicit setters always win:

```ts theme={null}
t.setInput(ticket);
t.setOutput(resolution);
```

## Sessions

Group multi-turn conversations by passing the same `sessionId` on each turn's trace:

```ts theme={null}
await glassray.trace("chat-turn", { sessionId: conversation.id }, async (t) => { ... });
```

## Deterministic trace IDs

To correlate a trace with an external system (a ticket ID, a request ID) before any span exists, derive the trace ID from a seed — the same seed always yields the same ID:

```ts theme={null}
import { createTraceId } from "@glassray/tracing";

await glassray.trace("handle-ticket", { traceId: createTraceId(ticket.id) }, async (t) => { ... });
```

<Tip>
  Sending nothing? Check `glassray.stats()` and the [troubleshooting checklist](/sdk-troubleshooting). Worried about what's inside the payloads? [Privacy & redaction](/sdk-privacy) covers what leaves the process.
</Tip>
