Tracing Overview
AgentSquad ships an OpenTelemetry-style tracing pipeline. A Tracer opens spans, a SpanProcessor assembles and batches them, and a TraceExporter ships the finished TraceEvent records to a backend. Each layer is a protocol — swap any piece without touching the others.
Pipeline
Section titled “Pipeline”Tracer ──► SpanProcessor ──► TraceExporter │ │ └─ SpanHandle └─ BatchSpanProcessor (built-in)Local dev — OSLogTracer writes to os.Logger; no network, no configuration.
Production — ProcessingTracer + BatchSpanProcessor + OTLPExporter posts OTLP/HTTP JSON to any compatible collector.
Tracer protocol
Section titled “Tracer protocol”public protocol Tracer: Sendable { func startTrace( name: String, userId: String?, sessionId: String?, metadata: JSONValue? ) -> any SpanHandle
func flush() async throws // drain buffer on app-background func shutdown() async // final drain + release resources}startTrace returns the root SpanHandle. Its id is the trace id — use it to deep-link into your backend.
flush() and shutdown() have no-op default implementations, so a simple tracer only needs startTrace.
SpanHandle
Section titled “SpanHandle”public protocol SpanHandle: Sendable { var id: String { get }
func span(_ name: String, input: JSONValue?) -> any SpanHandle func generation(_ name: String, model: String, input: JSONValue?) -> any GenerationHandle
func setInput(_ input: JSONValue) func setMetadata(_ metadata: JSONValue)
func end(output: JSONValue?, error: (any Error)?)}span— opens a child span (a step, a tool call).generation— opens a child generation for an LLM call; pairs withGenerationHandle.usage.setInput/setMetadata— can be called any time beforeend. Useful when the input only arrives after the span opens (e.g. a transcript for a voice turn). Calls afterendare silently dropped.setMetadata— takes a.objectJSONValue; top-level keys become span attributes on the backend. OnOTLPExporterthe keys are emitted verbatim alongside the built-in GenAI attributes.
GenerationHandle
Section titled “GenerationHandle”public protocol GenerationHandle: SpanHandle { func usage(promptTokens: Int?, completionTokens: Int?)}Call usage after the LLM response arrives. BatchSpanProcessor merges the numbers into the span before it exports.
SpanProcessor protocol
Section titled “SpanProcessor protocol”public protocol SpanProcessor: Sendable { func onOpen(_ span: SpanData) func onUsage(id: String, promptTokens: Int?, completionTokens: Int?) func onSetInput(id: String, input: JSONValue) // no-op default func onSetMetadata(id: String, metadata: JSONValue) // no-op default func onEnd(id: String, endedAt: Date, output: JSONValue?, error: String?) func flush() async throws func shutdown() async}TraceExporter protocol
Section titled “TraceExporter protocol”public protocol TraceExporter: Sendable { func export(_ batch: [TraceEvent]) async throws func flush() async throws func shutdown() async}flush and shutdown have no-op default implementations. Implement them if your exporter buffers internally.
SpanData
Section titled “SpanData”SpanData is the snapshot handed to SpanProcessor.onOpen at span-open time. Usage, late input, and metadata arrive later via onUsage / onSetInput / onSetMetadata.
public struct SpanData: Sendable { public let id: String public let traceId: String public let parentId: String? public let kind: TraceEvent.Kind public let name: String public let startedAt: Date public let input: JSONValue? public let model: String? public let userId: String? public let sessionId: String? public let metadata: JSONValue?}TraceEvent
Section titled “TraceEvent”TraceEvent is the finished, flat record handed to TraceExporter in batches. BatchSpanProcessor assembles it from SpanData plus any late mutations before export.
public struct TraceEvent: Sendable, Equatable, Codable { public enum Kind: String, Sendable, Codable { case trace // root span case span // step / tool call case generation // LLM call }
public enum Status: String, Sendable, Codable { case running case ok case error }
public let traceId: String public let id: String public let parentId: String? public let kind: Kind public let name: String public let status: Status public let startedAt: Date public let endedAt: Date? public let input: JSONValue? public let output: JSONValue? public let error: String? public let model: String? public let promptTokens: Int? public let completionTokens: Int? public let userId: String? public let sessionId: String? public let metadata: JSONValue?}Serialized wire keys are snake_case (trace_id, started_at, prompt_tokens, etc.) regardless of any future property rename.
Built-in implementations
Section titled “Built-in implementations”| Implementation | Use case |
|---|---|
OSLogTracer | Local development — logs to Console.app / Instruments, no network |
ProcessingTracer | Production — routes spans through a SpanProcessor |
BatchSpanProcessor | Batch-and-export — assembles TraceEvent records and ships on size or flush() |
OTLPExporter | OTLP/HTTP JSON — compatible with Langfuse, Datadog, Grafana, Honeycomb |
Custom tracing
Section titled “Custom tracing”Implement any layer of the pipeline to swap in your own backend, batching strategy, or metrics sink. See Custom Tracing for full conformance examples covering TraceExporter, SpanProcessor, Tracer, and Redactor.
Related pages
Section titled “Related pages”- Orchestrator — the orchestrator opens the root span and passes it down through
AgentContext. - Messages & events —
AgentContextcarries the activeSpanHandleso agents open child spans without global state. - Voice — voice sessions set
metadataon turn spans to carry modality and audio token breakdown.