@tangle-network/agent-app 0.60.200 → 0.60.202

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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runRouterTurn
3
- } from "./chunk-T4BLISYH.js";
3
+ } from "./chunk-PRER37K5.js";
4
4
 
5
5
  // src/runtime/protected-model.ts
6
6
  import {
@@ -219,4 +219,4 @@ export {
219
219
  createRouterProtectedModelPort,
220
220
  runProtectedRouterTurn
221
221
  };
222
- //# sourceMappingURL=chunk-2OWT3TNV.js.map
222
+ //# sourceMappingURL=chunk-QQJJ2XYW.js.map
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createChatTurnRoutes,
3
3
  streamChatRouteAsSandboxEvents
4
- } from "../chunk-6XXKFYIM.js";
4
+ } from "../chunk-CMK2F3QH.js";
5
5
  import "../chunk-Q7WEEN7P.js";
6
6
  import "../chunk-UTAIRVOK.js";
7
7
  import "../chunk-RN4WPTBH.js";
@@ -17,7 +17,7 @@ import "../chunk-ZGUXI5SI.js";
17
17
  import "../chunk-6VQCFJWP.js";
18
18
  import "../chunk-LQYIBL3G.js";
19
19
  import "../chunk-XELWWXJJ.js";
20
- import "../chunk-BKPJFES3.js";
20
+ import "../chunk-I4FFHWCS.js";
21
21
 
22
22
  // src/public-consultation/index.ts
23
23
  import { searchKnowledge } from "@tangle-network/agent-knowledge";
@@ -2,7 +2,7 @@ import {
2
2
  ProtectedModelSettlementError,
3
3
  createRouterProtectedModelPort,
4
4
  runProtectedRouterTurn
5
- } from "../chunk-2OWT3TNV.js";
5
+ } from "../chunk-QQJJ2XYW.js";
6
6
  import {
7
7
  DURABLE_HOP_BUDGET_MS,
8
8
  asksForLanguage,
@@ -24,7 +24,7 @@ import {
24
24
  turnKnowledgeInstructions,
25
25
  workspaceKnowledgeForTurn,
26
26
  workspaceTurnCheckpoints
27
- } from "../chunk-T4BLISYH.js";
27
+ } from "../chunk-PRER37K5.js";
28
28
  import "../chunk-7XQIY2LU.js";
