OpenCode Session Tracing with Future AGI

Export OpenCode coding-agent sessions, model steps, and tool calls to Future AGI with a project-local OpenCode plugin. Prompt and tool bodies stay off by default.

📝
TL;DR

Copy one plugin file into .opencode/plugins/, set three environment variables, and every OpenCode session becomes one Future AGI trace: session, turns, model steps with token counts, and tool calls. Prompts, answers, tool arguments, and tool output are not exported unless you set FI_CAPTURE_CONTENT=true.

About

OpenCode is an open-source coding agent that runs in your terminal. This page adds a small OpenCode plugin that listens to OpenCode’s documented plugin events and the tool.execute.after hook, and sends them to Future AGI as OTLP traces. You see each OpenCode session as one trace, with every model step and tool call inside it.

The plugin also uses the dispose hook to send what is still open when OpenCode shuts down. dispose is not on the Plugins page; it comes from the Hooks type in @opencode-ai/plugin 1.19.0. The plugin registers no hook that runs before a prompt or a tool call (chat.message, tool.execute.before), so it cannot hold either up.

This is not the same as:

  • Model SDK tracing. A traceAI instrumentor on the provider SDK would show model calls without OpenCode’s session, turns, or tools. This plugin traces the OpenCode application itself.
  • OpenCode’s built-in OpenTelemetry. OpenCode can export its internal spans and, behind a deprecated config flag, AI SDK spans that carry full prompts. They do not form one session tree. See Why not OpenCode’s built-in OpenTelemetry.
  • OpenCode Zen. Which models or gateway OpenCode uses does not change this setup. The provider is recorded as an attribute.

This recipe uses OpenCode 1.19.0. The plugin has no npm dependencies. See the OpenCode plugin documentation and plugin source.

What you get

opencode.session          AGENT   one per OpenCode session
  opencode.turn           CHAIN   one per message you send
    chat gpt-6-luna       LLM     one per model step, with token counts
      execute_tool read   TOOL    one per tool call the step made
      execute_tool bash   TOOL
    chat gpt-6-luna       LLM     the step that wrote the answer

OpenCode runs tools inside the model step that requested them, so a tool span sits under that step and the step’s duration includes its tools. Subagent sessions started by the task tool join the parent’s trace under the parent session span. A subtask command (an @agent mention or a command with subtask: true) makes no model call of its own, so its execute_tool task span sits directly under the turn.

A message you send while OpenCode is still working starts a new turn. The model step that was running stays in the previous turn with its own tokens and finish reason, and that turn ends when the step ends.

Set up

Add the plugin to your project

From the root of the project you open with OpenCode:

mkdir -p .opencode/plugins
curl -fsSL https://raw.githubusercontent.com/future-agi/docs/main/examples/coding-agents/opencode/futureagi-otlp.ts \
  -o .opencode/plugins/futureagi-otlp.ts

OpenCode loads every file in .opencode/plugins/ at startup. The plugin only applies to this project. You can put the same file in ~/.config/opencode/plugins/ to trace every project, but do not install it in both places or each span is sent twice.

Set your Future AGI credentials

export FI_API_KEY="YOUR_API_KEY"
export FI_SECRET_KEY="YOUR_SECRET_KEY"
export FI_PROJECT_NAME="my-opencode-project"

Keep these in your shell or a secrets manager, not in opencode.json, which is often committed. If any of the three is missing, the plugin does nothing.

Run OpenCode

opencode
# or, non-interactively:
opencode run "List the files in this folder and read README.md"

The first start in a project with a local plugin takes longer: OpenCode writes .opencode/package.json and installs @opencode-ai/plugin into .opencode/node_modules.

Open the trace

In Future AGI, open Observe, select my-opencode-project, and open the newest trace. Spans are sent each time the session goes idle after a message. Every send includes a fresh copy of the opencode.session span (same span id and start time, updated end time and counts; Future AGI keeps the latest copy), so the trace has its root while OpenCode is still running.

Configuration

VariableDefaultDescription
FI_API_KEYnone (required)Future AGI API key, sent as X-Api-Key.
FI_SECRET_KEYnone (required)Future AGI secret key, sent as X-Secret-Key.
FI_PROJECT_NAMEnone (required)Project the traces are filed under (project_name resource attribute).
FI_BASE_URLhttps://api.futureagi.comCollector base URL. /tracer/v1/traces is appended. Set this for a self-hosted Future AGI.
FI_OTLP_ENDPOINT$FI_BASE_URL/tracer/v1/tracesFull OTLP/HTTP traces URL. Overrides FI_BASE_URL.
FI_CAPTURE_CONTENTfalsetrue exports prompt, answer, tool argument, tool output, error, and session title text, and the project directory.
FI_OTLP_TIMEOUT_MS3000Timeout for each export request, in milliseconds.
FI_OTLP_DEBUGfalsetrue prints export failures to stderr. Failures are silent otherwise.

