/** * SDK 2.0 span emission helpers. * * Public-API methods on the client open and close spans through `SpanRecorder`. * The recorder is the single place that knows about the OTel API: when no OTel * is configured, every method returns a no-op `SpanLifecycle` so the call * sites don't branch. * * Span boundaries: * * - `qar.client.connect` — wraps `client.connect()` until the WS upgrade resolves. * - `qar.client.disconnect` — short-lived; ends when teardown returns. * - `qar.client.task.start` — wraps `tasks.start`; ends when the iterator closes. * - `qar.client.task.continue` — same shape, distinct name. * - `qar.client.task.cancel` — short-lived; ends when `task.done(canceled)` arrives. * - `qar.client.task.resubscribe` — wraps `tasks.resubscribe` invocation. * - `qar.client.tool.respond` — short-lived; ends when the WS write resolves. * - `qar.client.tool.handler` — wraps the inbound `tool.request` → handler invocation. * * Spans inherit from the consumer's currently-active OTel context. Errors * record an exception and set `ERROR` status. Cleanly-terminated spans set * `OK`. Iterator early-termination still records `OK` (cancellation isn't an * error), with an attribute distinguishing terminal vs cancel paths. */ import { TOOL_OUTCOME_DENIED, TOOL_OUTCOME_ERROR, TOOL_OUTCOME_SUCCESS } from './attributes.js'; import type { OTelAPI, OTelContext } from '../tracing/types.js'; /** Tool-handler outcome the SDK reports as a span attribute on `qar.client.tool.handler`. */ export type ToolHandlerOutcome = typeof TOOL_OUTCOME_SUCCESS | typeof TOOL_OUTCOME_ERROR | typeof TOOL_OUTCOME_DENIED; /** * A span's lifecycle handle. Non-recording when no OTel API is configured — * every method is a no-op, so call sites never branch. */ export interface SpanLifecycle { /** Set an attribute on the underlying span. No-op without OTel. */ setAttribute(key: string, value: string | number | boolean): void; /** Close the span with `OK` status. No-op if already closed. */ succeed(): void; /** Close the span with `ERROR` status, recording the exception. */ fail(err: unknown): void; /** * Run `fn` with this span's context active. Used to chain child operations * (e.g. an outbound `task.continue` from inside a `task.start` span). */ withContext(fn: () => T): T; /** * Return the OTel `Context` carrying this span. `undefined` when no OTel — * call sites use this to reattach context to async iterator boundaries. */ context(): OTelContext | undefined; } /** * A live task span's context handle. Threaded through `TaskSubscription` so the * subscription can re-attach the span's context every time it pushes an event * to the consumer's iterator (otherwise the consumer's `for await` resumes * outside the span and child spans would parent against the wrong frame). */ export interface TaskSpanContext { /** Span lifecycle handle — call sites end / fail this directly. */ readonly lifecycle: SpanLifecycle; /** OTel context carrying the span. `undefined` when no OTel. */ readonly otelContext: OTelContext | undefined; } /** * Span emission entry-point. One instance per `QodoClient`. Methods return a * `SpanLifecycle` the call site closes when the operation completes (or fails). * * A `SpanRecorder` constructed with `otel === undefined` returns a single * shared no-op lifecycle for every method. No allocations, no overhead beyond * a method-call dispatch. */ export declare class SpanRecorder { private readonly otel; private readonly tracer; private readonly statusOk; private readonly statusError; /** Cached resource attrs stamped on every span so Phoenix groups by service. */ private readonly resourceAttrs; constructor(otel: OTelAPI | undefined, sdkVersion: string); /** Whether OTel emission is wired (vs no-op). */ get isEnabled(): boolean; /** * Open a `qar.client.task.start` span. The caller is responsible for closing * it — typically via the `TaskSubscription` lifecycle wired in `TaskClient`. */ startTaskStartSpan(attrs: TaskStartSpanAttrs): TaskSpanContext; /** Open a `qar.client.task.continue` span. */ startTaskContinueSpan(attrs: TaskContinueSpanAttrs): TaskSpanContext; /** Open a `qar.client.task.cancel` span. */ startTaskCancelSpan(attrs: TaskCancelSpanAttrs): SpanLifecycle; /** Open a `qar.client.task.resubscribe` span. */ startTaskResubscribeSpan(attrs: TaskResubscribeSpanAttrs): TaskSpanContext; /** * Open a `qar.client.tool.respond` span — short-lived, ends on WS write. * * Every outbound `tool.response` envelope carries a `node_name` so the * span records it for parallel-graph telemetry routing. */ startToolRespondSpan(attrs: ToolRespondSpanAttrs): SpanLifecycle; /** * Open a `qar.client.tool.handler` span around the invocation of a registered * handler for one `ToolCall` from an inbound `tool.request`. Caller updates * `qar.tool_outcome` and closes via `succeed()` / `fail()` based on the * handler's per-call result. * * A parallel-batched `tool.request` carrying N calls opens N sibling * spans (one per call), each tagged with its own * `qar.tool_call_id` and the request's `qar.node_name`. The shared parent * is whatever OTel context was active when the inbound envelope arrived — * typically the task span. */ startToolHandlerSpan(attrs: ToolHandlerSpanAttrs): SpanLifecycle; /** * Record one tool soft-dropped during MCP projection under * `mcpTools.onMissing: 'drop'`. Emits a short-lived * `qar.client.mcp.tool_dropped` span (opened + closed immediately) so the * misconfiguration is queryable in the tracing backend — a deploy gate can * assert "zero `qar.client.mcp.tool_dropped` spans" to catch a typo'd tool * name. No-op without OTel (the caller also logs a structured breadcrumb * for the no-OTel path). * * `mcp` is omitted when it can't be inferred (a prefix-off override key * carries no `.` prefix). */ recordMcpToolDropped(attrs: McpToolDroppedSpanAttrs): void; /** Open a `qar.client.connect` span. Ends when the WS upgrade resolves. */ startConnectSpan(): SpanLifecycle; /** Open a `qar.client.disconnect` span. */ startDisconnectSpan(attrs: DisconnectSpanAttrs): SpanLifecycle; /** * Internal: open a span as a child of the consumer's currently-active * context, attaching attributes + the resource attrs. Returns a * `SpanLifecycle` the caller closes. */ private openSpan; /** * Internal: open a span and return both the lifecycle and the OTel context * pinning that span — so a caller (e.g. `TaskSubscription`) can re-attach * the context every time it yields an event to the consumer's iterator. */ private openTaskSpan; } /** Outcome-attribute helper used by `TaskClient`/`ToolClient` call sites. */ export declare function toolOutcomeFor(outcome: 'success' | 'error' | 'denied'): ToolHandlerOutcome; export interface TaskStartSpanAttrs { /** * Optional — `task.start` opens its span BEFORE the server's * `task.started` ack arrives with the derived `session_id`. The span * lifecycle remains correct without the attribute; consumers correlate * via `qar.task_id` (set lazily on first inbound) and `qar.message_id`. * The attribute is stamped onto the span by the subscription's later * `setAttribute` call when admission completes. */ readonly sessionId?: string; readonly messageId: string; readonly agentId?: string; readonly skillName?: string; /** Optional `gen_ai.operation.name` value, e.g. `"invoke_agent"`. */ readonly operationName?: string; /** * Inline-graph dispatch attrs. Set when the `task.start` payload * is `{ graph, input }`; absent for `{ agent_id }` / `{ agent }` shapes. */ readonly graphEntry?: string; /** Longest acyclic path length from `entry`, computed at validate-time. */ readonly graphDepth?: number; /** * `"inline"` when the dispatch carries an inline `AgentSpec` or a * graph with any inline-spec node; absent for purely server-static * `agent_id` dispatches. */ readonly agentKind?: string; } export interface TaskContinueSpanAttrs { /** Optional — see {@link TaskStartSpanAttrs.sessionId}. */ readonly sessionId?: string; readonly messageId: string; readonly taskId: string; } export interface TaskCancelSpanAttrs { /** Optional — see {@link TaskStartSpanAttrs.sessionId}. */ readonly sessionId?: string; readonly messageId: string; readonly taskId: string; } export interface TaskResubscribeSpanAttrs { /** Optional — see {@link TaskStartSpanAttrs.sessionId}. */ readonly sessionId?: string; readonly messageId: string; readonly taskId: string; readonly sinceMessageId?: string; } export interface ToolRespondSpanAttrs { /** Optional — see {@link TaskStartSpanAttrs.sessionId}. */ readonly sessionId?: string; readonly toolCallId: string; readonly outcome: ToolHandlerOutcome; /** Required `node_name` of the originating `tool.request`. */ readonly nodeName: string; } export interface ToolHandlerSpanAttrs { /** Optional — see {@link TaskStartSpanAttrs.sessionId}. */ readonly sessionId?: string; readonly toolCallId: string; readonly toolName: string; /** Required `node_name` of the inbound `tool.request`. */ readonly nodeName: string; } export interface DisconnectSpanAttrs { readonly sessionId?: string; } export interface McpToolDroppedSpanAttrs { /** The dropped reference — an MCP name, or a `.` / bare tool name. */ readonly tool: string; /** Owning MCP, or `null` when it can't be inferred (prefix-off override key). */ readonly mcp: string | null; /** `"unregistered_mcp"` or `"unknown_tool"`. */ readonly reason: 'unregistered_mcp' | 'unknown_tool'; } //# sourceMappingURL=spans.d.ts.map