> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://respan.ai/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://respan.ai/docs/_mcp/server.

# Pi Coding Agent SDK (tracing)

> Trace pi coding-agent SDK sessions with Respan: every agent run, LLM call, tool execution, and compaction from @earendil-works/pi-coding-agent.

The [Pi Coding Agent SDK](https://pi.dev) (`@earendil-works/pi-coding-agent`) runs pi coding agents programmatically from TypeScript. Use `@respan/instrumentation-pi` to trace every `session.prompt()` as a Respan trace: the user prompt, each LLM call with prompts, completions, token, cache and cost usage, each tool execution, and compactions. Nothing is written to disk; spans are batched in memory and sent to Respan.

> **Note**
>
> This page covers the SDK used inside your own application. To trace interactive `pi` sessions in the terminal, see [pi](/docs/integrations/pi).

#### Set up Respan

Create an account at [platform.respan.ai](https://platform.respan.ai) and grab an [API key](https://platform.respan.ai/platform/gateway/api-keys).

#### Use Respan Gateway

pi can also call models through the Respan gateway: register a provider with `"baseUrl": "https://api.respan.ai/api"` and `"api": "openai-completions"` in `~/.pi/agent/models.json` or in your `ModelRegistry`. Every LLM call is then logged by the gateway with no SDK; the tracing integration on this page adds the agent-level structure (runs, tool calls, threads) on top.

#### Package README

* [`@respan/instrumentation-pi` README](https://github.com/RespanAI/respan/tree/main/javascript-sdks/instrumentations/respan-instrumentation-pi) — full option, configuration, and troubleshooting reference

## Setup

#### Install packages

```bash
npm install @earendil-works/pi-coding-agent @respan/respan @respan/instrumentation-pi
```

#### Set environment variables

```bash
export RESPAN_API_KEY="YOUR_RESPAN_API_KEY"
```

#### Initialize and run

pi has no global patch point; wire the instrumentor into the session. The recommended way is an inline extension, which sees every pi event including the exact context sent to the model:

```typescript
import {
  createAgentSession,
  DefaultResourceLoader,
  getAgentDir,
} from "@earendil-works/pi-coding-agent";
import { Respan } from "@respan/respan";
import { PiInstrumentor } from "@respan/instrumentation-pi";

const instrumentor = new PiInstrumentor({ workflowName: "mail-agent" });
const respan = new Respan({
  apiKey: process.env.RESPAN_API_KEY,
  instrumentations: [instrumentor],
});
await respan.initialize();

const loader = new DefaultResourceLoader({
  cwd: process.cwd(),
  agentDir: getAgentDir(),
  extensionFactories: [instrumentor.extension],
});
await loader.reload();

const { session } = await createAgentSession({ resourceLoader: loader });
await session.prompt("Summarize the latest email in the thread");
await respan.flush();
```

If you cannot control the resource loader, attach to an existing session instead:

```typescript
const { session } = await createAgentSession();
const detach = instrumentor.attach(session, {
  threadIdentifier: emailChainId, // default: the pi session id
  customerIdentifier: mailboxOwnerId,
  metadata: { mailbox: "support" },
});
await session.prompt("Draft a reply");
detach();
```

Passing an explicit `instrumentations` list to `Respan` disables provider auto-instrumentation inside the pi process, so LLM calls are not traced twice. One tracer is created per session, so many sessions can run in one process.

#### View your trace

Open the [Traces page](https://platform.respan.ai/platform/traces). Each pi session is one trace and every `session.prompt()` adds a turn to it, as long as the session is persisted and reopened (`SessionManager.open` / `continueRecent`); all runs of a pi session also share the session id as thread id, so the [Threads view](/docs/documentation/features/observe/view-debug-traces#follow-a-session-with-threads) shows the whole session as a conversation.

> **Note**
>
> **Zero-code alternative.** pi's default resource loader loads installed pi packages into SDK sessions too. On a machine where `@respan/instrumentation-pi` is installed as a pi package (`pi install npm:@respan/instrumentation-pi`) and `RESPAN_API_KEY` is set, `createAgentSession()` is traced with no code changes. See [pi](/docs/integrations/pi) for that setup.

## Trace shape

Each agent run (one `session.prompt()` → `agent_end`) is one root agent span, `pi.turn-<n>.agent`, displayed as `agent.turn-<n>` — n is the prompt's number within the pi session, so a resumed session continues at the next turn. Chat and tool spans hang directly off it.

```text
pi.turn-1.agent (agent)                         one per agent run, shown as agent.turn-1
├── pi.chat (chat)                              one per assistant message
│     prompts, completion, tool_calls, usage, TTFT, cost
├── bash.tool (tool)                            one per tool execution
├── read.tool (tool)                            skill usage detected from SKILL.md
├── pi.turn-2.steer (task)                      a message steered into the running turn, shown as steer.turn-2
├── pi.chat (chat)
└── pi.compaction (task)                        when compaction happens mid-run
```

Chat and tool spans are emitted the moment they complete, so an hour-long run streams into the dashboard while it is still running; the turn span arrives when the run ends. Every span carries the pi session id as its thread, session and trace-group identifier.

## Captured data

| Data                            | Description                                                                                                                                                                          |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Agent runs                      | One root agent span per prompt (`pi.turn-<n>.agent`, numbered within the session), with the prompt as input, the final assistant text as output, and turn / tool-call counts.        |
| LLM calls                       | One chat span per assistant message: provider, model, prompt messages, completion, this turn's tool calls, available tools.                                                          |
| Usage                           | Input, output, total, cache-read and cache-creation tokens, reasoning tokens, estimated cost, time to first token.                                                                   |
| Tool executions                 | One tool span per execution with arguments and result; failures carry the error. Skill usage (`read` of a `SKILL.md`) is tagged.                                                     |
| Compaction and branch summaries | Task spans with the trigger and the resulting summary.                                                                                                                               |
| Steered messages                | A message delivered into a run that is already working (`session.steer()` / `followUp()`) gets its own `steer.turn-<n>` span at the point pi injected it. See [Steering](#steering). |
| Git metadata                    | Repository URL (credentials stripped), branch and commit of the working directory, on each turn span (`respan.metadata.git_repository` / `git_branch` / `git_commit`).               |
| Errors                          | Assistant errors and aborts, tool errors, and runs interrupted by a shutdown.                                                                                                        |

## Steering

`session.steer()` hands a new message to an agent that is already working, for example a second email that arrives on the same thread while the first is still being processed. pi delivers it into the running turn at the next tool boundary and the run carries on. `followUp()` works the same way, delivered when the agent would otherwise stop. No extra tracing code is needed:

```typescript
const run = session.prompt("Process the inbound email and update the load");

// A new email arrives while the agent is still working
await session.steer("New email on the same thread: pickup moved to Friday");

await run;
```

Respan records each delivered message as its own row under the turn, named `steer.turn-<n>`, at the exact point pi injected it:

```text
agent.turn-1                steer_count: 2
├── llm.gpt-5
├── tool.read
├── steer.turn-2            input = the steered message
├── llm.gpt-5               after_steer = true
├── tool.bash
├── steer.turn-3
├── llm.gpt-5               after_steer = true
└── tool.write
agent.turn-4
```

| Where                        | What is recorded                                                                                                                                                                               |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `steer.turn-<n>` span        | Input is the steered message. Metadata: `turn_number` (the steer's own number), `steered_into_turn`, `steer_index`, and `delivered_after` (`tool_results`, `assistant_reply`, or `run_start`). |
| Turn span                    | Input lists the original prompt followed by every steered message, so one click shows everything the run was given. Metadata: `steer_count`.                                                   |
| First LLM call after a steer | Metadata `after_steer = true` and `steer_turn_numbers`. The steered message is the last item of that call's input.                                                                             |

**Turn numbers.** Turn numbers count the session's user messages, which keeps them stable when a session is resumed in another process. A steered message takes the next number, so a `turn-1` run that received three steers (`steer.turn-2`, `steer.turn-3`, `steer.turn-4`) is followed by `agent.turn-5`. Nothing was skipped: the numbers in between are the steer rows inside the turn.

**Find steers across traces.** On the [Spans page](/docs/documentation/features/observe/explore-spans#filter-by-custom-properties), filter by custom property:

* `after_steer` is `true`: exactly the model calls that reacted to a steered message.
* `steer_count` is `1` (or `2`, `3`, ...): the turn spans that received that many steers.
* `steered_into_turn` is `1`: the steer spans delivered into turn 1, each with its message as input.

A steer is an event inside the run, not a new run: the steps after it stay under the same turn, because the agent usually keeps working on its original task while taking the new message into account.

Steer spans require `@respan/instrumentation-pi` 0.1.5 or later.

## Options

`new PiInstrumentor(options)` accepts:

| Option                           | Default         | Meaning                                                                                                                                    |
| -------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `traceScope`                     | `"session"`     | `"session"` = one multi-root trace per pi session; `"run"` = one trace per agent run (see below)                                           |
| `promptCapture`                  | `"full"`        | `"full"` records the whole context on every chat span; `"delta"` records only the messages appended since the previous LLM call of the run |
| `captureSystemPrompt`            | `true`          | Record the system prompt on the first chat span of each run                                                                                |
| `captureReasoning`               | `true`          | Record assistant thinking blocks                                                                                                           |
| `captureToolSpans`               | `true`          | Emit tool spans                                                                                                                            |
| `maxContentChars`                | `0` (unlimited) | Optional per-string cap; nothing is truncated by default                                                                                   |
| `workflowName`, `agentName`      | `"pi"`          | `traceloop.workflow.name` on every turn span; the agent name in the turn span name (`<agentName>.turn-<n>.agent`)                          |
| `customerIdentifier`, `metadata` | —               | Attached to every span                                                                                                                     |

## One trace per run or per session

A pi session that is resumed over days or weeks (an email thread handled by an always-on agent, for example) can be viewed two ways:

* `traceScope: "session"` (default): every run of the session joins one trace whose id is derived from the pi session id, so a session reopened in another process (`SessionManager.open(file)`) or after a week-long pause keeps adding turns to the same trace. Respan groups the runs as multiple roots under one synthetic root, ordered by time; the trace's duration then spans from the first to the latest run. No span is ever held open across idle time. If you keep sessions in memory (`SessionManager.inMemory()`), every wake is a new session: pass your own `threadIdentifier` (for example the email-chain id) to keep them grouped in Threads.
* `traceScope: "run"`: every prompt is its own trace with its own cost and latency, nested under an active OpenTelemetry span when there is one. All traces of the session still share `thread_identifier` = the pi session id, so the Threads view shows the whole session in order.

## Long-running sessions and volume

The instrumentation keeps only the state of the current run in memory and writes nothing to disk, so idle sessions cost nothing and a process that handles thousands of sessions a day stays bounded. Full context capture is quadratic in the number of LLM calls per run; for very high-volume deployments use `promptCapture: "delta"` (the full conversation is still reconstructable from the trace) or `maxContentChars`.

For at-least-once delivery across Respan outages and restarts, run the Respan Collector (see the [CLI](/docs/documentation/cli)) next to the application and set `RESPAN_BASE_URL=http://127.0.0.1:4318`. The collector's queue is bounded and drains as soon as Respan acknowledges the data, so nothing accumulates on the machine.

## Attributes

Use Respan attributes to group runs by user or conversation.

```typescript
await respan.propagateAttributes(
  {
    customer_identifier: "user_123",
    thread_identifier: "email-chain-abc",
    metadata: { mailbox: "support" },
  },
  async () => {
    await session.prompt("Draft a reply");
  }
);
```

| Attribute             | Type     | Description                                                     |
| --------------------- | -------- | --------------------------------------------------------------- |
| `customer_identifier` | `string` | Identifies the end user in Respan analytics.                    |
| `thread_identifier`   | `string` | Groups runs into a conversation; defaults to the pi session id. |
| `metadata`            | `object` | Custom key-value pairs attached to spans.                       |

## Resources

* [`@respan/instrumentation-pi` on npm](https://www.npmjs.com/package/@respan/instrumentation-pi)
* [Package README](https://github.com/RespanAI/respan/tree/main/javascript-sdks/instrumentations/respan-instrumentation-pi) — full option reference
* [pi](/docs/integrations/pi) — trace interactive pi sessions in the terminal
* [CLI: collector commands](/docs/documentation/cli)
* [Pi Coding Agent SDK on npm](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) · [pi documentation](https://pi.dev)