Pi Coding Agent SDK (tracing)

The Pi Coding Agent SDK (@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.

This page covers the SDK used inside your own application. To trace interactive pi sessions in the terminal, see pi.

Create an account at platform.respan.ai and grab an API key.

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.

Setup

1

Install packages

$npm install @earendil-works/pi-coding-agent @respan/respan @respan/instrumentation-pi
2

Set environment variables

$export RESPAN_API_KEY="YOUR_RESPAN_API_KEY"
3

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:

1import {
2 createAgentSession,
3 DefaultResourceLoader,
4 getAgentDir,
5} from "@earendil-works/pi-coding-agent";
6import { Respan } from "@respan/respan";
7import { PiInstrumentor } from "@respan/instrumentation-pi";
8
9const instrumentor = new PiInstrumentor({ workflowName: "mail-agent" });
10const respan = new Respan({
11 apiKey: process.env.RESPAN_API_KEY,
12 instrumentations: [instrumentor],
13});
14await respan.initialize();
15
16const loader = new DefaultResourceLoader({
17 cwd: process.cwd(),
18 agentDir: getAgentDir(),
19 extensionFactories: [instrumentor.extension],
20});
21await loader.reload();
22
23const { session } = await createAgentSession({ resourceLoader: loader });
24await session.prompt("Summarize the latest email in the thread");
25await respan.flush();

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

1const { session } = await createAgentSession();
2const detach = instrumentor.attach(session, {
3 threadIdentifier: emailChainId, // default: the pi session id
4 customerIdentifier: mailboxOwnerId,
5 metadata: { mailbox: "support" },
6});
7await session.prompt("Draft a reply");
8detach();

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.

4

View your trace

Open the Traces page. 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 shows the whole session as a conversation.

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, or respan integrate pi) and RESPAN_API_KEY is set, createAgentSession() is traced with no code changes. See 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.

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

DataDescription
Agent runsOne 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 callsOne chat span per assistant message: provider, model, prompt messages, completion, this turn’s tool calls, available tools.
UsageInput, output, total, cache-read and cache-creation tokens, reasoning tokens, estimated cost, time to first token.
Tool executionsOne tool span per execution with arguments and result; failures carry the error. Skill usage (read of a SKILL.md) is tagged.
Compaction and branch summariesTask spans with the trigger and the resulting summary.
Git metadataRepository URL (credentials stripped), branch and commit of the working directory, on each turn span (respan.metadata.git_repository / git_branch / git_commit).
ErrorsAssistant errors and aborts, tool errors, and runs interrupted by a shutdown.

Options

new PiInstrumentor(options) accepts:

OptionDefaultMeaning
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
captureSystemPrompttrueRecord the system prompt on the first chat span of each run
captureReasoningtrueRecord assistant thinking blocks
captureToolSpanstrueEmit tool spans
maxContentChars0 (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, metadataAttached 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 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.

1await respan.propagateAttributes(
2 {
3 customer_identifier: "user_123",
4 thread_identifier: "email-chain-abc",
5 metadata: { mailbox: "support" },
6 },
7 async () => {
8 await session.prompt("Draft a reply");
9 }
10);
AttributeTypeDescription
customer_identifierstringIdentifies the end user in Respan analytics.
thread_identifierstringGroups runs into a conversation; defaults to the pi session id.
metadataobjectCustom key-value pairs attached to spans.

Resources