Telemetry

On this page

Stratos records what happened across workflows, agents, MCP tools, and System One calls. Local logs and events are available by default. Optional OpenTelemetry traces show how those operations fit together and can be exported to a file or a backend such as Langfuse.

Evidence Available when What it contains
run.json Every top-level run Identity, timestamps, and completion metadata
run.log Every top-level run Captured workflow console output
events.jsonl Every top-level run Available native agent events, including descendants
OpenTelemetry traces A workflow or ancestor configures an exporter Timed operations, parent/child relationships, inputs, outputs, and available usage

Logs, events, and traces can contain sensitive task data. Credential redaction does not make arbitrary prompts or tool results safe to publish. Collection and export are best effort: failures are reported without failing the workflow.

For a first run, start with local logs and events. To follow the operation tree, add a file exporter or send traces to Langfuse. The later sections define the exact hierarchy, timing boundaries, and native observation limits.

Trace exporter configuration

Add one exporter under otel.traces.exporter in workflow.yaml. A child workflow can add its own destination to those inherited from its ancestors. Without a local or inherited exporter, no traces are exported; Stratos does not enable a console exporter implicitly.

Export to a local file

otel:
  traces:
    exporter:
      type: file
      path: ".stratos/telemetry"

path is a directory, relative to the invocation’s working directory unless absolute. Run the workflow normally; completed spans are written as standard OTLP JSON payloads in a JSONL file. This is separate from the default events.jsonl, which records agent events rather than trace export payloads.

See trace file export for filenames and child routing.

Export over OTLP

otel:
  traces:
    exporter:
      type: otlp
      endpoint: "https://telemetry.example.com/v1/traces"
      protocol: http/protobuf
      headers:
        Authorization: "{env:STRATOS_OTLP_AUTHORIZATION}"

For HTTP, endpoint is the full trace signal URL, including /v1/traces when required by the backend. For gRPC, it is the service address. Supported protocols are http/json, http/protobuf, and grpc (Protobuf).

Headers are optional; omit them for a destination that needs none. Set the referenced environment variable to the complete header value required by your backend. Stratos exports directly through the OpenTelemetry SDK, so a separate Collector or per-run receiver is not required.

Send traces to Langfuse

Use your Langfuse project’s public and secret API keys to prepare a Basic authorization header: Basic followed by the base64 encoding of public_key:secret_key. Store the complete value in LANGFUSE_AUTHORIZATION, not in the workflow manifest or version control. Base64 is encoding, not encryption.

For a local Langfuse deployment listening on port 3000:

otel:
  traces:
    exporter:
      type: otlp
      endpoint: "http://127.0.0.1:3000/api/public/otel/v1/traces"
      protocol: http/protobuf
      headers:
        Authorization: "{env:LANGFUSE_AUTHORIZATION}"

Replace the origin with your deployment’s address and port, or the correct Langfuse Cloud regional address. Use HTTPS for remote destinations. Langfuse accepts OTLP over HTTP JSON or Protobuf, not gRPC. For Langfuse v4 real-time ingestion, its current guidance also specifies the header x-langfuse-ingestion-version: "4".

Run a workflow with this configuration, then open the corresponding project’s trace view. Workflow, agent, model, and tool operations share a trace and retain their parent relationships; trace hierarchy explains the expected tree.

Stratos does not install or start Langfuse. See the Langfuse OpenTelemetry documentation for deployment/version requirements, regional endpoints, and ingestion behavior.

Telemetry authentication references

Exporter headers support environment variables and file contents:

headers:
  Authorization: "{env:STRATOS_OTLP_AUTHORIZATION}"

Alternatively, keep the complete header value in an owner-protected file:

headers:
  Authorization: "{file:~/.stratos/secrets/otlp-authorization}"

{env:VARIABLE} reads an environment variable; {file:path} reads a file. Secret-file paths resolve relative to the configuration file’s directory, or use absolute paths and ~/ for the home directory. This differs from a relative trace output path, which resolves against the invocation directory.

Keep secret files outside version control and restrict their permissions. References supply values, not token refresh. Missing environment variables, unreadable files, and exporter failures produce soft telemetry errors; the affected exporter is skipped without failing business work. Resolved credentials remain excluded from logs, errors, and telemetry content.