The plugin posts OTLP/HTTP JSON with traces only. It never blocks or changes a tool call, never runs shell commands, and never reads OpenCode’s session storage on disk. If an export fails, its spans are retried once with the next flush and then dropped. OpenCode keeps working either way.

What is captured

SpanSource eventAttributes
opencode.sessionsession.created, session.updated; re-sent with every idle flush and from disposefi.span.kind (AGENT), gen_ai.operation.name (invoke_agent), gen_ai.agent.name (opencode), opencode.version, opencode.session.slug, opencode.project.id, opencode.session.parent_id (subagents), opencode.turn.count, opencode.turn.error_count, opencode.llm.count, opencode.tool.count, opencode.usage.input_tokens, opencode.usage.output_tokens, error.type
opencode.turnmessage.updated (user message), message.part.updated (its text parts)fi.span.kind (CHAIN), opencode.message.id, opencode.turn.index, opencode.agent, opencode.model.id, opencode.provider.id, opencode.prompt.bytes, opencode.aborted, opencode.context_overflow, error.type
chat <model>message.updated (assistant message), message.part.updated (step parts)fi.span.kind (LLM), gen_ai.operation.name (chat), gen_ai.request.model, gen_ai.response.model, gen_ai.provider.name, gen_ai.usage.input_tokens (includes cache reads and writes), gen_ai.usage.output_tokens (includes reasoning), gen_ai.usage.total_tokens, gen_ai.usage.cache_read.input_tokens, gen_ai.usage.cache_creation.input_tokens, opencode.usage.reasoning_tokens, opencode.cost, gen_ai.response.finish_reasons, opencode.message.id, opencode.agent, opencode.aborted, opencode.incomplete, error.type
execute_tool <tool>message.part.updated (tool parts), tool.execute.afterfi.span.kind (TOOL), gen_ai.operation.name (execute_tool), gen_ai.tool.name, gen_ai.tool.call.id, opencode.tool.status, opencode.tool.input.bytes, opencode.tool.output.bytes, opencode.aborted, opencode.incomplete, error.type, start and end times from OpenCode

Every span carries session.id and gen_ai.conversation.id, so the trace also appears under Sessions. opencode.incomplete marks a model step or tool that was still running when OpenCode shut down.

Status:

  • A model error on the assistant message (for example APIError) marks that model step and its turn ERROR. A failed tool marks its tool span ERROR (error.type=ToolError). Any other session.error marks the running turn ERROR. Each gets error.type, and the status message is the error type. The error text replaces it only with content capture on.
  • An abort (Esc, or MessageAbortedError) is not an error. The model step, the tools it interrupted, and the turn get opencode.aborted=true and stay OK.
  • A ContextOverflowError that OpenCode’s automatic compaction recovers sets opencode.context_overflow=true on the turn and is not an error. If compaction is off, the model step and its turn fail with that error type.
  • The opencode.session span has the status of the latest finished turn, so a later successful turn clears an earlier error. opencode.turn.error_count counts the failed turns.

What is not available

  • Full model input. Plugin events do not include the messages OpenCode sends to the model (system prompt, history, tool schemas). With content capture on, you get your prompt text and the assistant’s answer text, not the full request.
  • The session-title model call. OpenCode makes a short model call to title each session. It does not create an assistant message, so it has no span and its tokens are not counted.
  • Streaming detail. Token deltas (message.part.delta) and time to first token are not exported.
  • A cancellation event. OpenCode documents none. When you abort, OpenCode records a MessageAbortedError on the assistant message; the plugin reads it and sets opencode.aborted=true (see Status above).
  • Cost for custom providers. opencode.cost comes from OpenCode’s model catalog. Providers without pricing report 0.
  • The turn that is running when OpenCode is killed. Spans are sent when the session goes idle and from dispose at a normal exit. If OpenCode is killed (closed terminal, kill) while it works, the spans of the unfinished turn are lost. Earlier turns and the session span sent with them stay in the trace.
  • Totals across OpenCode processes. A session continued in a new OpenCode process (opencode --continue, or after OpenCode reloads its config) joins the same trace, but the plugin starts counting again. In that process opencode.turn.index restarts at 1, and the session span’s counts and token sums cover only that process. Because Future AGI keeps the latest copy of the session span, it shows the session’s creation time as its start, the latest process’s last activity as its end, and that process’s counts.