29
29
  import {
30
30
  DEFAULT_TANGLE_BILLING_ENFORCEMENT_ENV_VAR,
@@ -52,7 +52,7 @@ import {
52
52
  resolveCatalogModelId,
53
53
  sortModelsByFreshness
54
54
  } from "../chunk-AMUXRAUO.js";
55
- import "../chunk-BKPJFES3.js";
55
+ import "../chunk-I4FFHWCS.js";
56
56
 
57
57
  // src/runtime/certified-delivery.ts
58
58
  import {
@@ -11,11 +11,11 @@ import {
11
11
  turnKnowledgeInstructions,
12
12
  workspaceKnowledgeForTurn,
13
13
  workspaceTurnCheckpoints
14
- } from "../chunk-T4BLISYH.js";
14
+ } from "../chunk-PRER37K5.js";
15
15
  import "../chunk-7XQIY2LU.js";
16
16
  import "../chunk-D6UJEOZQ.js";
17
17
  import "../chunk-AMUXRAUO.js";
18
- import "../chunk-BKPJFES3.js";
18
+ import "../chunk-I4FFHWCS.js";
19
19
  export {
20
20
  DURABLE_HOP_BUDGET_MS,
21
21
  asksForLanguage,
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * `@tangle-network/agent-app/turn-trace` — one trace per agent turn.
3
3
  *
4
- * The recorder and the Router/Sandbox helpers are runtime-neutral. The OTLP
5
- * sink imports the agent-runtime root, so a browser bundle must not import this
6
- * entry; server code imports it for `createIntelligenceTurnTraceSink`.
4
+ * The recorder, the Router/Sandbox helpers and the OTLP sink import nothing but
5
+ * each other, so a Worker, a Durable Object and Node all bundle this entry
6
+ * as it is. The sink needs `fetch` and a tenant key, so it is server code.
7
7
  */
8
8
  export * from './recorder';
9
9
  export * from './server-timing';
@@ -10,20 +10,38 @@ import {
10
10
  isTraceId,
11
11
  summarizeTurnSpans,
12
12
  turnTraceId
13
- } from "../chunk-BKPJFES3.js";
13
+ } from "../chunk-I4FFHWCS.js";
14
14
 
15
15
  // src/turn-trace/otel.ts
16
- import {
17
- createOtelExporter,
18
- padSpanId,
19
- padTraceId,
20
- toOtelAttributes
21
- } from "@tangle-network/agent-runtime";
22
- var SPAN_KIND = { "model.call": "LLM", "tool.call": "TOOL" };
16
+ function attributes(record) {
17
+ return Object.entries(record).flatMap(([key, value]) => {
18
+ if (typeof value === "number" && !Number.isFinite(value)) return [];
19
+ return [{
20
+ key,
21
+ value: typeof value === "number" ? Number.isInteger(value) ? { intValue: value.toString() } : { doubleValue: value } : typeof value === "boolean" ? { boolValue: value } : { stringValue: value }
22
+ }];
23
+ });
24
+ }
25
+ function otlpTraceId(traceId) {
26
+ if (/^[0-9a-f]{32}$/.test(traceId)) return traceId;
27
+ const dashless = traceId.replace(/-/g, "");
28
+ if (/^[0-9a-f]{32}$/.test(dashless)) return dashless;
29
+ return hexId(traceId, 32);
30
+ }
31
+ function hexId(input, length) {
32
+ const hashes = [2166136261, 2654435769, 2246822507, 3266489909];
33
+ for (const byte of new TextEncoder().encode(input)) {
34
+ for (let lane = 0; lane < hashes.length; lane += 1) hashes[lane] = Math.imul(hashes[lane] ^ byte + lane, 16777619);
35
+ }
36
+ let out = hashes.map((hash) => (hash >>> 0).toString(16).padStart(8, "0")).join("");
37
+ while (out.length < length) out += hexId(`${input}:${out.length}`, 32);
38
+ return out.slice(0, length);
39
+ }
23
40
  var msToNs = (ms) => (BigInt(Math.floor(ms)) * 1000000n).toString();
41
+ var SPAN_KIND = { "model.call": "LLM", "tool.call": "TOOL" };
24
42
  function buildTurnTraceOtelSpans(snapshot, labels = {}) {
25
- const traceId = padTraceId(snapshot.traceId);
26
- const root = padSpanId(snapshot.rootSpanId);
43
+ const traceId = otlpTraceId(snapshot.traceId);
44
+ const root = snapshot.rootSpanId;
27
45
  const { summary } = snapshot;
28
46
  const rootAttrs = {
29
47
  ...labels,
@@ -51,7 +69,7 @@ function buildTurnTraceOtelSpans(snapshot, labels = {}) {
51
69
  kind: 1,
52
70
  startTimeUnixNano: msToNs(snapshot.startedAt),
53
71
  endTimeUnixNano: msToNs(snapshot.endedAt),
54
- attributes: toOtelAttributes(rootAttrs),
72
+ attributes: attributes(rootAttrs),
55
73
  status: snapshot.status === "ok" ? { code: 1 } : { code: 2, ...snapshot.error ? { message: snapshot.error } : {} }
56
74
  }];
57
75
  for (const span of snapshot.spans) {
@@ -66,42 +84,49 @@ function buildTurnTraceOtelSpans(snapshot, labels = {}) {
66
84
  };
67
85
  spans.push({
68
86
  traceId,
69
- spanId: padSpanId(span.spanId),
70
- parentSpanId: padSpanId(span.parentSpanId),
87
+ spanId: span.spanId,
88
+ parentSpanId: span.parentSpanId,
71
89
  name: span.name,
72
90
  kind: 1,
73
91
  startTimeUnixNano: msToNs(span.startMs),
74
92
  endTimeUnixNano: msToNs(span.endMs),
75
- attributes: toOtelAttributes(attrs),
93
+ attributes: attributes(attrs),
76
94
  status: span.error ? { code: 2, message: span.error } : { code: 1 }
77
95
  });
78
96
  }
79
97
  return spans;
80
98
  }
81
99
  var DEFAULT_INTELLIGENCE_BASE_URL = "https://intelligence.tangle.tools";
100
+ var SCOPE = { name: "@tangle-network/agent-app", version: "turn-trace" };
82
101
  function createIntelligenceTurnTraceSink(options) {
83
- let exporter;
84
- const exporterFor = (apiKey) => {
85
- exporter ??= createOtelExporter({
86
- endpoint: `${(options.baseUrl ?? DEFAULT_INTELLIGENCE_BASE_URL).replace(/\/+$/, "")}/v1/otlp`,
87
- headers: { authorization: `Bearer ${apiKey}` },
88
- serviceName: options.project,
89
- resourceAttributes: { "tangle.project": options.project }
90
- });
91
- return exporter;
102
+ const url = `${(options.baseUrl ?? DEFAULT_INTELLIGENCE_BASE_URL).replace(/\/+$/, "")}/v1/otlp/v1/traces`;
103
+ const fail = (reason) => {
104
+ try {
105
+ options.onError?.(reason.slice(0, 200));
106
+ } catch {
107
+ }
92
108
  };
93
109
  return async (snapshot, labels) => {
94
110
  if (!options.apiKey) return;
95
111
  try {
96
- const sink = exporterFor(options.apiKey);
97
- if (!sink) return;
98
- for (const span of buildTurnTraceOtelSpans(snapshot, { project: options.project, ...labels })) sink.exportSpan(span);
99
- await sink.flush();
112
+ const body = {
113
+ resourceSpans: [{
114
+ resource: { attributes: attributes({ "service.name": options.project, "tangle.project": options.project }) },
115
+ scopeSpans: [{ scope: SCOPE, spans: buildTurnTraceOtelSpans(snapshot, { project: options.project, ...labels }) }]
116
+ }]
117
+ };
118
+ const response = await fetch(url, {
119
+ method: "POST",
120
+ headers: { "content-type": "application/json", authorization: `Bearer ${options.apiKey}` },
121
+ body: JSON.stringify(body),
122
+ signal: AbortSignal.timeout(options.timeoutMs ?? 5e3)
123
+ });
124
+ const text = await response.text();
125
+ if (!response.ok) return fail(`HTTP ${response.status} from the OTLP ingest`);
126
+ const rejected = Number(JSON.parse(text || "{}").partialSuccess?.rejectedSpans ?? 0);
127
+ if (rejected > 0) fail(`the OTLP ingest rejected ${rejected} spans`);
100
128
  } catch (error) {
101
- try {
102
- options.onError?.(error instanceof Error ? error.message.slice(0, 160) : "turn trace export failed");
103
- } catch {
104
- }
129
+ fail(error instanceof Error ? error.message : "turn trace export failed");
105
130
  }
106
131
  };
107
132
  }
@@ -112,6 +137,7 @@ export {
112
137
  createTurnTrace,
113
138
  isTraceId,
114
139
  nextCallIndex,
140
+ otlpTraceId,
115
141
  parseServerTiming,
116
142
  serverTimingAttrs,
117
143
  summarizeTurnSpans,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/turn-trace/otel.ts"],"sourcesContent":["/**\n * A finished turn trace as OTLP spans, sent to Tangle Intelligence.\n *\n * The span builders and the exporter are agent-runtime's (`createOtelExporter`,\n * `toOtelAttributes`, `padTraceId`); this file only maps the turn's tree onto\n * them, using the GenAI semantic-convention keys the runtime's own builders\n * write (`gen_ai.operation.name`, `gen_ai.request.model`, `gen_ai.usage.*`,\n * `gen_ai.tool.name`) and OpenInference's span kind.\n *\n * Server only: it imports the runtime root, which a browser bundle must not.\n */\nimport {\n createOtelExporter,\n padSpanId,\n padTraceId,\n toOtelAttributes,\n type OtelExporter,\n type OtelSpan,\n} from '@tangle-network/agent-runtime'\nimport { TURN_SPAN_KINDS, type TurnSpanAttrs, type TurnSpanKind, type TurnTraceSink, type TurnTraceSnapshot } from './recorder'\n\n/** OpenInference span kind per turn span: model calls read as LLM, tool calls as TOOL. */\nconst SPAN_KIND: Partial<Record<string, 'LLM' | 'TOOL'>> = { 'model.call': 'LLM', 'tool.call': 'TOOL' }\n\nconst msToNs = (ms: number) => (BigInt(Math.floor(ms)) * 1_000_000n).toString()\n\n/**\n * The root span (`agent_app.turn`) carries the host's labels and the turn's\n * numbers: wall time, the share inside child spans, and the time credited to\n * each kind of span. Children follow under it.\n */\nexport function buildTurnTraceOtelSpans(snapshot: TurnTraceSnapshot, labels: TurnSpanAttrs = {}): OtelSpan[] {\n const traceId = padTraceId(snapshot.traceId)\n const root = padSpanId(snapshot.rootSpanId)\n const { summary } = snapshot\n const rootAttrs: Record<string, string | number | boolean> = {\n ...labels,\n 'openinference.span.kind': 'CHAIN',\n 'tangle.execution_id': snapshot.executionId,\n 'tangle.trace_id': snapshot.traceId,\n 'tangle.started_at_ms': snapshot.startedAt,\n 'tangle.completed_at_ms': snapshot.endedAt,\n 'tangle.duration_ms': summary.wallMs,\n 'tangle.turn.wall_ms': summary.wallMs,\n 'tangle.turn.covered_ms': summary.coveredMs,\n 'tangle.turn.coverage_pct': summary.coveragePct,\n 'tangle.turn.uncovered_ms': summary.uncoveredMs,\n 'tangle.turn.largest_gap_ms': summary.largestGapMs,\n 'tangle.turn.spans': summary.spans,\n 'tangle.turn.model_calls': summary.modelCalls,\n 'tangle.turn.tool_calls': summary.toolCalls,\n ...(summary.dropped ? { 'tangle.turn.spans_dropped': summary.dropped } : {}),\n ...Object.fromEntries(TURN_SPAN_KINDS.map((kind: TurnSpanKind) => [`tangle.turn.${kind}_ms`, summary.byKind[kind]])),\n }\n const spans: OtelSpan[] = [{\n traceId,\n spanId: root,\n name: 'agent_app.turn',\n kind: 1,\n startTimeUnixNano: msToNs(snapshot.startedAt),\n endTimeUnixNano: msToNs(snapshot.endedAt),\n attributes: toOtelAttributes(rootAttrs),\n status: snapshot.status === 'ok' ? { code: 1 } : { code: 2, ...(snapshot.error ? { message: snapshot.error } : {}) },\n }]\n for (const span of snapshot.spans) {\n const attrs: Record<string, string | number | boolean> = {\n ...span.attrs,\n 'openinference.span.kind': SPAN_KIND[span.name] ?? 'CHAIN',\n 'tangle.turn.span_kind': span.kind,\n 'agent.parent.confidence': 'explicit',\n ...(span.name === 'tool.call' ? { 'gen_ai.operation.name': 'execute_tool' } : {}),\n ...(span.unfinished ? { 'tangle.turn.unfinished': true } : {}),\n ...(span.error ? { 'error.message': span.error } : {}),\n }\n spans.push({\n traceId,\n spanId: padSpanId(span.spanId),\n parentSpanId: padSpanId(span.parentSpanId),\n name: span.name,\n kind: 1,\n startTimeUnixNano: msToNs(span.startMs),\n endTimeUnixNano: msToNs(span.endMs),\n attributes: toOtelAttributes(attrs),\n status: span.error ? { code: 2, message: span.error } : { code: 1 },\n })\n }\n return spans\n}\n\nexport interface IntelligenceTurnTraceSinkOptions {\n /** The Intelligence project the spans belong to, e.g. `legal-agent`. */\n project: string\n /** The tenant key the ingest requires. Without one the sink does nothing. */\n apiKey: string | undefined\n /** Default `https://intelligence.tangle.tools`; spans go to `<base>/v1/otlp`. */\n baseUrl?: string\n /** Called with a short reason when a delivery fails. Never receives span content. */\n onError?: (reason: string) => void\n}\n\nconst DEFAULT_INTELLIGENCE_BASE_URL = 'https://intelligence.tangle.tools'\n\n/**\n * The sink a chat route or a Worker turn hands each finished trace to. The\n * exporter is built on first use, inside a request, because a Worker refuses a\n * timer at module scope. Each trace is flushed before the sink returns, so a\n * host that runs it under `waitUntil` keeps the isolate alive until delivery.\n */\nexport function createIntelligenceTurnTraceSink(options: IntelligenceTurnTraceSinkOptions): TurnTraceSink {\n let exporter: OtelExporter | undefined\n const exporterFor = (apiKey: string): OtelExporter | undefined => {\n exporter ??= createOtelExporter({\n endpoint: `${(options.baseUrl ?? DEFAULT_INTELLIGENCE_BASE_URL).replace(/\\/+$/, '')}/v1/otlp`,\n headers: { authorization: `Bearer ${apiKey}` },\n serviceName: options.project,\n resourceAttributes: { 'tangle.project': options.project },\n })\n return exporter\n }\n return async (snapshot, labels) => {\n if (!options.apiKey) return\n try {\n const sink = exporterFor(options.apiKey)\n if (!sink) return\n for (const span of buildTurnTraceOtelSpans(snapshot, { project: options.project, ...labels })) sink.exportSpan(span)\n await sink.flush()\n } catch (error) {\n try { options.onError?.(error instanceof Error ? error.message.slice(0, 160) : 'turn trace export failed') } catch { /* telemetry only */ }\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/turn-trace/otel.ts"],"sourcesContent":["/**\n * A finished turn trace as OTLP spans, sent to Tangle Intelligence.\n *\n * The attribute vocabulary is the one agent-runtime's span builders write\n * (`gen_ai.operation.name`, `gen_ai.request.model`, `gen_ai.usage.*`,\n * `gen_ai.tool.name`, OpenInference's span kind, `agent.parent.confidence`), and\n * the wire shape is the one its `createOtelExporter` posts to\n * `<base>/v1/otlp/v1/traces`.\n *\n * It does not import agent-runtime. The runtime root pulls the whole sandbox\n * SDK into a Worker bundle, and the SDK's chain client imports `viem`, which a\n * Worker build cannot resolve (Hospitality's release failed on exactly that);\n * and its exporter keeps a timer a Worker refuses at module scope. One OTLP\n * POST per finished turn needs neither.\n */\nimport { TURN_SPAN_KINDS, type TurnSpanAttrs, type TurnSpanKind, type TurnTraceSink, type TurnTraceSnapshot } from './recorder'\n\n/** One OTLP/JSON span, as Intelligence's ingest reads it. */\nexport interface OtlpSpan {\n traceId: string\n spanId: string\n parentSpanId?: string\n name: string\n kind: number\n startTimeUnixNano: string\n endTimeUnixNano: string\n attributes: Array<{ key: string; value: { stringValue?: string; intValue?: string; doubleValue?: number; boolValue?: boolean } }>\n status: { code: number; message?: string }\n}\n\nfunction attributes(record: Record<string, string | number | boolean>): OtlpSpan['attributes'] {\n return Object.entries(record).flatMap(([key, value]) => {\n if (typeof value === 'number' && !Number.isFinite(value)) return []\n return [{\n key,\n value: typeof value === 'number' ? (Number.isInteger(value) ? { intValue: value.toString() } : { doubleValue: value })\n : typeof value === 'boolean' ? { boolValue: value } : { stringValue: value },\n }]\n })\n}\n\n/**\n * A Tangle trace id as the 32-hex OTLP trace id. The mapping is the Sandbox SDK's `otelTraceIdForTangleTrace`, so a\n * Sandbox trace export of the same turn lands in the same OTLP trace. An id that is already 32 hex, or a UUID, is used as it is.\n */\nexport function otlpTraceId(traceId: string): string {\n if (/^[0-9a-f]{32}$/.test(traceId)) return traceId\n const dashless = traceId.replace(/-/g, '')\n if (/^[0-9a-f]{32}$/.test(dashless)) return dashless\n return hexId(traceId, 32)\n}\n\nfunction hexId(input: string, length: number): string {\n const hashes = [2166136261, 2654435769, 2246822507, 3266489909]\n for (const byte of new TextEncoder().encode(input)) {\n for (let lane = 0; lane < hashes.length; lane += 1) hashes[lane] = Math.imul((hashes[lane] as number) ^ (byte + lane), 16777619)\n }\n let out = hashes.map((hash) => (hash >>> 0).toString(16).padStart(8, '0')).join('')\n while (out.length < length) out += hexId(`${input}:${out.length}`, 32)\n return out.slice(0, length)\n}\n\nconst msToNs = (ms: number) => (BigInt(Math.floor(ms)) * 1_000_000n).toString()\n\n/** OpenInference span kind per turn span: model calls read as LLM, tool calls as TOOL. */\nconst SPAN_KIND: Partial<Record<string, 'LLM' | 'TOOL'>> = { 'model.call': 'LLM', 'tool.call': 'TOOL' }\n\n/**\n * The root span (`agent_app.turn`) carries the host's labels and the turn's\n * numbers: wall time, the share inside child spans, and the time credited to\n * each kind of span. Children follow under it.\n */\nexport function buildTurnTraceOtelSpans(snapshot: TurnTraceSnapshot, labels: TurnSpanAttrs = {}): OtlpSpan[] {\n const traceId = otlpTraceId(snapshot.traceId)\n const root = snapshot.rootSpanId\n const { summary } = snapshot\n const rootAttrs: Record<string, string | number | boolean> = {\n ...labels,\n 'openinference.span.kind': 'CHAIN',\n 'tangle.execution_id': snapshot.executionId,\n 'tangle.trace_id': snapshot.traceId,\n 'tangle.started_at_ms': snapshot.startedAt,\n 'tangle.completed_at_ms': snapshot.endedAt,\n 'tangle.duration_ms': summary.wallMs,\n 'tangle.turn.wall_ms': summary.wallMs,\n 'tangle.turn.covered_ms': summary.coveredMs,\n 'tangle.turn.coverage_pct': summary.coveragePct,\n 'tangle.turn.uncovered_ms': summary.uncoveredMs,\n 'tangle.turn.largest_gap_ms': summary.largestGapMs,\n 'tangle.turn.spans': summary.spans,\n 'tangle.turn.model_calls': summary.modelCalls,\n 'tangle.turn.tool_calls': summary.toolCalls,\n ...(summary.dropped ? { 'tangle.turn.spans_dropped': summary.dropped } : {}),\n ...Object.fromEntries(TURN_SPAN_KINDS.map((kind: TurnSpanKind) => [`tangle.turn.${kind}_ms`, summary.byKind[kind]])),\n }\n const spans: OtlpSpan[] = [{\n traceId,\n spanId: root,\n name: 'agent_app.turn',\n kind: 1,\n startTimeUnixNano: msToNs(snapshot.startedAt),\n endTimeUnixNano: msToNs(snapshot.endedAt),\n attributes: attributes(rootAttrs),\n status: snapshot.status === 'ok' ? { code: 1 } : { code: 2, ...(snapshot.error ? { message: snapshot.error } : {}) },\n }]\n for (const span of snapshot.spans) {\n const attrs: Record<string, string | number | boolean> = {\n ...span.attrs,\n 'openinference.span.kind': SPAN_KIND[span.name] ?? 'CHAIN',\n 'tangle.turn.span_kind': span.kind,\n 'agent.parent.confidence': 'explicit',\n ...(span.name === 'tool.call' ? { 'gen_ai.operation.name': 'execute_tool' } : {}),\n ...(span.unfinished ? { 'tangle.turn.unfinished': true } : {}),\n ...(span.error ? { 'error.message': span.error } : {}),\n }\n spans.push({\n traceId,\n spanId: span.spanId,\n parentSpanId: span.parentSpanId,\n name: span.name,\n kind: 1,\n startTimeUnixNano: msToNs(span.startMs),\n endTimeUnixNano: msToNs(span.endMs),\n attributes: attributes(attrs),\n status: span.error ? { code: 2, message: span.error } : { code: 1 },\n })\n }\n return spans\n}\n\nexport interface IntelligenceTurnTraceSinkOptions {\n /** The Intelligence project the spans belong to, e.g. `legal-agent`. */\n project: string\n /** The tenant key the ingest requires. Without one the sink does nothing. */\n apiKey: string | undefined\n /** Default `https://intelligence.tangle.tools`; spans go to `<base>/v1/otlp/v1/traces`. */\n baseUrl?: string\n /** Called with a short reason when a delivery fails. Never receives span content. */\n onError?: (reason: string) => void\n /** Milliseconds one POST may take (default 5000). */\n timeoutMs?: number\n}\n\nconst DEFAULT_INTELLIGENCE_BASE_URL = 'https://intelligence.tangle.tools'\nconst SCOPE = { name: '@tangle-network/agent-app', version: 'turn-trace' }\n\n/**\n * The sink a chat route or a Worker turn hands each finished trace to: one OTLP POST per turn, awaited, so a host that\n * runs it under `waitUntil` keeps the isolate alive until delivery. It never throws; a failure goes to `onError`.\n */\nexport function createIntelligenceTurnTraceSink(options: IntelligenceTurnTraceSinkOptions): TurnTraceSink {\n const url = `${(options.baseUrl ?? DEFAULT_INTELLIGENCE_BASE_URL).replace(/\\/+$/, '')}/v1/otlp/v1/traces`\n const fail = (reason: string) => { try { options.onError?.(reason.slice(0, 200)) } catch { /* telemetry only */ } }\n return async (snapshot, labels) => {\n if (!options.apiKey) return\n try {\n const body = {\n resourceSpans: [{\n resource: { attributes: attributes({ 'service.name': options.project, 'tangle.project': options.project }) },\n scopeSpans: [{ scope: SCOPE, spans: buildTurnTraceOtelSpans(snapshot, { project: options.project, ...labels }) }],\n }],\n }\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json', authorization: `Bearer ${options.apiKey}` },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(options.timeoutMs ?? 5_000),\n })\n const text = await response.text()\n if (!response.ok) return fail(`HTTP ${response.status} from the OTLP ingest`)\n // A 2xx can still refuse part of a batch (`partialSuccess.rejectedSpans`).\n const rejected = Number((JSON.parse(text || '{}') as { partialSuccess?: { rejectedSpans?: unknown } }).partialSuccess?.rejectedSpans ?? 0)\n if (rejected > 0) fail(`the OTLP ingest rejected ${rejected} spans`)\n } catch (error) {\n fail(error instanceof Error ? error.message : 'turn trace export failed')\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,36 +1,67 @@
1
1
  /**
2
2
  * A finished turn trace as OTLP spans, sent to Tangle Intelligence.
3
3
  *
4
- * The span builders and the exporter are agent-runtime's (`createOtelExporter`,
5
- * `toOtelAttributes`, `padTraceId`); this file only maps the turn's tree onto
6
- * them, using the GenAI semantic-convention keys the runtime's own builders
7
- * write (`gen_ai.operation.name`, `gen_ai.request.model`, `gen_ai.usage.*`,
8
- * `gen_ai.tool.name`) and OpenInference's span kind.
4
+ * The attribute vocabulary is the one agent-runtime's span builders write
5
+ * (`gen_ai.operation.name`, `gen_ai.request.model`, `gen_ai.usage.*`,
6
+ * `gen_ai.tool.name`, OpenInference's span kind, `agent.parent.confidence`), and
7
+ * the wire shape is the one its `createOtelExporter` posts to
8
+ * `<base>/v1/otlp/v1/traces`.
9
9
  *
10
- * Server only: it imports the runtime root, which a browser bundle must not.
10
+ * It does not import agent-runtime. The runtime root pulls the whole sandbox
11
+ * SDK into a Worker bundle, and the SDK's chain client imports `viem`, which a
12
+ * Worker build cannot resolve (Hospitality's release failed on exactly that);
13
+ * and its exporter keeps a timer a Worker refuses at module scope. One OTLP
14
+ * POST per finished turn needs neither.
11
15
  */
12
- import { type OtelSpan } from '@tangle-network/agent-runtime';
13
16
  import { type TurnSpanAttrs, type TurnTraceSink, type TurnTraceSnapshot } from './recorder';
17
+ /** One OTLP/JSON span, as Intelligence's ingest reads it. */
18
+ export interface OtlpSpan {
19
+ traceId: string;
20
+ spanId: string;
21
+ parentSpanId?: string;
22
+ name: string;
23
+ kind: number;
24
+ startTimeUnixNano: string;
25
+ endTimeUnixNano: string;
26
+ attributes: Array<{
27
+ key: string;
28
+ value: {
29
+ stringValue?: string;
30
+ intValue?: string;
31
+ doubleValue?: number;
32
+ boolValue?: boolean;
33
+ };
34
+ }>;
35
+ status: {
36
+ code: number;
37
+ message?: string;
38
+ };
39
+ }
40
+ /**
41
+ * A Tangle trace id as the 32-hex OTLP trace id. The mapping is the Sandbox SDK's `otelTraceIdForTangleTrace`, so a
42
+ * Sandbox trace export of the same turn lands in the same OTLP trace. An id that is already 32 hex, or a UUID, is used as it is.
43
+ */
44
+ export declare function otlpTraceId(traceId: string): string;
14
45
  /**
15
46
  * The root span (`agent_app.turn`) carries the host's labels and the turn's
16
47
  * numbers: wall time, the share inside child spans, and the time credited to
17
48
  * each kind of span. Children follow under it.
18
49
  */
19
- export declare function buildTurnTraceOtelSpans(snapshot: TurnTraceSnapshot, labels?: TurnSpanAttrs): OtelSpan[];
50
+ export declare function buildTurnTraceOtelSpans(snapshot: TurnTraceSnapshot, labels?: TurnSpanAttrs): OtlpSpan[];
20
51
  export interface IntelligenceTurnTraceSinkOptions {
21
52
  /** The Intelligence project the spans belong to, e.g. `legal-agent`. */
22
53
  project: string;
23
54
  /** The tenant key the ingest requires. Without one the sink does nothing. */
24
55
  apiKey: string | undefined;
25
- /** Default `https://intelligence.tangle.tools`; spans go to `<base>/v1/otlp`. */
56
+ /** Default `https://intelligence.tangle.tools`; spans go to `<base>/v1/otlp/v1/traces`. */
26
57
  baseUrl?: string;
27
58
  /** Called with a short reason when a delivery fails. Never receives span content. */
28
59
  onError?: (reason: string) => void;
60
+ /** Milliseconds one POST may take (default 5000). */
61
+ timeoutMs?: number;
29
62
  }
30
63
  /**
31
- * The sink a chat route or a Worker turn hands each finished trace to. The
32
- * exporter is built on first use, inside a request, because a Worker refuses a
33
- * timer at module scope. Each trace is flushed before the sink returns, so a
34
- * host that runs it under `waitUntil` keeps the isolate alive until delivery.
64
+ * The sink a chat route or a Worker turn hands each finished trace to: one OTLP POST per turn, awaited, so a host that
65
+ * runs it under `waitUntil` keeps the isolate alive until delivery. It never throws; a failure goes to `onError`.
35
66
  */
36
67
  export declare function createIntelligenceTurnTraceSink(options: IntelligenceTurnTraceSinkOptions): TurnTraceSink;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-app",
3
- "version": "0.60.200",
3
+ "version": "0.60.202",
4
4
  "packageManager": "pnpm@11.24.0",
5
5
  "description": "Build agent applications with typed chat, tools, sandboxes, integrations, billing, and evaluation.",
6
6
  "keywords": [