Default local logs and agent events

Local artifacts are stored centrally under ~/.stratos/runs/, even when trace export is not configured. Each top-level run has one directory containing the parent and all descendant workflows, not separate child directories.

~/.stratos/runs/
  -work-projects-example/
    workspace.json
    2026-10-01T21-04-49.964Z-review-pr-<execution-id>/
      run.json
      run.log
      events.jsonl

The workspace is the invoking process’s working directory, not a discovered Git root. Different subdirectories are therefore separate workspaces. Children retain the parent working directory.

Workspace directory names encode the full absolute path with separators replaced by hyphens. workspace.json stores the original path, for example {"path":"/work/projects/example"}. Stratos verifies this identity metadata before reusing the directory.

If that readable name belongs to a different workspace, Stratos appends -<hash>, using the first 12 lowercase hexadecimal characters of the path’s SHA-256 hash. Long names truncate the readable prefix while keeping the hash suffix. The hash describes the path, not repository contents. A collision at the hash-suffixed path is a soft error rather than permission to mix or overwrite data.

Run directories use <UTC-start-time>-<workflow-name>-<execution-id>, retaining milliseconds and the full execution UUID. Timestamp colons become hyphens. Characters outside ASCII letters, digits, _, and - in the workflow name become hyphens; only that name portion is truncated to fit the component limit.

Reading logs and events

run.json holds run identity, workflow name, timestamps, and completion metadata. run.log contains readable console output qualified by execution identity. events.jsonl combines events from every agent session in the run; it may be empty for a code-only workflow.

Event records include timestamp, workflowName, executionId, parentExecutionId and agentSessionId where applicable, and the payload in event. Log records carry the same identity qualifiers plus level and message. Repeated and parallel invocations remain distinguishable. Concurrent events can interleave, but serialized writes keep each JSONL record intact; independent top-level runs are not merged.

Workflow scripts use ordinary console.log, console.info, console.debug, console.warn, and console.error. Stratos subscribes to Node’s experimental diagnostics_channel console channels without replacing console methods. It uses the current async execution context and Node formatting, capturing arguments immediately rather than retaining their mutable array.

There is no custom logging library, level filter, logging.level setting, or --log-level flag. All captured agent events are retained, subject to credential exclusion. Capture does not promise direct stdout writes, other console methods, or subprocess streams. Write failures are reported to stderr and leave the workflow running, possibly with incomplete artifacts.

Retention

By default, Stratos retains the latest 10 completed runs per workspace and at most 100 overall. Both limits apply together and can be set under logging.retention in ~/.stratos/settings.json:

{
  "logging": {
    "retention": {
      "maxRunsPerWorkspace": 10,
      "maxRunsTotal": 100
    }
  }
}

A parent and all descendants count as one retained run. The group stays intact while any part is active. After execution finishes and artifacts are saved, a short-lived detached process deletes the oldest completed groups as needed. The result returns without waiting for cleanup, which can continue after the CLI exits without a permanent daemon. Cleanup failures are soft errors.

Retention covers logs, events, and trace files inside the run directory. It does not remove workflow definitions, trace files exported elsewhere, or data sent to an OTLP backend. Artifact persistence does not establish native event coverage or capture of direct stdout/subprocess streams.

Harness telemetry collection

Stratos consumes native harness observations, creates OpenTelemetry spans using GenAI conventions, and exports them through the SDK. Available model calls, tool calls, and compaction are recorded alongside workflow and agent spans. Prompts, responses, and tool contents are captured by default; credentials are excluded.

Destinations compose with workflows. Each workflow exports its own spans and descendants to its local destination plus inherited ancestor destinations. An unconfigured child inherits; a configured child can export even when its parent has no exporter. Independently invoked workflows use their own settings. Identical destination/configuration combinations receive each span once.

Every destination receives the same trace, span, and parent-span identities. An ancestor destination receives its whole subtree. A child-specific destination receives only that child’s subtree, so its root may refer to a parent absent from that destination. Trace context survives ancestors without exporters. Failure at one destination does not stop another export or fail execution.