Privacy

By default the plugin exports ids, model and provider names, token counts, cost, timings, tool names, status, error types, and byte sizes. It does not export:

  • your prompt text, the assistant’s answers, or the generated session title;
  • tool arguments (including file paths inside them), tool output, or tool metadata previews;
  • error messages, stack traces, or tool error text (only the error type, such as APIError or ToolError, which is also the status message);
  • local paths: the project directory (opencode.directory) is not exported, because it usually contains your OS user name.

To export content, set FI_CAPTURE_CONTENT=true before starting OpenCode. Prompts and answers go to input.value and output.value (with input.mime_type and output.mime_type) on the turn and model spans, tool arguments and output to the tool span, error text to the span’s status message (and a failed tool’s output), the title to opencode.session.title, and the project directory to opencode.directory on the session span. Each field is capped at 4096 bytes (UTF-8, never splitting a character). Strings that look like sk-... keys or Bearer tokens are replaced with [REDACTED], and so are the values of keys whose name ends in api_key, secret_key, private_key, access_key, token, secret, or password (any case, _ or -, any prefix, such as OPENAI_API_KEY, x-api-key, AWS_SECRET_ACCESS_KEY, or client_secret), written as key=value text or as JSON.

Warning

Tool arguments and output 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.

The plugin runs inside OpenCode with your user’s permissions and can see everything OpenCode sees. It is written to export only what the tables above list.

Why not OpenCode’s built-in OpenTelemetry

OpenCode 1.19.0 also has an OpenTelemetry exporter in its source. It starts when OTEL_EXPORTER_OTLP_ENDPOINT is set, and the experimental.openTelemetry config flag (marked deprecated in the config schema) adds AI SDK model and tool spans. It is not described on opencode.ai/docs:

Built-in exporterThis plugin
Spans and tracesIncludes internal spans such as sql.execute, file system and configSession, model step and tool spans
Session rootNone. Model and tool spans hang off internal spansopencode.session
Prompt and tool bodiesOn whenever the AI SDK spans are (ai.prompt.messages, ai.toolCall.args, ai.toolCall.result). Turning experimental.openTelemetry off removes them, and the model and tool spans with themOff unless FI_CAPTURE_CONTENT=true; model and tool spans either way
Other signalsAlso posts OTLP logs, which the Future AGI collector does not acceptTraces only

If you already run the built-in exporter for another backend, keep it pointed there and use this plugin for Future AGI.

Troubleshooting

No traces appear

Check that FI_API_KEY, FI_SECRET_KEY, and FI_PROJECT_NAME are set in the shell that starts OpenCode. Start OpenCode from the project root so it finds .opencode/plugins/futureagi-otlp.ts. 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.

OpenCode reports that a plugin export is not a function

OpenCode loads every export of a plugin file as a plugin. Do not add other exports to futureagi-otlp.ts. Put helpers in the same file without export.

Spans show up only after the answer finishes

Spans are sent when the session goes idle, each time with a refreshed opencode.session span. A long interactive session shows each turn, and an up-to-date session span, once that turn finishes. A turn that is still running is not visible yet.

opencode run hangs in a script or CI job

When stdin is not a terminal, opencode run reads stdin until it closes and adds it to the message. A script that leaves stdin open waits forever before the session starts. Run it as opencode run "..." < /dev/null. This is OpenCode CLI behavior, not the plugin.

OpenCode takes longer to exit

On exit the plugin waits for its last exports, at most two times FI_OTLP_TIMEOUT_MS (3 seconds by default, so up to 6 seconds), even when earlier exports are still queued. Spans not sent by then are dropped. If the collector does not answer, check the endpoint, or lower FI_OTLP_TIMEOUT_MS.

Every span appears twice

The plugin is installed both globally (~/.config/opencode/plugins/) and in the project (.opencode/plugins/). Keep one copy.

Token counts look higher than the provider's input count

gen_ai.usage.input_tokens includes cached input (cache_read plus cache_creation), following the OpenTelemetry GenAI conventions. OpenCode’s own tokens.input excludes cached tokens. The cache counts are also exported separately.

Next Steps

Was this page helpful?

Questions & Discussion