@apo-ai/sdk 0.9.3 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,8 +2,9 @@
2
2
  // Do not edit by hand — edit the src/ originals and rebuild.
3
3
  import {
4
4
  emitGenerationAndTools,
5
+ servingHostFromBaseURL,
5
6
  startGeneration
6
- } from "./chunk-3OQ3JBFJ.js";
7
+ } from "./chunk-Z64A566W.js";
7
8
 
8
9
  // src/agent-task/integrations/anthropic.ts
9
10
  function createApoAnthropic(client, options) {
@@ -15,13 +16,18 @@ function createApoAnthropic(client, options) {
15
16
  if (params.stream === true) {
16
17
  return realCreate(params);
17
18
  }
19
+ const serving = servingHostFromBaseURL(
20
+ client.baseURL,
21
+ "anthropic"
22
+ );
18
23
  const { spanId, startedAt } = startGeneration(trace, {
19
24
  model,
20
25
  system: typeof params.system === "string" ? params.system : void 0,
21
26
  messages: params.messages,
22
27
  parentSpanId,
23
28
  taskId,
24
- turnNumber
29
+ turnNumber,
30
+ ...serving
25
31
  });
26
32
  try {
27
33
  const response = await realCreate(params);
@@ -71,4 +77,4 @@ function createApoAnthropic(client, options) {
71
77
  export {
72
78
  createApoAnthropic
73
79
  };
74
- //# sourceMappingURL=chunk-43OI6GP7.js.map
80
+ //# sourceMappingURL=chunk-6H2GACBG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/agent-task/integrations/anthropic.ts"],"sourcesContent":["/**\n * Anthropic JS SDK tracing integration for apo.\n *\n * Wraps an `Anthropic` client so `messages.create()` calls are\n * automatically traced. Tool calls and generations are captured without\n * any manual span code in the adapter.\n *\n * @example\n * ```ts\n * import Anthropic from \"@anthropic-ai/sdk\";\n * import { createApoAnthropic } from \"@apo-ai/sdk/agent-task\";\n *\n * async sendUserTurn(turn, { trace, parentSpanId }) {\n * const client = createApoAnthropic(\n * new Anthropic({ apiKey }),\n * { trace, parentSpanId, taskId: ctx.task.id, turnNumber }\n * );\n * const response = await client.messages.create({\n * model: \"claude-sonnet-4-20250514\",\n * max_tokens: 1024,\n * messages: [{ role: \"user\", content: String(turn) }],\n * tools: [...],\n * });\n * const text = response.content.filter(b => b.type === \"text\").map(b => b.text).join(\"\");\n * return { response: text };\n * }\n * ```\n *\n * @module\n */\n\nimport type { AgentTaskTraceContext } from \"../tracing.ts\";\nimport {\n startGeneration,\n emitGenerationAndTools,\n servingHostFromBaseURL,\n} from \"./span-helpers.ts\";\n\nexport interface CreateApoAnthropicOptions {\n /** The tee'd trace context from `sendUserTurn`'s second argument. */\n trace: AgentTaskTraceContext;\n /** The parent span id for this turn (from `sendUserTurn`'s context). */\n parentSpanId?: string;\n /** Optional task id, surfaced in span metadata. */\n taskId?: string;\n /** Optional turn number, surfaced in span metadata. */\n turnNumber?: number;\n}\n\n/**\n * Minimal structural shape of the Anthropic client we intercept.\n */\ninterface AnthropicClientLike {\n messages: {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n create: (...args: any[]) => Promise<any>;\n };\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [key: string]: any;\n}\n\n/**\n * The response shape we read from `messages.create()`.\n */\ninterface AnthropicResponseLike {\n model?: string;\n content?: Array<\n | { type: \"text\"; text: string }\n | { type: \"tool_use\"; name: string; input?: unknown }\n | { type: string }\n >;\n usage?: {\n input_tokens?: number;\n output_tokens?: number;\n };\n}\n\n/**\n * Wrap an Anthropic client so `messages.create()` is traced.\n * Returns a Proxy-wrapped client — call it exactly as you would the\n * original. Non-streaming only; streaming responses pass through\n * untraced.\n *\n * For SDKs that emit OpenTelemetry natively (Claude Agent SDK, Vercel AI\n * SDK), prefer `configureApoTelemetry()` instead — no wrapper needed.\n * This manual wrapper is for the raw `@anthropic-ai/sdk` package or\n * custom clients that don't emit OTel on their own.\n */\nexport function createApoAnthropic<T extends AnthropicClientLike>(\n client: T,\n options: CreateApoAnthropicOptions,\n): T {\n const { trace, parentSpanId, taskId, turnNumber } = options;\n const realCreate = client.messages.create.bind(client.messages);\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const tracedCreate = async (...args: any[]): Promise<any> => {\n const params = (args[0] ?? {}) as Record<string, unknown>;\n const model = String(params.model ?? \"unknown\");\n\n // Non-streaming: trace fully. Streaming (stream: true): pass through.\n if (params.stream === true) {\n return realCreate(params);\n }\n\n // The Anthropic SDK can also be pointed at a gateway via baseURL —\n // resolve the host it actually calls (issue #307).\n const serving = servingHostFromBaseURL(\n (client as { baseURL?: unknown }).baseURL,\n \"anthropic\",\n );\n const { spanId, startedAt } = startGeneration(trace, {\n model,\n system: typeof params.system === \"string\" ? params.system : undefined,\n messages: params.messages,\n parentSpanId,\n taskId,\n turnNumber,\n ...serving,\n });\n\n try {\n const response = (await realCreate(params)) as AnthropicResponseLike;\n const blocks = response.content ?? [];\n const text = blocks\n .filter((b): b is { type: \"text\"; text: string } => b.type === \"text\")\n .map((b) => b.text)\n .join(\"\\n\");\n const toolCalls = blocks\n .filter(\n (b): b is { type: \"tool_use\"; name: string; input?: unknown } =>\n b.type === \"tool_use\",\n )\n .map((b) => ({\n name: b.name,\n input: b.input,\n }));\n\n emitGenerationAndTools(trace, spanId, startedAt, {\n text: text || undefined,\n promptTokens: response.usage?.input_tokens,\n completionTokens: response.usage?.output_tokens,\n toolCalls,\n taskId,\n turnNumber,\n });\n\n return response;\n } catch (error) {\n emitGenerationAndTools(trace, spanId, startedAt, {\n taskId,\n turnNumber,\n error: { message: error instanceof Error ? error.message : String(error) },\n });\n throw error;\n }\n };\n\n // Proxy that intercepts client.messages.create but passes\n // everything else through unchanged.\n return new Proxy(client, {\n get(target, prop, receiver) {\n if (prop === \"messages\") {\n return new Proxy(target.messages, {\n get(messagesTarget, messagesProp) {\n if (messagesProp === \"create\") {\n return tracedCreate;\n }\n return Reflect.get(messagesTarget, messagesProp);\n },\n });\n }\n return Reflect.get(target, prop, receiver);\n },\n }) as T;\n}\n"],"mappings":";;;;;;;;;AAwFO,SAAS,mBACd,QACA,SACG;AACH,QAAM,EAAE,OAAO,cAAc,QAAQ,WAAW,IAAI;AACpD,QAAM,aAAa,OAAO,SAAS,OAAO,KAAK,OAAO,QAAQ;AAG9D,QAAM,eAAe,UAAU,SAA8B;AAC3D,UAAM,SAAU,KAAK,CAAC,KAAK,CAAC;AAC5B,UAAM,QAAQ,OAAO,OAAO,SAAS,SAAS;AAG9C,QAAI,OAAO,WAAW,MAAM;AAC1B,aAAO,WAAW,MAAM;AAAA,IAC1B;AAIA,UAAM,UAAU;AAAA,MACb,OAAiC;AAAA,MAClC;AAAA,IACF;AACA,UAAM,EAAE,QAAQ,UAAU,IAAI,gBAAgB,OAAO;AAAA,MACnD;AAAA,MACA,QAAQ,OAAO,OAAO,WAAW,WAAW,OAAO,SAAS;AAAA,MAC5D,UAAU,OAAO;AAAA,MACjB;AAAA,MACA;AAAA,MACA;AAAA,MACA,GAAG;AAAA,IACL,CAAC;AAED,QAAI;AACF,YAAM,WAAY,MAAM,WAAW,MAAM;AACzC,YAAM,SAAS,SAAS,WAAW,CAAC;AACpC,YAAM,OAAO,OACV,OAAO,CAAC,MAA2C,EAAE,SAAS,MAAM,EACpE,IAAI,CAAC,MAAM,EAAE,IAAI,EACjB,KAAK,IAAI;AACZ,YAAM,YAAY,OACf;AAAA,QACC,CAAC,MACC,EAAE,SAAS;AAAA,MACf,EACC,IAAI,CAAC,OAAO;AAAA,QACX,MAAM,EAAE;AAAA,QACR,OAAO,EAAE;AAAA,MACX,EAAE;AAEJ,6BAAuB,OAAO,QAAQ,WAAW;AAAA,QAC/C,MAAM,QAAQ;AAAA,QACd,cAAc,SAAS,OAAO;AAAA,QAC9B,kBAAkB,SAAS,OAAO;AAAA,QAClC;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAED,aAAO;AAAA,IACT,SAAS,OAAO;AACd,6BAAuB,OAAO,QAAQ,WAAW;AAAA,QAC/C;AAAA,QACA;AAAA,QACA,OAAO,EAAE,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE;AAAA,MAC3E,CAAC;AACD,YAAM;AAAA,IACR;AAAA,EACF;AAIA,SAAO,IAAI,MAAM,QAAQ;AAAA,IACvB,IAAI,QAAQ,MAAM,UAAU;AAC1B,UAAI,SAAS,YAAY;AACvB,eAAO,IAAI,MAAM,OAAO,UAAU;AAAA,UAChC,IAAI,gBAAgB,cAAc;AAChC,gBAAI,iBAAiB,UAAU;AAC7B,qBAAO;AAAA,YACT;AACA,mBAAO,QAAQ,IAAI,gBAAgB,YAAY;AAAA,UACjD;AAAA,QACF,CAAC;AAAA,MACH;AACA,aAAO,QAAQ,IAAI,QAAQ,MAAM,QAAQ;AAAA,IAC3C;AAAA,EACF,CAAC;AACH;","names":[]}
@@ -40,6 +40,10 @@ function createApoTracer(options) {
40
40
  step_name: stepName,
41
41
  tool_name: observationType === "TOOL" ? String(initialAttrs["ai.toolCall.name"] ?? "unknown") : void 0,
42
42
  model: String(initialAttrs["ai.model.id"] ?? "ai-sdk"),
43
+ // The AI SDK reports the model's provider itself (e.g. "openai",
44
+ // "anthropic.chat") — pass it through so runs are labeled by host
45
+ // (issue #307). Absent attribute = unknown, not guessed.
46
+ provider: typeof initialAttrs["ai.model.provider"] === "string" && initialAttrs["ai.model.provider"] !== "" ? String(initialAttrs["ai.model.provider"]) : void 0,
43
47
  observation_type: observationType,
44
48
  ...input !== void 0 ? { input } : {},
45
49
  metadata: {
@@ -230,4 +234,4 @@ function extractTextFromAttrs(attrs) {
230
234
  export {
231
235
  createApoTracer
232
236
  };
233
- //# sourceMappingURL=chunk-IK4GJX7H.js.map
237
+ //# sourceMappingURL=chunk-OZWOLCJ5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/agent-task/integrations/ai-sdk.ts"],"sourcesContent":["/**\n * Vercel AI SDK tracing integration for apo.\n *\n * Pass the result of {@link createApoTracer} to `generateText`'s\n * `experimental_telemetry` option. The AI SDK already emits OpenTelemetry\n * spans natively (e.g. `ai.toolCall`, `ai.generateText`); this tracer\n * intercepts them and translates them into apo trace spans, where they\n * become the TOOL/GENERATION observations that `t.calledTool`,\n * `t.noFailedActions`, and `t.messageIncludes` read.\n *\n * The adapter author writes zero manual span code.\n *\n * @example\n * ```ts\n * import { generateText } from \"ai\";\n * import { createApoTracer } from \"@apo-ai/sdk/agent-task\";\n *\n * async sendUserTurn(turn, { trace, parentSpanId }) {\n * const result = await generateText({\n * model, system, messages, tools,\n * experimental_telemetry: createApoTracer({ trace, parentSpanId }),\n * });\n * return { response: result.text };\n * }\n * ```\n *\n * @module\n */\n\nimport type { AgentTaskTraceContext } from \"../tracing.ts\";\nimport { extractTokenUsage } from \"./token-usage.ts\";\n\n// ── OpenTelemetry-compatible types ───────────────────────────────────────\n// We implement the subset of the OTel `Tracer` and `Span` interfaces that the\n// Vercel AI SDK touches (verified from `ai@6` source: `recordSpan` uses\n// `startActiveSpan`; the AI SDK also calls `startSpan` on the default tracer\n// for some operations). We re-declare the structural shape here so the\n// `experimental_telemetry: { tracer }` option type-checks against the AI\n// SDK's `TelemetrySettings` without taking a runtime dep on `@opentelemetry/api`.\n\ntype OTelAttributeValue =\n | string\n | number\n | boolean\n | null\n | undefined\n | Array<string | number | boolean | null | undefined>;\ntype OTelAttributes = Record<string, OTelAttributeValue>;\n\ninterface OTelSpanContext {\n traceId: string;\n spanId: string;\n traceFlags: number;\n}\n\ninterface OTelSpanStatus {\n code: number; // 0 = UNSET, 1 = OK, 2 = ERROR\n message?: string;\n}\n\ninterface OTelException {\n name: string;\n message: string;\n stack?: string;\n}\n\n/**\n * Minimal `Span` implementation. The AI SDK calls:\n * - `setAttributes` / `setAttribute` (tool result, response text)\n * - `setStatus` / `recordException` (errors)\n * - `end()` (finalises the span)\n *\n * Other methods (`addEvent`, `addLink`, `updateName`) are no-ops but must\n * exist on the object for structural type compatibility with the OTel `Span`\n * interface the AI SDK expects.\n */\ninterface ApoSpan {\n spanContext(): OTelSpanContext;\n setAttribute(key: string, value: OTelAttributeValue): this;\n setAttributes(attrs: OTelAttributes): this;\n addEvent(name: string, attributesOrStartTime?: unknown, startTime?: unknown): this;\n addLink(link: unknown): this;\n addLinks(links: unknown[]): this;\n setStatus(status: OTelSpanStatus): this;\n updateName(name: string): this;\n end(endTime?: unknown): void;\n isRecording(): boolean;\n recordException(exception: OTelException | string, time?: unknown): this;\n}\n\n/**\n * Minimal `Tracer` implementation. The AI SDK's `recordSpan` uses\n * `startActiveSpan(name, { attributes }, fn)`. We also implement\n * `startSpan(name, opts)` for structural compatibility — it returns a span\n * without invoking a callback, and the caller is responsible for `.end()`.\n */\ninterface ApoTracer {\n startSpan(name: string, options?: { attributes?: OTelAttributes }): ApoSpan;\n startActiveSpan<T>(\n name: string,\n options: { attributes?: OTelAttributes },\n fn: (span: ApoSpan) => Promise<T> | T,\n ): Promise<T>;\n startActiveSpan<T>(\n name: string,\n options: { attributes?: OTelAttributes },\n context: unknown,\n fn: (span: ApoSpan) => Promise<T> | T,\n ): Promise<T>;\n startActiveSpan<T>(\n name: string,\n fn: (span: ApoSpan) => Promise<T> | T,\n ): Promise<T>;\n}\n\n// ── helpers ──────────────────────────────────────────────────────────────\n\nfunction safeParse(raw: unknown): unknown {\n if (typeof raw !== \"string\") return raw;\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n}\n\nfunction monotonicNowMs(): number {\n if (typeof performance !== \"undefined\" && typeof performance.now === \"function\") {\n return performance.now();\n }\n return Date.now();\n}\n\nfunction round3(n: number): number {\n return Math.round(n * 1000) / 1000;\n}\n\nconst SPAN_ID_PREFIX = \"ai-sdk-\";\nlet spanCounter = 0;\n\nfunction nextSpanId(): string {\n spanCounter += 1;\n return `${SPAN_ID_PREFIX}${spanCounter}`;\n}\n\n// ── createApoTracer ──────────────────────────────────────────────────────\n\nexport interface CreateApoTracerOptions {\n /** The tee'd trace context from `sendUserTurn`'s second argument. */\n trace: AgentTaskTraceContext;\n /** The parent span id for this turn (from `sendUserTurn`'s context). */\n parentSpanId?: string;\n /** Optional task id, surfaced in span metadata. */\n taskId?: string;\n /** Optional turn number, surfaced in span metadata. */\n turnNumber?: number;\n}\n\n/**\n * Build a `Tracer` compatible with the Vercel AI SDK's\n * `experimental_telemetry.tracer` option. The returned object satisfies the\n * shape `{ isEnabled: true, tracer }` so it can be passed directly to\n * `experimental_telemetry`.\n *\n * The AI SDK emits spans named `ai.generateText`, `ai.generateText.doGenerate`,\n * and `ai.toolCall`. We translate the load-bearing ones:\n *\n * - `ai.toolCall` → a TOOL span (so `t.calledTool(name)` works)\n * - `ai.generateText` → a GENERATION span (so `t.messageIncludes(text)` works)\n * and error status → `t.noFailedActions()` catches it\n *\n * Other spans (`ai.generateText.doGenerate`, etc.) are ignored — only\n * TOOL/AGENT/GENERATION observation types are visible to `t.*` assertions.\n */\nexport function createApoTracer(\n options: CreateApoTracerOptions,\n): { isEnabled: true; tracer: ApoTracer } {\n const { trace, parentSpanId, taskId, turnNumber } = options;\n\n // The generation span id for the current turn. Once `ai.generateText`\n // creates it, subsequent `ai.toolCall` spans should nest under it (the\n // agent calls tools *during* generation), not under the turn span.\n // Without this, every tool renders as a sibling of `agent.generate`\n // instead of a child, flattening the trace hierarchy.\n let currentGenerationSpanId: string | undefined;\n\n /**\n * Build a live span that accumulates attributes and, on `.end()`,\n * calls `trace.createSpan` + `trace.endSpan` with the right observation type.\n */\n function makeLiveSpan(\n name: string,\n observationType: \"TOOL\" | \"GENERATION\",\n initialAttrs: OTelAttributes,\n ): ApoSpan {\n const stepName =\n observationType === \"TOOL\"\n ? String(initialAttrs[\"ai.toolCall.name\"] ?? \"unknown\")\n : \"agent.generate\";\n\n const input =\n observationType === \"TOOL\"\n ? (safeParse(initialAttrs[\"ai.toolCall.args\"]) as Record<string, unknown>)\n : undefined;\n\n const spanId = trace.createSpan({\n task_id: taskId ?? \"ai-sdk\",\n parent_call_id:\n observationType === \"TOOL\"\n ? (currentGenerationSpanId ?? parentSpanId ?? trace.rootSpanId)\n : (parentSpanId ?? trace.rootSpanId),\n step_name: stepName,\n tool_name:\n observationType === \"TOOL\"\n ? String(initialAttrs[\"ai.toolCall.name\"] ?? \"unknown\")\n : undefined,\n model: String(initialAttrs[\"ai.model.id\"] ?? \"ai-sdk\"),\n // The AI SDK reports the model's provider itself (e.g. \"openai\",\n // \"anthropic.chat\") — pass it through so runs are labeled by host\n // (issue #307). Absent attribute = unknown, not guessed.\n provider:\n typeof initialAttrs[\"ai.model.provider\"] === \"string\" &&\n initialAttrs[\"ai.model.provider\"] !== \"\"\n ? String(initialAttrs[\"ai.model.provider\"])\n : undefined,\n observation_type: observationType,\n ...(input !== undefined ? { input } : {}),\n metadata: {\n ...(turnNumber !== undefined ? { turnNumber } : {}),\n ...(taskId ? { taskId } : {}),\n },\n });\n\n // Record the generation span id so tool spans created after it nest\n // under it (tools are called during generation).\n if (observationType === \"GENERATION\") {\n currentGenerationSpanId = spanId;\n }\n\n const startedAt = monotonicNowMs();\n let pendingAttrs: OTelAttributes = { ...initialAttrs };\n let status: OTelSpanStatus = { code: 0 };\n let errored = false;\n let ended = false;\n\n const span: ApoSpan = {\n spanContext(): OTelSpanContext {\n return { traceId: trace.runId, spanId, traceFlags: 0 };\n },\n setAttribute(key: string, value: OTelAttributeValue): ApoSpan {\n pendingAttrs[key] = value;\n return span;\n },\n setAttributes(attrs: OTelAttributes): ApoSpan {\n Object.assign(pendingAttrs, attrs);\n return span;\n },\n addEvent(_name: string, _attributesOrStartTime?: unknown, _startTime?: unknown): ApoSpan {\n return span; // no-op — events aren't load-bearing for Flow assertions\n },\n addLink(_link: unknown): ApoSpan {\n return span;\n },\n addLinks(_links: unknown[]): ApoSpan {\n return span;\n },\n setStatus(s: OTelSpanStatus): ApoSpan {\n status = s;\n if (s.code === 2) errored = true;\n return span;\n },\n updateName(_name: string): ApoSpan {\n return span; // name was set at creation time\n },\n end(_endTime?: unknown): void {\n if (ended) return;\n ended = true;\n const latency = round3(monotonicNowMs() - startedAt);\n\n if (observationType === \"TOOL\") {\n const resultRaw = pendingAttrs[\"ai.toolCall.result\"];\n const output = resultRaw !== undefined ? safeParse(resultRaw) : undefined;\n trace.endSpan(spanId, {\n ...(output !== undefined\n ? {\n output:\n output && typeof output === \"object\"\n ? (output as Record<string, unknown>)\n : { value: output },\n }\n : {}),\n latency_ms: latency,\n ...(errored ? { level: \"ERROR\" as const } : {}),\n });\n } else {\n const text = extractTextFromAttrs(pendingAttrs);\n const tokens = extractTokenUsage(pendingAttrs);\n trace.endSpan(spanId, {\n output: text !== undefined ? { text } : {},\n latency_ms: latency,\n ...tokens,\n ...(errored\n ? { level: \"ERROR\" as const, status_message: status.message }\n : {}),\n });\n }\n },\n isRecording(): boolean {\n return !ended;\n },\n recordException(exception: OTelException | string, _time?: unknown): ApoSpan {\n errored = true;\n const msg = typeof exception === \"string\" ? exception : exception.message;\n status = { code: 2, message: msg };\n return span;\n },\n };\n\n return span;\n }\n\n function makeNoopSpan(): ApoSpan {\n const noop: ApoSpan = {\n spanContext: () => ({ traceId: \"\", spanId: nextSpanId(), traceFlags: 0 }),\n setAttribute() {\n return noop;\n },\n setAttributes() {\n return noop;\n },\n addEvent() {\n return noop;\n },\n addLink() {\n return noop;\n },\n addLinks() {\n return noop;\n },\n setStatus() {\n return noop;\n },\n updateName() {\n return noop;\n },\n end() {},\n isRecording: () => false,\n recordException() {\n return noop;\n },\n };\n return noop;\n }\n\n const tracer: ApoTracer = {\n startSpan(name: string, opts?: { attributes?: OTelAttributes }): ApoSpan {\n const attrs = opts?.attributes ?? {};\n const isToolCall = name === \"ai.toolCall\";\n const isGeneration = name === \"ai.generateText\" || name === \"ai.streamText\";\n if (!isToolCall && !isGeneration) return makeNoopSpan();\n return makeLiveSpan(name, isToolCall ? \"TOOL\" : \"GENERATION\", attrs);\n },\n\n startActiveSpan<T>(\n name: string,\n optionsOrFn:\n | { attributes?: OTelAttributes }\n | ((span: ApoSpan) => Promise<T> | T),\n contextOrFn?: ((span: ApoSpan) => Promise<T> | T) | unknown,\n maybeFn?: (span: ApoSpan) => Promise<T> | T,\n ): Promise<T> {\n // Three overload shapes:\n // (name, fn) → optionsOrFn is the fn\n // (name, options, fn) → contextOrFn is the fn\n // (name, options, context, fn) → maybeFn is the fn (context ignored)\n let opts: { attributes?: OTelAttributes };\n let fn: (span: ApoSpan) => Promise<T> | T;\n\n if (typeof optionsOrFn === \"function\") {\n opts = {};\n fn = optionsOrFn;\n } else if (typeof contextOrFn === \"function\") {\n opts = optionsOrFn;\n fn = contextOrFn as (span: ApoSpan) => Promise<T> | T;\n } else {\n opts = optionsOrFn;\n fn = maybeFn!;\n }\n const attrs = opts.attributes ?? {};\n const isToolCall = name === \"ai.toolCall\";\n const isGeneration = name === \"ai.generateText\" || name === \"ai.streamText\";\n\n if (!isToolCall && !isGeneration) {\n const noopSpan = makeNoopSpan();\n return Promise.resolve(fn(noopSpan)).then((result) => {\n noopSpan.end();\n return result;\n });\n }\n\n const span = makeLiveSpan(\n name,\n isToolCall ? \"TOOL\" : \"GENERATION\",\n attrs,\n );\n\n return Promise.resolve(fn(span))\n .then((result) => {\n span.end();\n return result;\n })\n .catch((error) => {\n span.setStatus({\n code: 2,\n message: error instanceof Error ? error.message : String(error),\n });\n span.recordException({\n name: error instanceof Error ? error.name : \"Error\",\n message: error instanceof Error ? error.message : String(error),\n ...(error instanceof Error && error.stack ? { stack: error.stack } : {}),\n });\n span.end();\n throw error;\n });\n },\n };\n\n return { isEnabled: true, tracer };\n}\n\n// ── internals ────────────────────────────────────────────────────────────\n\n/**\n * The AI SDK sets the model's response text under various attribute keys\n * depending on the operation. Try the known ones.\n */\nfunction extractTextFromAttrs(attrs: OTelAttributes): string | undefined {\n const candidates = [\n \"ai.response.text\",\n \"ai.response.output\",\n \"ai.generateText.result\",\n ];\n for (const key of candidates) {\n const val = attrs[key];\n if (typeof val === \"string\" && val.length > 0) {\n const parsed = safeParse(val);\n if (typeof parsed === \"string\") return parsed;\n return val;\n }\n }\n return undefined;\n}\n"],"mappings":"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","names":[]}
@@ -3,8 +3,9 @@
3
3
  import {
4
4
  emitGenerationAndTools,
5
5
  safeParse,
6
+ servingHostFromBaseURL,
6
7
  startGeneration
7
- } from "./chunk-3OQ3JBFJ.js";
8
+ } from "./chunk-Z64A566W.js";
8
9
 
9
10
  // src/agent-task/integrations/openai.ts
10
11
  function createApoOpenAI(client, options) {
@@ -16,13 +17,18 @@ function createApoOpenAI(client, options) {
16
17
  if (params.stream === true) {
17
18
  return realCreate(params);
18
19
  }
20
+ const serving = servingHostFromBaseURL(
21
+ client.baseURL,
22
+ "openai"
23
+ );
19
24
  const { spanId, startedAt } = startGeneration(trace, {
20
25
  model,
21
26
  system: typeof params.system === "string" ? params.system : void 0,
22
27
  messages: params.messages,
23
28
  parentSpanId,
24
29
  taskId,
25
- turnNumber
30
+ turnNumber,
31
+ ...serving
26
32
  });
27
33
  try {
28
34
  const response = await realCreate(params);
@@ -80,4 +86,4 @@ function createApoOpenAI(client, options) {
80
86
  export {
81
87
  createApoOpenAI
82
88
  };
83
- //# sourceMappingURL=chunk-3B3L2H4Y.js.map
89
+ //# sourceMappingURL=chunk-P43KL5AR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/agent-task/integrations/openai.ts"],"sourcesContent":["/**\n * OpenAI JS SDK tracing integration for apo.\n *\n * Wraps an `OpenAI` client so `chat.completions.create()` calls are\n * automatically traced. Tool calls and generations are captured without\n * any manual span code in the adapter.\n *\n * Uses a Proxy to intercept the `create` method, then emits GENERATION +\n * TOOL spans through the trace context, where they become the observations\n * that `t.calledTool` reads.\n *\n * @example\n * ```ts\n * import OpenAI from \"openai\";\n * import { createApoOpenAI } from \"@apo-ai/sdk/agent-task\";\n *\n * async sendUserTurn(turn, { trace, parentSpanId }) {\n * const client = createApoOpenAI(\n * new OpenAI({ apiKey, baseURL: \"https://openrouter.ai/api/v1\" }),\n * { trace, parentSpanId, taskId: ctx.task.id, turnNumber }\n * );\n * const response = await client.chat.completions.create({\n * model: \"deepseek/deepseek-v4.1-flash\",\n * messages: [{ role: \"user\", content: String(turn) }],\n * tools: [...],\n * });\n * return { response: response.choices[0].message.content ?? \"\" };\n * }\n * ```\n *\n * @module\n */\n\nimport type { AgentTaskTraceContext } from \"../tracing.ts\";\nimport {\n startGeneration,\n emitGenerationAndTools,\n safeParse,\n servingHostFromBaseURL,\n} from \"./span-helpers.ts\";\n\nexport interface CreateApoOpenAIOptions {\n /** The tee'd trace context from `sendUserTurn`'s second argument. */\n trace: AgentTaskTraceContext;\n /** The parent span id for this turn (from `sendUserTurn`'s context). */\n parentSpanId?: string;\n /** Optional task id, surfaced in span metadata. */\n taskId?: string;\n /** Optional turn number, surfaced in span metadata. */\n turnNumber?: number;\n}\n\n/**\n * Minimal structural shape of the OpenAI client we need to intercept.\n * We avoid importing `openai` at the type level so this module doesn't\n * take a runtime dependency — the wrapper works with any client that has\n * `chat.completions.create`.\n *\n * The `create` signature uses a broad type so the OpenAI SDK's overloaded\n * `create` method (which has specific param types) is assignable to this.\n */\ninterface OpenAIClientLike {\n chat: {\n completions: {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n create: (...args: any[]) => Promise<any>;\n };\n };\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [key: string]: any;\n}\n\n/**\n * The response shape we read from `chat.completions.create()`.\n */\ninterface OpenAIResponseLike {\n model?: string;\n choices?: Array<{\n message?: {\n content?: string | null;\n tool_calls?: Array<{\n function?: { name?: string; arguments?: string };\n }>;\n };\n }>;\n usage?: {\n prompt_tokens?: number;\n completion_tokens?: number;\n completion_tokens_details?: {\n /** Thinking tokens, reported by o-series/DeepSeek/GLM-style providers. */\n reasoning_tokens?: number;\n };\n };\n}\n\n/**\n * Wrap an OpenAI client so `chat.completions.create()` is traced.\n * Returns a Proxy-wrapped client — call it exactly as you would the\n * original. Non-streaming only; streaming responses pass through\n * untraced.\n *\n * For SDKs that emit OpenTelemetry natively (OpenAI Agents SDK, Vercel AI\n * SDK, Claude Agent SDK), prefer `configureApoTelemetry()` instead — no\n * wrapper needed. This manual wrapper is for the raw `openai` package or\n * custom clients that don't emit OTel on their own.\n */\nexport function createApoOpenAI<T extends OpenAIClientLike>(\n client: T,\n options: CreateApoOpenAIOptions,\n): T {\n const { trace, parentSpanId, taskId, turnNumber } = options;\n const realCreate = client.chat.completions.create.bind(client.chat.completions);\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const tracedCreate = async (...args: any[]): Promise<any> => {\n const params = (args[0] ?? {}) as Record<string, unknown>;\n const model = String(params.model ?? \"unknown\");\n\n // Non-streaming: trace fully. Streaming (stream: true): pass through.\n if (params.stream === true) {\n return realCreate(params);\n }\n\n // The OpenAI SDK is often pointed at a gateway via baseURL — the host\n // it actually calls is the run's serving host (issue #307).\n const serving = servingHostFromBaseURL(\n (client as { baseURL?: unknown }).baseURL,\n \"openai\",\n );\n const { spanId, startedAt } = startGeneration(trace, {\n model,\n system: typeof params.system === \"string\" ? params.system : undefined,\n messages: params.messages,\n parentSpanId,\n taskId,\n turnNumber,\n ...serving,\n });\n\n try {\n const response = (await realCreate(params)) as OpenAIResponseLike;\n const message = response.choices?.[0]?.message;\n const text = message?.content ?? undefined;\n const toolCalls = (message?.tool_calls ?? []).map((tc) => ({\n name: tc.function?.name ?? \"unknown\",\n input: tc.function?.arguments ? safeParse(tc.function.arguments) : undefined,\n }));\n\n emitGenerationAndTools(trace, spanId, startedAt, {\n text,\n promptTokens: response.usage?.prompt_tokens,\n completionTokens: response.usage?.completion_tokens,\n // OpenAI-compatible providers report thinking tokens here (o-series,\n // DeepSeek, GLM). Absent detail = unreported, not zero.\n reasoningTokens:\n response.usage?.completion_tokens_details?.reasoning_tokens,\n toolCalls,\n taskId,\n turnNumber,\n });\n\n return response;\n } catch (error) {\n emitGenerationAndTools(trace, spanId, startedAt, {\n taskId,\n turnNumber,\n error: { message: error instanceof Error ? error.message : String(error) },\n });\n throw error;\n }\n };\n\n // Proxy that intercepts client.chat.completions.create but passes\n // everything else through unchanged.\n return new Proxy(client, {\n get(target, prop, receiver) {\n if (prop === \"chat\") {\n return new Proxy(target.chat, {\n get(chatTarget, chatProp) {\n if (chatProp === \"completions\") {\n return new Proxy(chatTarget.completions, {\n get(completionsTarget, completionsProp) {\n if (completionsProp === \"create\") {\n return tracedCreate;\n }\n return Reflect.get(completionsTarget, completionsProp);\n },\n });\n }\n return Reflect.get(chatTarget, chatProp);\n },\n });\n }\n return Reflect.get(target, prop, receiver);\n },\n }) as T;\n}\n"],"mappings":";;;;;;;;;;AA0GO,SAAS,gBACd,QACA,SACG;AACH,QAAM,EAAE,OAAO,cAAc,QAAQ,WAAW,IAAI;AACpD,QAAM,aAAa,OAAO,KAAK,YAAY,OAAO,KAAK,OAAO,KAAK,WAAW;AAG9E,QAAM,eAAe,UAAU,SAA8B;AAC3D,UAAM,SAAU,KAAK,CAAC,KAAK,CAAC;AAC5B,UAAM,QAAQ,OAAO,OAAO,SAAS,SAAS;AAG9C,QAAI,OAAO,WAAW,MAAM;AAC1B,aAAO,WAAW,MAAM;AAAA,IAC1B;AAIA,UAAM,UAAU;AAAA,MACb,OAAiC;AAAA,MAClC;AAAA,IACF;AACA,UAAM,EAAE,QAAQ,UAAU,IAAI,gBAAgB,OAAO;AAAA,MACnD;AAAA,MACA,QAAQ,OAAO,OAAO,WAAW,WAAW,OAAO,SAAS;AAAA,MAC5D,UAAU,OAAO;AAAA,MACjB;AAAA,MACA;AAAA,MACA;AAAA,MACA,GAAG;AAAA,IACL,CAAC;AAED,QAAI;AACF,YAAM,WAAY,MAAM,WAAW,MAAM;AACzC,YAAM,UAAU,SAAS,UAAU,CAAC,GAAG;AACvC,YAAM,OAAO,SAAS,WAAW;AACjC,YAAM,aAAa,SAAS,cAAc,CAAC,GAAG,IAAI,CAAC,QAAQ;AAAA,QACzD,MAAM,GAAG,UAAU,QAAQ;AAAA,QAC3B,OAAO,GAAG,UAAU,YAAY,UAAU,GAAG,SAAS,SAAS,IAAI;AAAA,MACrE,EAAE;AAEF,6BAAuB,OAAO,QAAQ,WAAW;AAAA,QAC/C;AAAA,QACA,cAAc,SAAS,OAAO;AAAA,QAC9B,kBAAkB,SAAS,OAAO;AAAA;AAAA;AAAA,QAGlC,iBACE,SAAS,OAAO,2BAA2B;AAAA,QAC7C;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAED,aAAO;AAAA,IACT,SAAS,OAAO;AACd,6BAAuB,OAAO,QAAQ,WAAW;AAAA,QAC/C;AAAA,QACA;AAAA,QACA,OAAO,EAAE,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE;AAAA,MAC3E,CAAC;AACD,YAAM;AAAA,IACR;AAAA,EACF;AAIA,SAAO,IAAI,MAAM,QAAQ;AAAA,IACvB,IAAI,QAAQ,MAAM,UAAU;AAC1B,UAAI,SAAS,QAAQ;AACnB,eAAO,IAAI,MAAM,OAAO,MAAM;AAAA,UAC5B,IAAI,YAAY,UAAU;AACxB,gBAAI,aAAa,eAAe;AAC9B,qBAAO,IAAI,MAAM,WAAW,aAAa;AAAA,gBACvC,IAAI,mBAAmB,iBAAiB;AACtC,sBAAI,oBAAoB,UAAU;AAChC,2BAAO;AAAA,kBACT;AACA,yBAAO,QAAQ,IAAI,mBAAmB,eAAe;AAAA,gBACvD;AAAA,cACF,CAAC;AAAA,YACH;AACA,mBAAO,QAAQ,IAAI,YAAY,QAAQ;AAAA,UACzC;AAAA,QACF,CAAC;AAAA,MACH;AACA,aAAO,QAAQ,IAAI,QAAQ,MAAM,QAAQ;AAAA,IAC3C;AAAA,EACF,CAAC;AACH;","names":[]}
@@ -49,12 +49,46 @@ function emitGenerationAndTools(trace, genSpanId, genStartedAt, opts) {
49
49
  }
50
50
  }
51
51
  }
52
+ function servingHostFromBaseURL(baseURL, defaultVendor) {
53
+ if (typeof baseURL !== "string" || baseURL.trim() === "") {
54
+ return defaultVendor ? { provider: defaultVendor } : {};
55
+ }
56
+ let host;
57
+ try {
58
+ host = new URL(baseURL).hostname;
59
+ } catch {
60
+ return defaultVendor ? { provider: defaultVendor } : {};
61
+ }
62
+ if (host === "localhost" || /^(127\.|0\.0\.0\.0|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/.test(host)) {
63
+ return {};
64
+ }
65
+ const known = {
66
+ "api.openai.com": "openai",
67
+ "api.anthropic.com": "anthropic",
68
+ "openrouter.ai": "openrouter",
69
+ "api.fireworks.ai": "fireworks",
70
+ "api.groq.com": "groq",
71
+ "api.cerebras.ai": "cerebras",
72
+ "api.deepseek.com": "deepseek",
73
+ "api.mistral.ai": "mistral",
74
+ "api.x.ai": "xai",
75
+ "api.together.xyz": "together",
76
+ "generativelanguage.googleapis.com": "google"
77
+ };
78
+ const provider = known[host];
79
+ if (provider) return { provider };
80
+ const labels = host.split(".");
81
+ if (labels.length < 2) return {};
82
+ return { provider: labels.slice(-2).join("."), route: host };
83
+ }
52
84
  function startGeneration(trace, opts) {
53
85
  const spanId = trace.createSpan({
54
86
  task_id: opts.taskId ?? "trace",
55
87
  parent_call_id: opts.parentSpanId ?? trace.rootSpanId,
56
88
  step_name: "agent.generate",
57
89
  model: opts.model,
90
+ provider: opts.provider,
91
+ route: opts.route,
58
92
  observation_type: "GENERATION",
59
93
  input: {
60
94
  ...opts.system !== void 0 ? { system: opts.system } : {},
@@ -71,6 +105,7 @@ function startGeneration(trace, opts) {
71
105
  export {
72
106
  safeParse,
73
107
  emitGenerationAndTools,
108
+ servingHostFromBaseURL,
74
109
  startGeneration
75
110
  };
76
- //# sourceMappingURL=chunk-3OQ3JBFJ.js.map
111
+ //# sourceMappingURL=chunk-Z64A566W.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/agent-task/integrations/span-helpers.ts"],"sourcesContent":["/**\n * Shared span-emission helpers for the tracing integrations.\n *\n * The OpenAI and Anthropic wrappers both need to:\n * 1. Create a GENERATION span before the LLM call\n * 2. End it with text/tokens/latency after the response\n * 3. Emit a TOOL span for each tool call in the response\n *\n * These helpers keep that logic in one place so the wrappers stay thin.\n *\n * @internal\n */\n\nimport type { AgentTaskTraceContext } from \"../tracing.ts\";\n\nfunction monotonicNowMs(): number {\n if (typeof performance !== \"undefined\" && typeof performance.now === \"function\") {\n return performance.now();\n }\n return Date.now();\n}\n\nfunction round3(n: number): number {\n return Math.round(n * 1000) / 1000;\n}\n\nfunction safeParse(raw: unknown): unknown {\n if (typeof raw !== \"string\") return raw;\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n}\n\n/**\n * Emit a GENERATION span + TOOL spans for a completed LLM call.\n *\n * Both wrappers call this after the SDK returns the response. It:\n * 1. Ends the GENERATION span (created by {@link startGeneration}) with\n * text, token counts, and latency.\n * 2. Emits a TOOL span per tool call, so `t.calledTool(name)` works.\n */\nexport function emitGenerationAndTools(\n trace: AgentTaskTraceContext,\n genSpanId: string,\n genStartedAt: number,\n opts: {\n text?: string;\n promptTokens?: number;\n completionTokens?: number;\n /** Reasoning tokens, when the provider reports the dimension. */\n reasoningTokens?: number;\n toolCalls?: Array<{ name: string; input?: unknown }>;\n taskId?: string;\n turnNumber?: number;\n error?: { message: string };\n },\n): void {\n const latency = round3(monotonicNowMs() - genStartedAt);\n const isError = !!opts.error;\n\n // End the GENERATION span\n trace.endSpan(genSpanId, {\n latency_ms: latency,\n prompt_tokens: opts.promptTokens,\n completion_tokens: opts.completionTokens,\n reasoning_tokens: opts.reasoningTokens,\n output: {\n ...(opts.text !== undefined ? { text: opts.text } : {}),\n ...(opts.error ? { error: opts.error.message } : {}),\n },\n ...(isError\n ? { level: \"ERROR\" as const, status_message: opts.error!.message }\n : {}),\n });\n\n // Emit TOOL spans (only on success — errors are captured on the GENERATION span)\n if (!isError && opts.toolCalls) {\n for (const tc of opts.toolCalls) {\n const toolSpanId = trace.createSpan({\n task_id: opts.taskId ?? \"trace\",\n parent_call_id: genSpanId,\n step_name: tc.name,\n observation_type: \"TOOL\",\n ...(tc.input !== undefined\n ? {\n input:\n tc.input && typeof tc.input === \"object\"\n ? (tc.input as Record<string, unknown>)\n : { value: tc.input },\n }\n : {}),\n metadata: { toolName: tc.name },\n });\n trace.endSpan(toolSpanId, {});\n }\n }\n}\n\n/**\n * Resolve the serving host a client actually talks to, from its baseURL.\n *\n * The vendor default (no baseURL) means the SDK's own endpoint. A custom\n * baseURL is the serving host itself — for a known gateway the provider is\n * its name; for anything else the registered domain is the provider and the\n * full host the route, which is exactly the split the backend stores\n * (`gen_ai.provider.name` / `apo.llm.route`, issue #307). Local/private\n * hosts are proxies in front of an unknown backend — report nothing rather\n * than a wrong host.\n */\nexport function servingHostFromBaseURL(\n baseURL: unknown,\n defaultVendor?: string,\n): { provider?: string; route?: string } {\n if (typeof baseURL !== \"string\" || baseURL.trim() === \"\") {\n return defaultVendor ? { provider: defaultVendor } : {};\n }\n let host: string;\n try {\n host = new URL(baseURL).hostname;\n } catch {\n return defaultVendor ? { provider: defaultVendor } : {};\n }\n if (\n host === \"localhost\" ||\n /^(127\\.|0\\.0\\.0\\.0|10\\.|192\\.168\\.|172\\.(1[6-9]|2\\d|3[01])\\.)/.test(host)\n ) {\n return {};\n }\n const known: Record<string, string> = {\n \"api.openai.com\": \"openai\",\n \"api.anthropic.com\": \"anthropic\",\n \"openrouter.ai\": \"openrouter\",\n \"api.fireworks.ai\": \"fireworks\",\n \"api.groq.com\": \"groq\",\n \"api.cerebras.ai\": \"cerebras\",\n \"api.deepseek.com\": \"deepseek\",\n \"api.mistral.ai\": \"mistral\",\n \"api.x.ai\": \"xai\",\n \"api.together.xyz\": \"together\",\n \"generativelanguage.googleapis.com\": \"google\",\n };\n const provider = known[host];\n if (provider) return { provider };\n const labels = host.split(\".\");\n if (labels.length < 2) return {};\n return { provider: labels.slice(-2).join(\".\"), route: host };\n}\n\n/**\n * Create a GENERATION span before the LLM call.\n * Returns `{ spanId, startedAt }` to pass to {@link emitGenerationAndTools}.\n */\nexport function startGeneration(\n trace: AgentTaskTraceContext,\n opts: {\n model: string;\n system?: string;\n messages?: unknown;\n parentSpanId?: string;\n taskId?: string;\n turnNumber?: number;\n /** Serving host attributes for this generation (issue #307). */\n provider?: string;\n route?: string;\n },\n): { spanId: string; startedAt: number } {\n const spanId = trace.createSpan({\n task_id: opts.taskId ?? \"trace\",\n parent_call_id: opts.parentSpanId ?? trace.rootSpanId,\n step_name: \"agent.generate\",\n model: opts.model,\n provider: opts.provider,\n route: opts.route,\n observation_type: \"GENERATION\",\n input: {\n ...(opts.system !== undefined ? { system: opts.system } : {}),\n ...(opts.messages !== undefined ? { messages: opts.messages } : {}),\n },\n metadata: {\n ...(opts.turnNumber !== undefined ? { turnNumber: opts.turnNumber } : {}),\n ...(opts.taskId ? { taskId: opts.taskId } : {}),\n },\n });\n return { spanId, startedAt: monotonicNowMs() };\n}\n\n// Re-export safeParse for the wrappers (OpenAI arguments is a JSON string)\nexport { safeParse };\n"],"mappings":"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","names":[]}
@@ -22,6 +22,14 @@ interface CreateSpanParams {
22
22
  step_index?: number;
23
23
  version?: string;
24
24
  model?: string;
25
+ /** Serving provider of the model, emitted as `gen_ai.provider.name` so
26
+ * runs can be labeled and compared by host (issue #307). Only set when
27
+ * actually known (vendor SDK or baseURL) — never guessed from the model
28
+ * id; the backend renders absent as unknown. */
29
+ provider?: string;
30
+ /** Finer-grained serving route (e.g. a custom gateway host), emitted as
31
+ * `apo.llm.route`. */
32
+ route?: string;
25
33
  /** Tool name for TOOL observations; emitted as `gen_ai.tool.name`. */
26
34
  tool_name?: string;
27
35
  input?: Record<string, unknown>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apo-ai/sdk",
3
- "version": "0.9.3",
3
+ "version": "0.10.1",
4
4
  "description": "TypeScript SDK for executable agent capability specifications and harness engineering with Apo",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/agent-task/integrations/openai.ts"],"sourcesContent":["/**\n * OpenAI JS SDK tracing integration for apo.\n *\n * Wraps an `OpenAI` client so `chat.completions.create()` calls are\n * automatically traced. Tool calls and generations are captured without\n * any manual span code in the adapter.\n *\n * Uses a Proxy to intercept the `create` method, then emits GENERATION +\n * TOOL spans through the trace context, where they become the observations\n * that `t.calledTool` reads.\n *\n * @example\n * ```ts\n * import OpenAI from \"openai\";\n * import { createApoOpenAI } from \"@apo-ai/sdk/agent-task\";\n *\n * async sendUserTurn(turn, { trace, parentSpanId }) {\n * const client = createApoOpenAI(\n * new OpenAI({ apiKey, baseURL: \"https://openrouter.ai/api/v1\" }),\n * { trace, parentSpanId, taskId: ctx.task.id, turnNumber }\n * );\n * const response = await client.chat.completions.create({\n * model: \"deepseek/deepseek-v4.1-flash\",\n * messages: [{ role: \"user\", content: String(turn) }],\n * tools: [...],\n * });\n * return { response: response.choices[0].message.content ?? \"\" };\n * }\n * ```\n *\n * @module\n */\n\nimport type { AgentTaskTraceContext } from \"../tracing.ts\";\nimport { startGeneration, emitGenerationAndTools, safeParse } from \"./span-helpers.ts\";\n\nexport interface CreateApoOpenAIOptions {\n /** The tee'd trace context from `sendUserTurn`'s second argument. */\n trace: AgentTaskTraceContext;\n /** The parent span id for this turn (from `sendUserTurn`'s context). */\n parentSpanId?: string;\n /** Optional task id, surfaced in span metadata. */\n taskId?: string;\n /** Optional turn number, surfaced in span metadata. */\n turnNumber?: number;\n}\n\n/**\n * Minimal structural shape of the OpenAI client we need to intercept.\n * We avoid importing `openai` at the type level so this module doesn't\n * take a runtime dependency — the wrapper works with any client that has\n * `chat.completions.create`.\n *\n * The `create` signature uses a broad type so the OpenAI SDK's overloaded\n * `create` method (which has specific param types) is assignable to this.\n */\ninterface OpenAIClientLike {\n chat: {\n completions: {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n create: (...args: any[]) => Promise<any>;\n };\n };\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [key: string]: any;\n}\n\n/**\n * The response shape we read from `chat.completions.create()`.\n */\ninterface OpenAIResponseLike {\n model?: string;\n choices?: Array<{\n message?: {\n content?: string | null;\n tool_calls?: Array<{\n function?: { name?: string; arguments?: string };\n }>;\n };\n }>;\n usage?: {\n prompt_tokens?: number;\n completion_tokens?: number;\n completion_tokens_details?: {\n /** Thinking tokens, reported by o-series/DeepSeek/GLM-style providers. */\n reasoning_tokens?: number;\n };\n };\n}\n\n/**\n * Wrap an OpenAI client so `chat.completions.create()` is traced.\n * Returns a Proxy-wrapped client — call it exactly as you would the\n * original. Non-streaming only; streaming responses pass through\n * untraced.\n *\n * For SDKs that emit OpenTelemetry natively (OpenAI Agents SDK, Vercel AI\n * SDK, Claude Agent SDK), prefer `configureApoTelemetry()` instead — no\n * wrapper needed. This manual wrapper is for the raw `openai` package or\n * custom clients that don't emit OTel on their own.\n */\nexport function createApoOpenAI<T extends OpenAIClientLike>(\n client: T,\n options: CreateApoOpenAIOptions,\n): T {\n const { trace, parentSpanId, taskId, turnNumber } = options;\n const realCreate = client.chat.completions.create.bind(client.chat.completions);\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const tracedCreate = async (...args: any[]): Promise<any> => {\n const params = (args[0] ?? {}) as Record<string, unknown>;\n const model = String(params.model ?? \"unknown\");\n\n // Non-streaming: trace fully. Streaming (stream: true): pass through.\n if (params.stream === true) {\n return realCreate(params);\n }\n\n const { spanId, startedAt } = startGeneration(trace, {\n model,\n system: typeof params.system === \"string\" ? params.system : undefined,\n messages: params.messages,\n parentSpanId,\n taskId,\n turnNumber,\n });\n\n try {\n const response = (await realCreate(params)) as OpenAIResponseLike;\n const message = response.choices?.[0]?.message;\n const text = message?.content ?? undefined;\n const toolCalls = (message?.tool_calls ?? []).map((tc) => ({\n name: tc.function?.name ?? \"unknown\",\n input: tc.function?.arguments ? safeParse(tc.function.arguments) : undefined,\n }));\n\n emitGenerationAndTools(trace, spanId, startedAt, {\n text,\n promptTokens: response.usage?.prompt_tokens,\n completionTokens: response.usage?.completion_tokens,\n // OpenAI-compatible providers report thinking tokens here (o-series,\n // DeepSeek, GLM). Absent detail = unreported, not zero.\n reasoningTokens:\n response.usage?.completion_tokens_details?.reasoning_tokens,\n toolCalls,\n taskId,\n turnNumber,\n });\n\n return response;\n } catch (error) {\n emitGenerationAndTools(trace, spanId, startedAt, {\n taskId,\n turnNumber,\n error: { message: error instanceof Error ? error.message : String(error) },\n });\n throw error;\n }\n };\n\n // Proxy that intercepts client.chat.completions.create but passes\n // everything else through unchanged.\n return new Proxy(client, {\n get(target, prop, receiver) {\n if (prop === \"chat\") {\n return new Proxy(target.chat, {\n get(chatTarget, chatProp) {\n if (chatProp === \"completions\") {\n return new Proxy(chatTarget.completions, {\n get(completionsTarget, completionsProp) {\n if (completionsProp === \"create\") {\n return tracedCreate;\n }\n return Reflect.get(completionsTarget, completionsProp);\n },\n });\n }\n return Reflect.get(chatTarget, chatProp);\n },\n });\n }\n return Reflect.get(target, prop, receiver);\n },\n }) as T;\n}\n"],"mappings":";;;;;;;;;AAqGO,SAAS,gBACd,QACA,SACG;AACH,QAAM,EAAE,OAAO,cAAc,QAAQ,WAAW,IAAI;AACpD,QAAM,aAAa,OAAO,KAAK,YAAY,OAAO,KAAK,OAAO,KAAK,WAAW;AAG9E,QAAM,eAAe,UAAU,SAA8B;AAC3D,UAAM,SAAU,KAAK,CAAC,KAAK,CAAC;AAC5B,UAAM,QAAQ,OAAO,OAAO,SAAS,SAAS;AAG9C,QAAI,OAAO,WAAW,MAAM;AAC1B,aAAO,WAAW,MAAM;AAAA,IAC1B;AAEA,UAAM,EAAE,QAAQ,UAAU,IAAI,gBAAgB,OAAO;AAAA,MACnD;AAAA,MACA,QAAQ,OAAO,OAAO,WAAW,WAAW,OAAO,SAAS;AAAA,MAC5D,UAAU,OAAO;AAAA,MACjB;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAED,QAAI;AACF,YAAM,WAAY,MAAM,WAAW,MAAM;AACzC,YAAM,UAAU,SAAS,UAAU,CAAC,GAAG;AACvC,YAAM,OAAO,SAAS,WAAW;AACjC,YAAM,aAAa,SAAS,cAAc,CAAC,GAAG,IAAI,CAAC,QAAQ;AAAA,QACzD,MAAM,GAAG,UAAU,QAAQ;AAAA,QAC3B,OAAO,GAAG,UAAU,YAAY,UAAU,GAAG,SAAS,SAAS,IAAI;AAAA,MACrE,EAAE;AAEF,6BAAuB,OAAO,QAAQ,WAAW;AAAA,QAC/C;AAAA,QACA,cAAc,SAAS,OAAO;AAAA,QAC9B,kBAAkB,SAAS,OAAO;AAAA;AAAA;AAAA,QAGlC,iBACE,SAAS,OAAO,2BAA2B;AAAA,QAC7C;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAED,aAAO;AAAA,IACT,SAAS,OAAO;AACd,6BAAuB,OAAO,QAAQ,WAAW;AAAA,QAC/C;AAAA,QACA;AAAA,QACA,OAAO,EAAE,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE;AAAA,MAC3E,CAAC;AACD,YAAM;AAAA,IACR;AAAA,EACF;AAIA,SAAO,IAAI,MAAM,QAAQ;AAAA,IACvB,IAAI,QAAQ,MAAM,UAAU;AAC1B,UAAI,SAAS,QAAQ;AACnB,eAAO,IAAI,MAAM,OAAO,MAAM;AAAA,UAC5B,IAAI,YAAY,UAAU;AACxB,gBAAI,aAAa,eAAe;AAC9B,qBAAO,IAAI,MAAM,WAAW,aAAa;AAAA,gBACvC,IAAI,mBAAmB,iBAAiB;AACtC,sBAAI,oBAAoB,UAAU;AAChC,2BAAO;AAAA,kBACT;AACA,yBAAO,QAAQ,IAAI,mBAAmB,eAAe;AAAA,gBACvD;AAAA,cACF,CAAC;AAAA,YACH;AACA,mBAAO,QAAQ,IAAI,YAAY,QAAQ;AAAA,UACzC;AAAA,QACF,CAAC;AAAA,MACH;AACA,aAAO,QAAQ,IAAI,QAAQ,MAAM,QAAQ;AAAA,IAC3C;AAAA,EACF,CAAC;AACH;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/agent-task/integrations/span-helpers.ts"],"sourcesContent":["/**\n * Shared span-emission helpers for the tracing integrations.\n *\n * The OpenAI and Anthropic wrappers both need to:\n * 1. Create a GENERATION span before the LLM call\n * 2. End it with text/tokens/latency after the response\n * 3. Emit a TOOL span for each tool call in the response\n *\n * These helpers keep that logic in one place so the wrappers stay thin.\n *\n * @internal\n */\n\nimport type { AgentTaskTraceContext } from \"../tracing.ts\";\n\nfunction monotonicNowMs(): number {\n if (typeof performance !== \"undefined\" && typeof performance.now === \"function\") {\n return performance.now();\n }\n return Date.now();\n}\n\nfunction round3(n: number): number {\n return Math.round(n * 1000) / 1000;\n}\n\nfunction safeParse(raw: unknown): unknown {\n if (typeof raw !== \"string\") return raw;\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n}\n\n/**\n * Emit a GENERATION span + TOOL spans for a completed LLM call.\n *\n * Both wrappers call this after the SDK returns the response. It:\n * 1. Ends the GENERATION span (created by {@link startGeneration}) with\n * text, token counts, and latency.\n * 2. Emits a TOOL span per tool call, so `t.calledTool(name)` works.\n */\nexport function emitGenerationAndTools(\n trace: AgentTaskTraceContext,\n genSpanId: string,\n genStartedAt: number,\n opts: {\n text?: string;\n promptTokens?: number;\n completionTokens?: number;\n /** Reasoning tokens, when the provider reports the dimension. */\n reasoningTokens?: number;\n toolCalls?: Array<{ name: string; input?: unknown }>;\n taskId?: string;\n turnNumber?: number;\n error?: { message: string };\n },\n): void {\n const latency = round3(monotonicNowMs() - genStartedAt);\n const isError = !!opts.error;\n\n // End the GENERATION span\n trace.endSpan(genSpanId, {\n latency_ms: latency,\n prompt_tokens: opts.promptTokens,\n completion_tokens: opts.completionTokens,\n reasoning_tokens: opts.reasoningTokens,\n output: {\n ...(opts.text !== undefined ? { text: opts.text } : {}),\n ...(opts.error ? { error: opts.error.message } : {}),\n },\n ...(isError\n ? { level: \"ERROR\" as const, status_message: opts.error!.message }\n : {}),\n });\n\n // Emit TOOL spans (only on success — errors are captured on the GENERATION span)\n if (!isError && opts.toolCalls) {\n for (const tc of opts.toolCalls) {\n const toolSpanId = trace.createSpan({\n task_id: opts.taskId ?? \"trace\",\n parent_call_id: genSpanId,\n step_name: tc.name,\n observation_type: \"TOOL\",\n ...(tc.input !== undefined\n ? {\n input:\n tc.input && typeof tc.input === \"object\"\n ? (tc.input as Record<string, unknown>)\n : { value: tc.input },\n }\n : {}),\n metadata: { toolName: tc.name },\n });\n trace.endSpan(toolSpanId, {});\n }\n }\n}\n\n/**\n * Create a GENERATION span before the LLM call.\n * Returns `{ spanId, startedAt }` to pass to {@link emitGenerationAndTools}.\n */\nexport function startGeneration(\n trace: AgentTaskTraceContext,\n opts: {\n model: string;\n system?: string;\n messages?: unknown;\n parentSpanId?: string;\n taskId?: string;\n turnNumber?: number;\n },\n): { spanId: string; startedAt: number } {\n const spanId = trace.createSpan({\n task_id: opts.taskId ?? \"trace\",\n parent_call_id: opts.parentSpanId ?? trace.rootSpanId,\n step_name: \"agent.generate\",\n model: opts.model,\n observation_type: \"GENERATION\",\n input: {\n ...(opts.system !== undefined ? { system: opts.system } : {}),\n ...(opts.messages !== undefined ? { messages: opts.messages } : {}),\n },\n metadata: {\n ...(opts.turnNumber !== undefined ? { turnNumber: opts.turnNumber } : {}),\n ...(opts.taskId ? { taskId: opts.taskId } : {}),\n },\n });\n return { spanId, startedAt: monotonicNowMs() };\n}\n\n// Re-export safeParse for the wrappers (OpenAI arguments is a JSON string)\nexport { safeParse };\n"],"mappings":";;;;AAeA,SAAS,iBAAyB;AAChC,MAAI,OAAO,gBAAgB,eAAe,OAAO,YAAY,QAAQ,YAAY;AAC/E,WAAO,YAAY,IAAI;AAAA,EACzB;AACA,SAAO,KAAK,IAAI;AAClB;AAEA,SAAS,OAAO,GAAmB;AACjC,SAAO,KAAK,MAAM,IAAI,GAAI,IAAI;AAChC;AAEA,SAAS,UAAU,KAAuB;AACxC,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,MAAI;AACF,WAAO,KAAK,MAAM,GAAG;AAAA,EACvB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAUO,SAAS,uBACd,OACA,WACA,cACA,MAWM;AACN,QAAM,UAAU,OAAO,eAAe,IAAI,YAAY;AACtD,QAAM,UAAU,CAAC,CAAC,KAAK;AAGvB,QAAM,QAAQ,WAAW;AAAA,IACvB,YAAY;AAAA,IACZ,eAAe,KAAK;AAAA,IACpB,mBAAmB,KAAK;AAAA,IACxB,kBAAkB,KAAK;AAAA,IACvB,QAAQ;AAAA,MACN,GAAI,KAAK,SAAS,SAAY,EAAE,MAAM,KAAK,KAAK,IAAI,CAAC;AAAA,MACrD,GAAI,KAAK,QAAQ,EAAE,OAAO,KAAK,MAAM,QAAQ,IAAI,CAAC;AAAA,IACpD;AAAA,IACA,GAAI,UACA,EAAE,OAAO,SAAkB,gBAAgB,KAAK,MAAO,QAAQ,IAC/D,CAAC;AAAA,EACP,CAAC;AAGD,MAAI,CAAC,WAAW,KAAK,WAAW;AAC9B,eAAW,MAAM,KAAK,WAAW;AAC/B,YAAM,aAAa,MAAM,WAAW;AAAA,QAClC,SAAS,KAAK,UAAU;AAAA,QACxB,gBAAgB;AAAA,QAChB,WAAW,GAAG;AAAA,QACd,kBAAkB;AAAA,QAClB,GAAI,GAAG,UAAU,SACb;AAAA,UACE,OACE,GAAG,SAAS,OAAO,GAAG,UAAU,WAC3B,GAAG,QACJ,EAAE,OAAO,GAAG,MAAM;AAAA,QAC1B,IACA,CAAC;AAAA,QACL,UAAU,EAAE,UAAU,GAAG,KAAK;AAAA,MAChC,CAAC;AACD,YAAM,QAAQ,YAAY,CAAC,CAAC;AAAA,IAC9B;AAAA,EACF;AACF;AAMO,SAAS,gBACd,OACA,MAQuC;AACvC,QAAM,SAAS,MAAM,WAAW;AAAA,IAC9B,SAAS,KAAK,UAAU;AAAA,IACxB,gBAAgB,KAAK,gBAAgB,MAAM;AAAA,IAC3C,WAAW;AAAA,IACX,OAAO,KAAK;AAAA,IACZ,kBAAkB;AAAA,IAClB,OAAO;AAAA,MACL,GAAI,KAAK,WAAW,SAAY,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;AAAA,MAC3D,GAAI,KAAK,aAAa,SAAY,EAAE,UAAU,KAAK,SAAS,IAAI,CAAC;AAAA,IACnE;AAAA,IACA,UAAU;AAAA,MACR,GAAI,KAAK,eAAe,SAAY,EAAE,YAAY,KAAK,WAAW,IAAI,CAAC;AAAA,MACvE,GAAI,KAAK,SAAS,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;AAAA,IAC/C;AAAA,EACF,CAAC;AACD,SAAO,EAAE,QAAQ,WAAW,eAAe,EAAE;AAC/C;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/agent-task/integrations/anthropic.ts"],"sourcesContent":["/**\n * Anthropic JS SDK tracing integration for apo.\n *\n * Wraps an `Anthropic` client so `messages.create()` calls are\n * automatically traced. Tool calls and generations are captured without\n * any manual span code in the adapter.\n *\n * @example\n * ```ts\n * import Anthropic from \"@anthropic-ai/sdk\";\n * import { createApoAnthropic } from \"@apo-ai/sdk/agent-task\";\n *\n * async sendUserTurn(turn, { trace, parentSpanId }) {\n * const client = createApoAnthropic(\n * new Anthropic({ apiKey }),\n * { trace, parentSpanId, taskId: ctx.task.id, turnNumber }\n * );\n * const response = await client.messages.create({\n * model: \"claude-sonnet-4-20250514\",\n * max_tokens: 1024,\n * messages: [{ role: \"user\", content: String(turn) }],\n * tools: [...],\n * });\n * const text = response.content.filter(b => b.type === \"text\").map(b => b.text).join(\"\");\n * return { response: text };\n * }\n * ```\n *\n * @module\n */\n\nimport type { AgentTaskTraceContext } from \"../tracing.ts\";\nimport { startGeneration, emitGenerationAndTools } from \"./span-helpers.ts\";\n\nexport interface CreateApoAnthropicOptions {\n /** The tee'd trace context from `sendUserTurn`'s second argument. */\n trace: AgentTaskTraceContext;\n /** The parent span id for this turn (from `sendUserTurn`'s context). */\n parentSpanId?: string;\n /** Optional task id, surfaced in span metadata. */\n taskId?: string;\n /** Optional turn number, surfaced in span metadata. */\n turnNumber?: number;\n}\n\n/**\n * Minimal structural shape of the Anthropic client we intercept.\n */\ninterface AnthropicClientLike {\n messages: {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n create: (...args: any[]) => Promise<any>;\n };\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [key: string]: any;\n}\n\n/**\n * The response shape we read from `messages.create()`.\n */\ninterface AnthropicResponseLike {\n model?: string;\n content?: Array<\n | { type: \"text\"; text: string }\n | { type: \"tool_use\"; name: string; input?: unknown }\n | { type: string }\n >;\n usage?: {\n input_tokens?: number;\n output_tokens?: number;\n };\n}\n\n/**\n * Wrap an Anthropic client so `messages.create()` is traced.\n * Returns a Proxy-wrapped client — call it exactly as you would the\n * original. Non-streaming only; streaming responses pass through\n * untraced.\n *\n * For SDKs that emit OpenTelemetry natively (Claude Agent SDK, Vercel AI\n * SDK), prefer `configureApoTelemetry()` instead — no wrapper needed.\n * This manual wrapper is for the raw `@anthropic-ai/sdk` package or\n * custom clients that don't emit OTel on their own.\n */\nexport function createApoAnthropic<T extends AnthropicClientLike>(\n client: T,\n options: CreateApoAnthropicOptions,\n): T {\n const { trace, parentSpanId, taskId, turnNumber } = options;\n const realCreate = client.messages.create.bind(client.messages);\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const tracedCreate = async (...args: any[]): Promise<any> => {\n const params = (args[0] ?? {}) as Record<string, unknown>;\n const model = String(params.model ?? \"unknown\");\n\n // Non-streaming: trace fully. Streaming (stream: true): pass through.\n if (params.stream === true) {\n return realCreate(params);\n }\n\n const { spanId, startedAt } = startGeneration(trace, {\n model,\n system: typeof params.system === \"string\" ? params.system : undefined,\n messages: params.messages,\n parentSpanId,\n taskId,\n turnNumber,\n });\n\n try {\n const response = (await realCreate(params)) as AnthropicResponseLike;\n const blocks = response.content ?? [];\n const text = blocks\n .filter((b): b is { type: \"text\"; text: string } => b.type === \"text\")\n .map((b) => b.text)\n .join(\"\\n\");\n const toolCalls = blocks\n .filter(\n (b): b is { type: \"tool_use\"; name: string; input?: unknown } =>\n b.type === \"tool_use\",\n )\n .map((b) => ({\n name: b.name,\n input: b.input,\n }));\n\n emitGenerationAndTools(trace, spanId, startedAt, {\n text: text || undefined,\n promptTokens: response.usage?.input_tokens,\n completionTokens: response.usage?.output_tokens,\n toolCalls,\n taskId,\n turnNumber,\n });\n\n return response;\n } catch (error) {\n emitGenerationAndTools(trace, spanId, startedAt, {\n taskId,\n turnNumber,\n error: { message: error instanceof Error ? error.message : String(error) },\n });\n throw error;\n }\n };\n\n // Proxy that intercepts client.messages.create but passes\n // everything else through unchanged.\n return new Proxy(client, {\n get(target, prop, receiver) {\n if (prop === \"messages\") {\n return new Proxy(target.messages, {\n get(messagesTarget, messagesProp) {\n if (messagesProp === \"create\") {\n return tracedCreate;\n }\n return Reflect.get(messagesTarget, messagesProp);\n },\n });\n }\n return Reflect.get(target, prop, receiver);\n },\n }) as T;\n}\n"],"mappings":";;;;;;;;AAoFO,SAAS,mBACd,QACA,SACG;AACH,QAAM,EAAE,OAAO,cAAc,QAAQ,WAAW,IAAI;AACpD,QAAM,aAAa,OAAO,SAAS,OAAO,KAAK,OAAO,QAAQ;AAG9D,QAAM,eAAe,UAAU,SAA8B;AAC3D,UAAM,SAAU,KAAK,CAAC,KAAK,CAAC;AAC5B,UAAM,QAAQ,OAAO,OAAO,SAAS,SAAS;AAG9C,QAAI,OAAO,WAAW,MAAM;AAC1B,aAAO,WAAW,MAAM;AAAA,IAC1B;AAEA,UAAM,EAAE,QAAQ,UAAU,IAAI,gBAAgB,OAAO;AAAA,MACnD;AAAA,MACA,QAAQ,OAAO,OAAO,WAAW,WAAW,OAAO,SAAS;AAAA,MAC5D,UAAU,OAAO;AAAA,MACjB;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAED,QAAI;AACF,YAAM,WAAY,MAAM,WAAW,MAAM;AACzC,YAAM,SAAS,SAAS,WAAW,CAAC;AACpC,YAAM,OAAO,OACV,OAAO,CAAC,MAA2C,EAAE,SAAS,MAAM,EACpE,IAAI,CAAC,MAAM,EAAE,IAAI,EACjB,KAAK,IAAI;AACZ,YAAM,YAAY,OACf;AAAA,QACC,CAAC,MACC,EAAE,SAAS;AAAA,MACf,EACC,IAAI,CAAC,OAAO;AAAA,QACX,MAAM,EAAE;AAAA,QACR,OAAO,EAAE;AAAA,MACX,EAAE;AAEJ,6BAAuB,OAAO,QAAQ,WAAW;AAAA,QAC/C,MAAM,QAAQ;AAAA,QACd,cAAc,SAAS,OAAO;AAAA,QAC9B,kBAAkB,SAAS,OAAO;AAAA,QAClC;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAED,aAAO;AAAA,IACT,SAAS,OAAO;AACd,6BAAuB,OAAO,QAAQ,WAAW;AAAA,QAC/C;AAAA,QACA;AAAA,QACA,OAAO,EAAE,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE;AAAA,MAC3E,CAAC;AACD,YAAM;AAAA,IACR;AAAA,EACF;AAIA,SAAO,IAAI,MAAM,QAAQ;AAAA,IACvB,IAAI,QAAQ,MAAM,UAAU;AAC1B,UAAI,SAAS,YAAY;AACvB,eAAO,IAAI,MAAM,OAAO,UAAU;AAAA,UAChC,IAAI,gBAAgB,cAAc;AAChC,gBAAI,iBAAiB,UAAU;AAC7B,qBAAO;AAAA,YACT;AACA,mBAAO,QAAQ,IAAI,gBAAgB,YAAY;AAAA,UACjD;AAAA,QACF,CAAC;AAAA,MACH;AACA,aAAO,QAAQ,IAAI,QAAQ,MAAM,QAAQ;AAAA,IAC3C;AAAA,EACF,CAAC;AACH;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/agent-task/integrations/ai-sdk.ts"],"sourcesContent":["/**\n * Vercel AI SDK tracing integration for apo.\n *\n * Pass the result of {@link createApoTracer} to `generateText`'s\n * `experimental_telemetry` option. The AI SDK already emits OpenTelemetry\n * spans natively (e.g. `ai.toolCall`, `ai.generateText`); this tracer\n * intercepts them and translates them into apo trace spans, where they\n * become the TOOL/GENERATION observations that `t.calledTool`,\n * `t.noFailedActions`, and `t.messageIncludes` read.\n *\n * The adapter author writes zero manual span code.\n *\n * @example\n * ```ts\n * import { generateText } from \"ai\";\n * import { createApoTracer } from \"@apo-ai/sdk/agent-task\";\n *\n * async sendUserTurn(turn, { trace, parentSpanId }) {\n * const result = await generateText({\n * model, system, messages, tools,\n * experimental_telemetry: createApoTracer({ trace, parentSpanId }),\n * });\n * return { response: result.text };\n * }\n * ```\n *\n * @module\n */\n\nimport type { AgentTaskTraceContext } from \"../tracing.ts\";\nimport { extractTokenUsage } from \"./token-usage.ts\";\n\n// ── OpenTelemetry-compatible types ───────────────────────────────────────\n// We implement the subset of the OTel `Tracer` and `Span` interfaces that the\n// Vercel AI SDK touches (verified from `ai@6` source: `recordSpan` uses\n// `startActiveSpan`; the AI SDK also calls `startSpan` on the default tracer\n// for some operations). We re-declare the structural shape here so the\n// `experimental_telemetry: { tracer }` option type-checks against the AI\n// SDK's `TelemetrySettings` without taking a runtime dep on `@opentelemetry/api`.\n\ntype OTelAttributeValue =\n | string\n | number\n | boolean\n | null\n | undefined\n | Array<string | number | boolean | null | undefined>;\ntype OTelAttributes = Record<string, OTelAttributeValue>;\n\ninterface OTelSpanContext {\n traceId: string;\n spanId: string;\n traceFlags: number;\n}\n\ninterface OTelSpanStatus {\n code: number; // 0 = UNSET, 1 = OK, 2 = ERROR\n message?: string;\n}\n\ninterface OTelException {\n name: string;\n message: string;\n stack?: string;\n}\n\n/**\n * Minimal `Span` implementation. The AI SDK calls:\n * - `setAttributes` / `setAttribute` (tool result, response text)\n * - `setStatus` / `recordException` (errors)\n * - `end()` (finalises the span)\n *\n * Other methods (`addEvent`, `addLink`, `updateName`) are no-ops but must\n * exist on the object for structural type compatibility with the OTel `Span`\n * interface the AI SDK expects.\n */\ninterface ApoSpan {\n spanContext(): OTelSpanContext;\n setAttribute(key: string, value: OTelAttributeValue): this;\n setAttributes(attrs: OTelAttributes): this;\n addEvent(name: string, attributesOrStartTime?: unknown, startTime?: unknown): this;\n addLink(link: unknown): this;\n addLinks(links: unknown[]): this;\n setStatus(status: OTelSpanStatus): this;\n updateName(name: string): this;\n end(endTime?: unknown): void;\n isRecording(): boolean;\n recordException(exception: OTelException | string, time?: unknown): this;\n}\n\n/**\n * Minimal `Tracer` implementation. The AI SDK's `recordSpan` uses\n * `startActiveSpan(name, { attributes }, fn)`. We also implement\n * `startSpan(name, opts)` for structural compatibility — it returns a span\n * without invoking a callback, and the caller is responsible for `.end()`.\n */\ninterface ApoTracer {\n startSpan(name: string, options?: { attributes?: OTelAttributes }): ApoSpan;\n startActiveSpan<T>(\n name: string,\n options: { attributes?: OTelAttributes },\n fn: (span: ApoSpan) => Promise<T> | T,\n ): Promise<T>;\n startActiveSpan<T>(\n name: string,\n options: { attributes?: OTelAttributes },\n context: unknown,\n fn: (span: ApoSpan) => Promise<T> | T,\n ): Promise<T>;\n startActiveSpan<T>(\n name: string,\n fn: (span: ApoSpan) => Promise<T> | T,\n ): Promise<T>;\n}\n\n// ── helpers ──────────────────────────────────────────────────────────────\n\nfunction safeParse(raw: unknown): unknown {\n if (typeof raw !== \"string\") return raw;\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n}\n\nfunction monotonicNowMs(): number {\n if (typeof performance !== \"undefined\" && typeof performance.now === \"function\") {\n return performance.now();\n }\n return Date.now();\n}\n\nfunction round3(n: number): number {\n return Math.round(n * 1000) / 1000;\n}\n\nconst SPAN_ID_PREFIX = \"ai-sdk-\";\nlet spanCounter = 0;\n\nfunction nextSpanId(): string {\n spanCounter += 1;\n return `${SPAN_ID_PREFIX}${spanCounter}`;\n}\n\n// ── createApoTracer ──────────────────────────────────────────────────────\n\nexport interface CreateApoTracerOptions {\n /** The tee'd trace context from `sendUserTurn`'s second argument. */\n trace: AgentTaskTraceContext;\n /** The parent span id for this turn (from `sendUserTurn`'s context). */\n parentSpanId?: string;\n /** Optional task id, surfaced in span metadata. */\n taskId?: string;\n /** Optional turn number, surfaced in span metadata. */\n turnNumber?: number;\n}\n\n/**\n * Build a `Tracer` compatible with the Vercel AI SDK's\n * `experimental_telemetry.tracer` option. The returned object satisfies the\n * shape `{ isEnabled: true, tracer }` so it can be passed directly to\n * `experimental_telemetry`.\n *\n * The AI SDK emits spans named `ai.generateText`, `ai.generateText.doGenerate`,\n * and `ai.toolCall`. We translate the load-bearing ones:\n *\n * - `ai.toolCall` → a TOOL span (so `t.calledTool(name)` works)\n * - `ai.generateText` → a GENERATION span (so `t.messageIncludes(text)` works)\n * and error status → `t.noFailedActions()` catches it\n *\n * Other spans (`ai.generateText.doGenerate`, etc.) are ignored — only\n * TOOL/AGENT/GENERATION observation types are visible to `t.*` assertions.\n */\nexport function createApoTracer(\n options: CreateApoTracerOptions,\n): { isEnabled: true; tracer: ApoTracer } {\n const { trace, parentSpanId, taskId, turnNumber } = options;\n\n // The generation span id for the current turn. Once `ai.generateText`\n // creates it, subsequent `ai.toolCall` spans should nest under it (the\n // agent calls tools *during* generation), not under the turn span.\n // Without this, every tool renders as a sibling of `agent.generate`\n // instead of a child, flattening the trace hierarchy.\n let currentGenerationSpanId: string | undefined;\n\n /**\n * Build a live span that accumulates attributes and, on `.end()`,\n * calls `trace.createSpan` + `trace.endSpan` with the right observation type.\n */\n function makeLiveSpan(\n name: string,\n observationType: \"TOOL\" | \"GENERATION\",\n initialAttrs: OTelAttributes,\n ): ApoSpan {\n const stepName =\n observationType === \"TOOL\"\n ? String(initialAttrs[\"ai.toolCall.name\"] ?? \"unknown\")\n : \"agent.generate\";\n\n const input =\n observationType === \"TOOL\"\n ? (safeParse(initialAttrs[\"ai.toolCall.args\"]) as Record<string, unknown>)\n : undefined;\n\n const spanId = trace.createSpan({\n task_id: taskId ?? \"ai-sdk\",\n parent_call_id:\n observationType === \"TOOL\"\n ? (currentGenerationSpanId ?? parentSpanId ?? trace.rootSpanId)\n : (parentSpanId ?? trace.rootSpanId),\n step_name: stepName,\n tool_name:\n observationType === \"TOOL\"\n ? String(initialAttrs[\"ai.toolCall.name\"] ?? \"unknown\")\n : undefined,\n model: String(initialAttrs[\"ai.model.id\"] ?? \"ai-sdk\"),\n observation_type: observationType,\n ...(input !== undefined ? { input } : {}),\n metadata: {\n ...(turnNumber !== undefined ? { turnNumber } : {}),\n ...(taskId ? { taskId } : {}),\n },\n });\n\n // Record the generation span id so tool spans created after it nest\n // under it (tools are called during generation).\n if (observationType === \"GENERATION\") {\n currentGenerationSpanId = spanId;\n }\n\n const startedAt = monotonicNowMs();\n let pendingAttrs: OTelAttributes = { ...initialAttrs };\n let status: OTelSpanStatus = { code: 0 };\n let errored = false;\n let ended = false;\n\n const span: ApoSpan = {\n spanContext(): OTelSpanContext {\n return { traceId: trace.runId, spanId, traceFlags: 0 };\n },\n setAttribute(key: string, value: OTelAttributeValue): ApoSpan {\n pendingAttrs[key] = value;\n return span;\n },\n setAttributes(attrs: OTelAttributes): ApoSpan {\n Object.assign(pendingAttrs, attrs);\n return span;\n },\n addEvent(_name: string, _attributesOrStartTime?: unknown, _startTime?: unknown): ApoSpan {\n return span; // no-op — events aren't load-bearing for Flow assertions\n },\n addLink(_link: unknown): ApoSpan {\n return span;\n },\n addLinks(_links: unknown[]): ApoSpan {\n return span;\n },\n setStatus(s: OTelSpanStatus): ApoSpan {\n status = s;\n if (s.code === 2) errored = true;\n return span;\n },\n updateName(_name: string): ApoSpan {\n return span; // name was set at creation time\n },\n end(_endTime?: unknown): void {\n if (ended) return;\n ended = true;\n const latency = round3(monotonicNowMs() - startedAt);\n\n if (observationType === \"TOOL\") {\n const resultRaw = pendingAttrs[\"ai.toolCall.result\"];\n const output = resultRaw !== undefined ? safeParse(resultRaw) : undefined;\n trace.endSpan(spanId, {\n ...(output !== undefined\n ? {\n output:\n output && typeof output === \"object\"\n ? (output as Record<string, unknown>)\n : { value: output },\n }\n : {}),\n latency_ms: latency,\n ...(errored ? { level: \"ERROR\" as const } : {}),\n });\n } else {\n const text = extractTextFromAttrs(pendingAttrs);\n const tokens = extractTokenUsage(pendingAttrs);\n trace.endSpan(spanId, {\n output: text !== undefined ? { text } : {},\n latency_ms: latency,\n ...tokens,\n ...(errored\n ? { level: \"ERROR\" as const, status_message: status.message }\n : {}),\n });\n }\n },\n isRecording(): boolean {\n return !ended;\n },\n recordException(exception: OTelException | string, _time?: unknown): ApoSpan {\n errored = true;\n const msg = typeof exception === \"string\" ? exception : exception.message;\n status = { code: 2, message: msg };\n return span;\n },\n };\n\n return span;\n }\n\n function makeNoopSpan(): ApoSpan {\n const noop: ApoSpan = {\n spanContext: () => ({ traceId: \"\", spanId: nextSpanId(), traceFlags: 0 }),\n setAttribute() {\n return noop;\n },\n setAttributes() {\n return noop;\n },\n addEvent() {\n return noop;\n },\n addLink() {\n return noop;\n },\n addLinks() {\n return noop;\n },\n setStatus() {\n return noop;\n },\n updateName() {\n return noop;\n },\n end() {},\n isRecording: () => false,\n recordException() {\n return noop;\n },\n };\n return noop;\n }\n\n const tracer: ApoTracer = {\n startSpan(name: string, opts?: { attributes?: OTelAttributes }): ApoSpan {\n const attrs = opts?.attributes ?? {};\n const isToolCall = name === \"ai.toolCall\";\n const isGeneration = name === \"ai.generateText\" || name === \"ai.streamText\";\n if (!isToolCall && !isGeneration) return makeNoopSpan();\n return makeLiveSpan(name, isToolCall ? \"TOOL\" : \"GENERATION\", attrs);\n },\n\n startActiveSpan<T>(\n name: string,\n optionsOrFn:\n | { attributes?: OTelAttributes }\n | ((span: ApoSpan) => Promise<T> | T),\n contextOrFn?: ((span: ApoSpan) => Promise<T> | T) | unknown,\n maybeFn?: (span: ApoSpan) => Promise<T> | T,\n ): Promise<T> {\n // Three overload shapes:\n // (name, fn) → optionsOrFn is the fn\n // (name, options, fn) → contextOrFn is the fn\n // (name, options, context, fn) → maybeFn is the fn (context ignored)\n let opts: { attributes?: OTelAttributes };\n let fn: (span: ApoSpan) => Promise<T> | T;\n\n if (typeof optionsOrFn === \"function\") {\n opts = {};\n fn = optionsOrFn;\n } else if (typeof contextOrFn === \"function\") {\n opts = optionsOrFn;\n fn = contextOrFn as (span: ApoSpan) => Promise<T> | T;\n } else {\n opts = optionsOrFn;\n fn = maybeFn!;\n }\n const attrs = opts.attributes ?? {};\n const isToolCall = name === \"ai.toolCall\";\n const isGeneration = name === \"ai.generateText\" || name === \"ai.streamText\";\n\n if (!isToolCall && !isGeneration) {\n const noopSpan = makeNoopSpan();\n return Promise.resolve(fn(noopSpan)).then((result) => {\n noopSpan.end();\n return result;\n });\n }\n\n const span = makeLiveSpan(\n name,\n isToolCall ? \"TOOL\" : \"GENERATION\",\n attrs,\n );\n\n return Promise.resolve(fn(span))\n .then((result) => {\n span.end();\n return result;\n })\n .catch((error) => {\n span.setStatus({\n code: 2,\n message: error instanceof Error ? error.message : String(error),\n });\n span.recordException({\n name: error instanceof Error ? error.name : \"Error\",\n message: error instanceof Error ? error.message : String(error),\n ...(error instanceof Error && error.stack ? { stack: error.stack } : {}),\n });\n span.end();\n throw error;\n });\n },\n };\n\n return { isEnabled: true, tracer };\n}\n\n// ── internals ────────────────────────────────────────────────────────────\n\n/**\n * The AI SDK sets the model's response text under various attribute keys\n * depending on the operation. Try the known ones.\n */\nfunction extractTextFromAttrs(attrs: OTelAttributes): string | undefined {\n const candidates = [\n \"ai.response.text\",\n \"ai.response.output\",\n \"ai.generateText.result\",\n ];\n for (const key of candidates) {\n const val = attrs[key];\n if (typeof val === \"string\" && val.length > 0) {\n const parsed = safeParse(val);\n if (typeof parsed === \"string\") return parsed;\n return val;\n }\n }\n return undefined;\n}\n"],"mappings":";;;;;;;AAqHA,SAAS,UAAU,KAAuB;AACxC,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,MAAI;AACF,WAAO,KAAK,MAAM,GAAG;AAAA,EACvB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,iBAAyB;AAChC,MAAI,OAAO,gBAAgB,eAAe,OAAO,YAAY,QAAQ,YAAY;AAC/E,WAAO,YAAY,IAAI;AAAA,EACzB;AACA,SAAO,KAAK,IAAI;AAClB;AAEA,SAAS,OAAO,GAAmB;AACjC,SAAO,KAAK,MAAM,IAAI,GAAI,IAAI;AAChC;AAEA,IAAM,iBAAiB;AACvB,IAAI,cAAc;AAElB,SAAS,aAAqB;AAC5B,iBAAe;AACf,SAAO,GAAG,cAAc,GAAG,WAAW;AACxC;AA+BO,SAAS,gBACd,SACwC;AACxC,QAAM,EAAE,OAAO,cAAc,QAAQ,WAAW,IAAI;AAOpD,MAAI;AAMJ,WAAS,aACP,MACA,iBACA,cACS;AACT,UAAM,WACJ,oBAAoB,SAChB,OAAO,aAAa,kBAAkB,KAAK,SAAS,IACpD;AAEN,UAAM,QACJ,oBAAoB,SACf,UAAU,aAAa,kBAAkB,CAAC,IAC3C;AAEN,UAAM,SAAS,MAAM,WAAW;AAAA,MAC9B,SAAS,UAAU;AAAA,MACnB,gBACE,oBAAoB,SACf,2BAA2B,gBAAgB,MAAM,aACjD,gBAAgB,MAAM;AAAA,MAC7B,WAAW;AAAA,MACX,WACE,oBAAoB,SAChB,OAAO,aAAa,kBAAkB,KAAK,SAAS,IACpD;AAAA,MACN,OAAO,OAAO,aAAa,aAAa,KAAK,QAAQ;AAAA,MACrD,kBAAkB;AAAA,MAClB,GAAI,UAAU,SAAY,EAAE,MAAM,IAAI,CAAC;AAAA,MACvC,UAAU;AAAA,QACR,GAAI,eAAe,SAAY,EAAE,WAAW,IAAI,CAAC;AAAA,QACjD,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,MAC7B;AAAA,IACF,CAAC;AAID,QAAI,oBAAoB,cAAc;AACpC,gCAA0B;AAAA,IAC5B;AAEA,UAAM,YAAY,eAAe;AACjC,QAAI,eAA+B,EAAE,GAAG,aAAa;AACrD,QAAI,SAAyB,EAAE,MAAM,EAAE;AACvC,QAAI,UAAU;AACd,QAAI,QAAQ;AAEZ,UAAM,OAAgB;AAAA,MACpB,cAA+B;AAC7B,eAAO,EAAE,SAAS,MAAM,OAAO,QAAQ,YAAY,EAAE;AAAA,MACvD;AAAA,MACA,aAAa,KAAa,OAAoC;AAC5D,qBAAa,GAAG,IAAI;AACpB,eAAO;AAAA,MACT;AAAA,MACA,cAAc,OAAgC;AAC5C,eAAO,OAAO,cAAc,KAAK;AACjC,eAAO;AAAA,MACT;AAAA,MACA,SAAS,OAAe,wBAAkC,YAA+B;AACvF,eAAO;AAAA,MACT;AAAA,MACA,QAAQ,OAAyB;AAC/B,eAAO;AAAA,MACT;AAAA,MACA,SAAS,QAA4B;AACnC,eAAO;AAAA,MACT;AAAA,MACA,UAAU,GAA4B;AACpC,iBAAS;AACT,YAAI,EAAE,SAAS,EAAG,WAAU;AAC5B,eAAO;AAAA,MACT;AAAA,MACA,WAAW,OAAwB;AACjC,eAAO;AAAA,MACT;AAAA,MACA,IAAI,UAA0B;AAC5B,YAAI,MAAO;AACX,gBAAQ;AACR,cAAM,UAAU,OAAO,eAAe,IAAI,SAAS;AAEnD,YAAI,oBAAoB,QAAQ;AAC9B,gBAAM,YAAY,aAAa,oBAAoB;AACnD,gBAAM,SAAS,cAAc,SAAY,UAAU,SAAS,IAAI;AAChE,gBAAM,QAAQ,QAAQ;AAAA,YACpB,GAAI,WAAW,SACX;AAAA,cACE,QACE,UAAU,OAAO,WAAW,WACvB,SACD,EAAE,OAAO,OAAO;AAAA,YACxB,IACA,CAAC;AAAA,YACL,YAAY;AAAA,YACZ,GAAI,UAAU,EAAE,OAAO,QAAiB,IAAI,CAAC;AAAA,UAC/C,CAAC;AAAA,QACH,OAAO;AACL,gBAAM,OAAO,qBAAqB,YAAY;AAC9C,gBAAM,SAAS,kBAAkB,YAAY;AAC7C,gBAAM,QAAQ,QAAQ;AAAA,YACpB,QAAQ,SAAS,SAAY,EAAE,KAAK,IAAI,CAAC;AAAA,YACzC,YAAY;AAAA,YACZ,GAAG;AAAA,YACH,GAAI,UACA,EAAE,OAAO,SAAkB,gBAAgB,OAAO,QAAQ,IAC1D,CAAC;AAAA,UACP,CAAC;AAAA,QACH;AAAA,MACF;AAAA,MACA,cAAuB;AACrB,eAAO,CAAC;AAAA,MACV;AAAA,MACA,gBAAgB,WAAmC,OAA0B;AAC3E,kBAAU;AACV,cAAM,MAAM,OAAO,cAAc,WAAW,YAAY,UAAU;AAClE,iBAAS,EAAE,MAAM,GAAG,SAAS,IAAI;AACjC,eAAO;AAAA,MACT;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAEA,WAAS,eAAwB;AAC/B,UAAM,OAAgB;AAAA,MACpB,aAAa,OAAO,EAAE,SAAS,IAAI,QAAQ,WAAW,GAAG,YAAY,EAAE;AAAA,MACvE,eAAe;AACb,eAAO;AAAA,MACT;AAAA,MACA,gBAAgB;AACd,eAAO;AAAA,MACT;AAAA,MACA,WAAW;AACT,eAAO;AAAA,MACT;AAAA,MACA,UAAU;AACR,eAAO;AAAA,MACT;AAAA,MACA,WAAW;AACT,eAAO;AAAA,MACT;AAAA,MACA,YAAY;AACV,eAAO;AAAA,MACT;AAAA,MACA,aAAa;AACX,eAAO;AAAA,MACT;AAAA,MACA,MAAM;AAAA,MAAC;AAAA,MACP,aAAa,MAAM;AAAA,MACnB,kBAAkB;AAChB,eAAO;AAAA,MACT;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,QAAM,SAAoB;AAAA,IACxB,UAAU,MAAc,MAAiD;AACvE,YAAM,QAAQ,MAAM,cAAc,CAAC;AACnC,YAAM,aAAa,SAAS;AAC5B,YAAM,eAAe,SAAS,qBAAqB,SAAS;AAC5D,UAAI,CAAC,cAAc,CAAC,aAAc,QAAO,aAAa;AACtD,aAAO,aAAa,MAAM,aAAa,SAAS,cAAc,KAAK;AAAA,IACrE;AAAA,IAEA,gBACE,MACA,aAGA,aACA,SACY;AAKZ,UAAI;AACJ,UAAI;AAEJ,UAAI,OAAO,gBAAgB,YAAY;AACrC,eAAO,CAAC;AACR,aAAK;AAAA,MACP,WAAW,OAAO,gBAAgB,YAAY;AAC5C,eAAO;AACP,aAAK;AAAA,MACP,OAAO;AACL,eAAO;AACP,aAAK;AAAA,MACP;AACA,YAAM,QAAQ,KAAK,cAAc,CAAC;AAClC,YAAM,aAAa,SAAS;AAC5B,YAAM,eAAe,SAAS,qBAAqB,SAAS;AAE5D,UAAI,CAAC,cAAc,CAAC,cAAc;AAChC,cAAM,WAAW,aAAa;AAC9B,eAAO,QAAQ,QAAQ,GAAG,QAAQ,CAAC,EAAE,KAAK,CAAC,WAAW;AACpD,mBAAS,IAAI;AACb,iBAAO;AAAA,QACT,CAAC;AAAA,MACH;AAEA,YAAM,OAAO;AAAA,QACX;AAAA,QACA,aAAa,SAAS;AAAA,QACtB;AAAA,MACF;AAEA,aAAO,QAAQ,QAAQ,GAAG,IAAI,CAAC,EAC5B,KAAK,CAAC,WAAW;AAChB,aAAK,IAAI;AACT,eAAO;AAAA,MACT,CAAC,EACA,MAAM,CAAC,UAAU;AAChB,aAAK,UAAU;AAAA,UACb,MAAM;AAAA,UACN,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,QAChE,CAAC;AACD,aAAK,gBAAgB;AAAA,UACnB,MAAM,iBAAiB,QAAQ,MAAM,OAAO;AAAA,UAC5C,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,UAC9D,GAAI,iBAAiB,SAAS,MAAM,QAAQ,EAAE,OAAO,MAAM,MAAM,IAAI,CAAC;AAAA,QACxE,CAAC;AACD,aAAK,IAAI;AACT,cAAM;AAAA,MACR,CAAC;AAAA,IACL;AAAA,EACF;AAEA,SAAO,EAAE,WAAW,MAAM,OAAO;AACnC;AAQA,SAAS,qBAAqB,OAA2C;AACvE,QAAM,aAAa;AAAA,IACjB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,aAAW,OAAO,YAAY;AAC5B,UAAM,MAAM,MAAM,GAAG;AACrB,QAAI,OAAO,QAAQ,YAAY,IAAI,SAAS,GAAG;AAC7C,YAAM,SAAS,UAAU,GAAG;AAC5B,UAAI,OAAO,WAAW,SAAU,QAAO;AACvC,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;","names":[]}