Collection, observation integration, unavailable transcripts, parsing, span creation, and export failures are logged softly. This is separate from control-hook failures, which affect execution.

Codex uses persisted session rollout transcripts to reconstruct model-response steps, inputs/outputs, tool calls, and available per-step usage. The input is reconstructed history, not a guaranteed exact provider request; transcript intervals are not exact API latency. The transcript format is not a stable Codex interface. Hooks expose a nullable transcript_path; missing transcripts are a reported observation gap, not a workflow failure.

Trace hierarchy and node spans

A trace describes a complete top-level run. Each span describes a timed operation and points to its immediate parent span, not a separate parent trace. Child workflow invocations keep their own execution IDs within the same trace.

workflow
|-- decision model call
|-- MCP tool call (typed wrapper)
|-- harness.preflight
|-- harness.initialize
|-- agent
|   |-- harness.startup (Codex native turn readiness)
|   |-- model call
|   |-- compaction
|   `-- tool call
`-- child workflow
    `-- agent
        |-- model call
        |-- compaction
        `-- tool call
Span Captured information, when available
Workflow, including children Name, description, inputs, invocation overrides, output, and outcome
Agent invocation Invocation input, resolved user/system prompts, effective configuration including harness/model/effort, output, and outcome
Model call Model settings, input/output, token usage, and outcome
Tool call Tool name, arguments, result or error, and outcome
Compaction Observed boundaries, outcome/error details, and exposed usage

With the workflow-description split, the existing stratos.workflow.description span attribute uses shortDescription. Do not copy longDescription into workflow-description attributes or add a separate manual attribute.

All captured values follow the credential-exclusion policy. Every span carries its owning workflow name and execution ID. Durations derive from observed start/end timestamps; unavailable boundaries are not invented.

In an agent’s main conversation, model and tool calls are siblings directly under the agent span. Native task, spawning-tool, and compaction ownership is preserved when exposed. Background work stays attached to its originating invocation across later prompts and may finish after that prompt returns; foreground success does not establish background success.

Direct MCP calls and Decision calls sit directly under their calling workflow. Decision question builders (choice, score, and noul) do not execute requests and create no separate spans.

Compaction has a timed span only when both boundaries are observed. A completion without a start is an event marked as missing-start, not an invented duration. Codex reconstructed compaction is completion-only. Owner cleanup closes remaining open observations as incomplete if no native completion was seen.

Errors, cancellation, and parent status

A parent reflects its own outcome, not an automatic roll-up of child failures. A caught child error remains an error on that child while the parent can succeed. An unhandled error that fails the parent marks the parent as failed.

Cancellation or timeout closes affected open spans with its outcome and reason. Timeout sets OpenTelemetry Error status. Intentional cancellation records a cancelled outcome without marking the operation as an error; OpenTelemetry has no dedicated cancelled or timeout status. Already-completed spans stay unchanged. When the whole workflow is intentionally cancelled, affected operations record cancellation. Events and spans do not implement an inactivity watchdog.

Harness preparation and startup timing

Preparation is timed separately from agent and model work:

Span Boundary and parent
harness.preflight <harness> Native installation/authentication/catalog probe, including its cleanup; child of the owning workflow
harness.initialize <harness> Selected adapter’s initialize() call; child of the owning workflow
harness.startup codex Each native turn, from before hook-server/SDK execution preparation to the first observed turn.started; child of that invoke_agent span

Preparation spans repeat only when the operation runs, including selection attempts or an explicit retry after failed initialization. Successive prompts reuse initialization. Codex retains the conversation but starts a native process on each SDK turn, so startup is measured each time.

OpenCode initialization starts its owned server, creates the session, and waits for event-subscription readiness. Its preflight uses a separate temporary server; no separate startup span or extra readiness request is added. Claude has preflight/initialize timing but no separate instrumented native-ready boundary: initialization is not proof of full readiness for model work.

SDK-timed spans carry stratos.harness.name, stratos.harness.phase, stratos.workflow.name, stratos.workflow.execution.id, stratos.agent.session.id, and gen_ai.agent.name. Provider is included when known, and the selected canonical model is included for initialize/startup. Codex adds stratos.harness.ready_boundary = codex.turn.started.

