Pi Session Tracing with Future AGI
Export Pi coding-agent sessions, prompts, turns, model calls, and tool executions to Future AGI with a small Pi extension. Prompt and tool bodies stay off by default.
Copy one extension file into Pi’s extensions directory, set three environment variables, and every Pi session becomes one Future AGI trace: session, prompts, turns, model calls with token counts, and tool executions. Prompts, answers, tool arguments, and tool results are not exported unless you set FI_CAPTURE_CONTENT=true.
Pi is an open-source coding agent that runs in your terminal (binary pi). Pi has no built-in OpenTelemetry exporter, but it loads TypeScript extensions that receive its session, agent, turn, message, and tool events. This page adds a small extension that turns those events into OTLP traces and sends them to Future AGI. You see each Pi session as one trace, with every prompt, model call, and tool execution inside it.
This is not the same as:
- The Pi Coding Agent SDK. Code that embeds Pi with
createAgentSession()is traced with the@traceai/pi-coding-agentpackage, which has its own page in this section. - Oh My Pi (
omp, the omp.sh fork). It is a different binary with its own hook API and has its own page. This extension is for upstream Pi only. - Model SDK tracing. A traceAI instrumentor on a provider SDK shows model calls without Pi’s session, turns, or tools. Pi makes its model calls itself.
pi-trace-extensionand other local trace viewers. They write files on your machine. This extension sends traces to Future AGI and writes nothing to disk.
This recipe installs Pi 1.2.0. The extension has no npm dependencies. See the Pi extension documentation and extension source.
Note
Pi moved from the @mariozechner/* npm scope to @earendil-works/* at version 0.74.0. Install @earendil-works/pi-coding-agent. The @mariozechner/pi-coding-agent package stopped at 0.73.1.
What you get
pi.session AGENT one per Pi session id
pi.prompt CHAIN one per prompt you submit (an agent run)
pi.turn CHAIN one per Pi turn: a model request and the tools it called
chat gpt-6-luna LLM model, provider, token counts, stop reason
execute_tool bash TOOL tool name, call id, duration, error flag
execute_tool read TOOL
pi.turn CHAIN
chat gpt-6-luna LLM the turn that wrote the answer
The trace id is derived from Pi’s session id, so continuing a session (pi -c, pi --session <id>, /resume) adds new prompts to the same trace. /new and /fork start a new session and a new trace. Every span carries session.id, so the session also appears under Sessions.
Set up
Install Pi
npm install -g @earendil-works/pi-coding-agent@1.2.0
pi --version Add the extension
To trace every Pi session on this machine, put the file in your user extensions directory:
mkdir -p ~/.pi/agent/extensions
curl -fsSL https://raw.githubusercontent.com/future-agi/docs/main/examples/coding-agents/pi/futureagi-otlp.ts \
-o ~/.pi/agent/extensions/futureagi-otlp.tsTo trace a single project instead, put it in that project’s .pi/extensions/ directory:
mkdir -p .pi/extensions
curl -fsSL https://raw.githubusercontent.com/future-agi/docs/main/examples/coding-agents/pi/futureagi-otlp.ts \
-o .pi/extensions/futureagi-otlp.tsPi loads project extensions only after you trust the project. Interactive Pi asks on first start. Print, JSON, and RPC modes cannot ask: trust the project once with /trust, or pass --approve (-a) for one run. If PI_CODING_AGENT_DIR is set, the user directory is $PI_CODING_AGENT_DIR/extensions/ instead of ~/.pi/agent/extensions/.
Install the file in one place only. A copy in both directories sends every span twice. For a one-off run you can also load it with pi -e ./futureagi-otlp.ts.
Set your Future AGI credentials
export FI_API_KEY="YOUR_API_KEY"
export FI_SECRET_KEY="YOUR_SECRET_KEY"
export FI_PROJECT_NAME="my-pi-project"Keep these in your shell or a secrets manager, not in a committed .pi/settings.json. If any of the three is missing, the extension registers nothing and Pi runs as usual.
Run Pi
pi
# or, non-interactively:
pi -p "List the files in this folder and read README.md"Pi uses its own model configuration. Any provider Pi supports works, including OpenAI-compatible endpoints in ~/.pi/agent/models.json. The provider is recorded on each model span.
Open the trace
In Future AGI, open Observe, select my-pi-project, and open the newest pi.session trace. A finished turn is sent about two seconds after it ends. Each export also carries a provisional copy of the running pi.prompt span and the current pi.turn (same span ids, marked pi.incomplete=true), so the tree stays complete while Pi works. The final copies replace them when the prompt’s run settles. Spans still open when Pi exits are closed and sent then.
Configuration
Environment variable reference
| Variable | Default | Description |
|---|---|---|
FI_API_KEY | none (required) | Future AGI API key, sent as X-Api-Key. |
FI_SECRET_KEY | none (required) | Future AGI secret key, sent as X-Secret-Key. |
FI_PROJECT_NAME | none (required) | Project the traces are filed under (project_name resource attribute). |
FI_BASE_URL | https://api.futureagi.com | Collector base URL. /tracer/v1/traces is appended. Set this for a self-hosted Future AGI. |
FI_OTLP_ENDPOINT | $FI_BASE_URL/tracer/v1/traces | Full OTLP/HTTP traces URL. Overrides FI_BASE_URL. |
FI_CAPTURE_CONTENT | false | true exports prompt, answer, tool argument, tool result, provider error message, session name, and working directory text. |
FI_OTLP_TIMEOUT_MS | 10000 | Timeout for each export request. When Pi quits or switches sessions, the extension waits at most this long or 3 seconds, whichever is shorter. |
FI_OTLP_DEBUG | false | true prints export failures to stderr. Failures are silent otherwise. |
The extension posts OTLP/HTTP JSON with traces only, one request at a time. It subscribes to session_start, session_info_changed, session_shutdown, agent_start, agent_end, agent_settled, turn_start, message_end, tool_execution_start, and tool_execution_end. Of these, only message_end is actionable in Pi (a handler may return a replacement message). The extension returns nothing from any handler, so Pi keeps every message as it is. It does not subscribe to turn_end, an actionable boundary that makes Pi rebuild its session view after every turn, nor to before_provider_request, before_provider_headers, tool_call, or tool_result, so it never blocks or rewrites a tool call or a provider request. It never reads or writes Pi’s session files. If an export fails, its spans are retried once with a later export and then dropped. Pi keeps working either way.
What is captured
Span and attribute reference
| Span | Pi events | Attributes |
|---|---|---|
pi.session | session_start, session_info_changed, session_shutdown | session.id, gen_ai.conversation.id, gen_ai.agent.name=pi, pi.version, pi.mode, pi.session.start_reason, pi.session.parent (sessions with a parent, such as forks), prompt, turn, model call, and tool counts, summed pi.usage.input_tokens, pi.usage.output_tokens, pi.usage.cost, error.type |
pi.prompt | agent_start, message_end (user), agent_end, agent_settled | pi.prompt.index, pi.prompt.attempts (counts automatic retries, compaction recovery, and queued follow-up messages within one run), pi.prompt.turns, pi.prompt.bytes (every user message in the run), pi.prompt.aborted (you cancelled the run) |
pi.turn | turn_start, message_end (assistant); closed by the next turn_start or by agent_end | pi.turn.index (Pi’s index), pi.turn.seq (unique within one Pi process or extension reload), pi.turn.outcome (completed, aborted, or error, from the assistant message’s stop reason, as Pi computes it, except that a model request you cancelled is aborted) |
chat <model> | message_end (assistant) | gen_ai.request.model, gen_ai.response.model, gen_ai.provider.name, gen_ai.response.id, gen_ai.usage.input_tokens (includes cache reads and writes), gen_ai.usage.output_tokens, gen_ai.usage.total_tokens, gen_ai.usage.cache_read.input_tokens, gen_ai.usage.cache_creation.input_tokens, gen_ai.response.finish_reasons (Pi stop reason), pi.api, pi.usage.reasoning_tokens, pi.usage.cost, pi.thinking_level, pi.llm.tool_calls, pi.response.bytes |
execute_tool <tool> | tool_execution_start, tool_execution_end | gen_ai.tool.name, gen_ai.tool.call.id, pi.tool.parent_call.id (tools called by another tool), pi.tool.input.bytes, pi.tool.output.bytes; duration from Pi’s durationMs |
Model call timing comes from the assistant message (timestamp and durationMs). Tool calls that another tool makes through ctx.executeTool(), for example codemode scripts, nest under the calling tool’s span.
Errors set the span status to ERROR with the message error details omitted by default:
- a model call that ends with stop reason
errorin a run you did not cancel (error.type=ProviderError), its turn (error.type=TurnError), and its prompt (error.type=ProviderError). If Pi retries and the retry succeeds, the prompt isOK; - a tool execution that Pi reports with
isError(error.type=ToolError). The prompt staysOKif the model recovers; - the
pi.sessionspan, when any prompt failed in the current Pi process or since the last/reload(error.type=PromptError).
Cancelling a run (Escape, or /new, /resume, or /fork while Pi is working) is not an error. The prompt gets pi.prompt.aborted=true and stays OK. The model call and turn that were cut off get pi.incomplete=true and stay OK, and the model call keeps the finish reason Pi reported: aborted, or error when you cancel while a tool runs and Pi’s next model request is cancelled before it is sent. The extension tells that case apart from a provider error by the run’s abort signal, as Pi’s providers do. A tool that was running or queued when you cancelled is reported by Pi itself as failed (isError), so its span is ERROR.
If Pi exits in the middle of a turn, the open spans are closed and marked pi.incomplete=true. They are not marked as errors.
What is not available
- The model request. The extension does not read
before_provider_request, so the system prompt, history, and tool schemas Pi sends to the model are never exported, even with content capture on. - Streaming detail. Token deltas (
message_update) and time to first token are not exported. - Model calls outside the agent loop. Compaction summaries, branch summaries, and nested model calls made by other extensions do not produce assistant messages in the session, so they have no span.
- Cost for custom models.
pi.usage.costcomes from Pi’s pricing. Models added inmodels.jsonwithout prices report0and get no cost attribute. - Tokens in the trace list. The trace list’s Tokens column reads the root span. Tokens are on each
chatspan and are summed on the session aspi.usage.*. - Hard kills.
SIGTERMandSIGHUPlet Pi shut the session down, so the extension sends its last spans. ASIGKILLloses whatever was not sent yet. - Session counters across processes. Continuing a session in a new Pi process adds its prompts to the same trace, and
/reloadreplaces the extension inside one process. The counters and summed usage onpi.session(and, with content capture on, its first prompt and latest answer) describe the latest Pi process or extension reload. - Oh My Pi. Loading this file in
ompis not supported. Use the Oh My Pi page.
Privacy
By default the extension exports ids, model and provider names, token counts, cost, timings, stop reasons, tool names, status, and byte sizes. It does not export:
- your prompt text or the assistant’s answers;
- tool arguments or tool results;
- the session name you set with
/nameor--name; - error messages: neither a failed tool’s error text nor the provider’s error message, only the error type;
- local paths: the working directory (
pi.cwd) is not exported, because it usually contains your OS user name.
To export content, set FI_CAPTURE_CONTENT=true before starting Pi. The message that started each run and the run’s final answer go to input.value and output.value on the pi.prompt span. The first prompt and latest answer of the current Pi process or extension reload also go on pi.session (so they show in the trace list). Answer text goes on each chat span. Tool arguments and results go on the tool span, and a failed tool’s result is its error text. A provider error message becomes the status message of the failed chat span. The session name goes to pi.session.name, and the working directory goes to pi.cwd on the session span. Tool arguments and results can contain local paths too. Each field is capped at 4096 characters (tool arguments cut at the cap are labeled text/plain instead of application/json), and strings that look like sk-... keys, Bearer tokens, or api_key=... values are replaced with [REDACTED].
Warning
Tool arguments and results can contain file contents, shell output, and secrets. Pattern redaction is not a data-loss-prevention control. Turn content capture on only for projects whose data you are allowed to store in Future AGI.
Pi extensions run inside the Pi process with your user’s permissions and can see prompts, files, and credentials. This extension is written to export only what the tables above list. Read it before you install it, as you would any extension.
Troubleshooting
No traces appear
Check that FI_API_KEY, FI_SECRET_KEY, and FI_PROJECT_NAME are set in the shell that starts Pi. If you installed the extension in .pi/extensions/, make sure the project is trusted: print mode skips untrusted project extensions without asking, so use --approve, run /trust once interactively, or install the file in ~/.pi/agent/extensions/. Set FI_OTLP_DEBUG=true to print collector errors such as collector returned HTTP 401 to stderr. For a self-hosted Future AGI, set FI_BASE_URL or FI_OTLP_ENDPOINT.
pi -p waits forever in a script or CI job
When stdin is not a terminal, pi reads it until it closes and adds it to the prompt. A script that leaves stdin open waits before the session starts. Run it as pi -p "..." < /dev/null. This is Pi CLI behavior, not the extension.
Pi takes longer to exit or to switch sessions
Pi waits for the extension’s last export when it quits and also on /new, /resume, /fork, and /reload. The extension sends one request at a time and stops waiting after FI_OTLP_TIMEOUT_MS or 3 seconds, whichever is shorter. It then cancels the request and drops the spans that were not sent, so an unreachable collector delays each of these by at most 3 seconds. Check the endpoint with FI_OTLP_DEBUG=true, or lower FI_OTLP_TIMEOUT_MS.
Every span appears twice
The extension is installed both in ~/.pi/agent/extensions/ and in the project’s .pi/extensions/, or loaded again with -e. Keep one copy.
A turn appears at the top level of the trace
Its pi.prompt span did not arrive. Every export carries the prompt span, provisional while the prompt runs, so this means the exports failed (see FI_OTLP_DEBUG), or Pi was killed with SIGKILL before its first export. The turn keeps its parent id, so the tree is complete again if the prompt span arrives later.
Two turns have the same pi.turn.index
Pi can reuse a turn index when it retries a model request. Use pi.turn.seq, which is unique within one Pi process or extension reload, to tell them apart.
Token counts look higher than the provider's input count
gen_ai.usage.input_tokens includes cached input (cache reads plus cache writes), following the OpenTelemetry GenAI conventions. Pi’s own usage.input excludes cached tokens. The cache counts are also exported separately.
Related guides
Questions & Discussion