stratos.completion.status is success, unavailable, error, cancelled, or incomplete. Unavailable/error sets OpenTelemetry Error; intentional cancellation and missing readiness retain Unset. If execution ends before readiness, startup closes with the observed failure, cancellation, or incomplete outcome. That duration includes cleanup and is an upper bound, not successful startup timing.

These spans exclude earlier prompt rendering, worktree acquisition, MCP credentials, and queue waits. Readiness is not pure process-spawn time, complete MCP readiness, exact provider-request start, or time to first token. Startup overlaps the agent span; do not add nested durations as if they were independent. Instrumentation does not change selection, fallback, reuse, or configuration. Timing errors remain soft; timing spans contain neither native configuration nor native error text.

Decision measurements

Each decision(...) invocation creates a model-call span under its workflow. It records structured state and questions, validated answers, requested and returned model, available tokens, and outcome. It does not invent a chat conversation. The API and question semantics remain in the Decision reference.

Content and measurement mapping

Alongside GenAI model/usage attributes, Stratos uses OpenInference’s input.value and output.value JSON strings with matching *.mime_type set to application/json, and openinference.span.kind: LLM. Langfuse recognizes these as the observation’s Input and Output. Stratos also records stratos.decision.input and stratos.decision.output with the same values; credential redaction applies to both copies.

Measurement Source and meaning
Input input.value: the submitted state and questions
Output output.value: validated, unchanged answers, including supplied probabilities, confidence, and scores
Invocation latency Span end minus start, after configuration/input validation and before credential resolution through retries/waits and response validation; client-observed time, not isolated model compute or time to first token
Model gen_ai.request.model and gen_ai.response.model; returned model may differ from the requested alias
Tokens gen_ai.usage.input_tokens and gen_ai.usage.output_tokens from the successful response; no estimates for failed attempts
Outcome stratos.completion.status: success, error, or cancelled; errors carry an exception event, error.type, and Error status; intentional cancellation carries a reason without Error status

A failed request without a validated result does not synthesize output or token counts. Cost is not inferred without known provider pricing. Confidence is a model-reported value, not measured accuracy.

Langfuse can round a short duration to 0.00s; its observations API exposes latency in seconds and span timestamps. Fixture timings are not Jev performance evidence. Total CLI time also includes builds and process startup, so it differs from the Decision span duration.

Trace file export

A file destination writes one JSONL file per configured export scope, named with the owning workflow’s formatted UTC start time, name, and execution ID:

2026-09-24T18-30-45.123Z-review-pr-<execution-id>.jsonl

Hyphens replace time-component colons for portability; execution IDs distinguish concurrent runs. Each line is a standard OTLP JSON trace export payload, which may contain several spans and preserves resource/scope information, IDs, attributes, and timing.

An ancestor’s file receives its workflow and descendant spans. A child with a different destination gets its own subtree file. Parent and child sharing the same resolved destination/configuration reuse the inherited file and write each span once. This file is not the agent events.jsonl.

OTEL metrics (deferred)

Separate OpenTelemetry metrics collection/export and metric correlation are not implemented. Traces carry available token usage and span timestamps. Native visibility limitations still apply, including approximate Codex transcript durations rather than exact provider-request latency.

Troubleshooting

Symptom What to check
Logs exist, but no traces appear Configure otel.traces.exporter; local logs do not imply a trace exporter is enabled. Check stderr for soft export/authentication errors.
Langfuse has no new trace Check the full signal URL, HTTP protocol, project credentials, and ingestion behavior for your Langfuse version.
The trace tree is incomplete Use an ancestor destination for the complete run. A child-only destination intentionally lacks ancestor spans; missing native observations also remain gaps.
events.jsonl is empty Code-only workflows need not emit agent events. Native coverage also varies by harness.
Duration differs from CLI elapsed time Build, preparation, queueing, agent work, and model calls have different boundaries. Nested spans overlap.
Blocking a tool result did not remove it from artifacts An after-tool hook controls model-visible feedback, not artifact redaction or rollback of tool side effects.