@tangle-network/agent-app 0.60.204 → 0.60.206

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.
Files changed (31) hide show
  1. package/dist/chat-routes/index.js +7 -6
  2. package/dist/chat-routes/index.js.map +1 -1
  3. package/dist/chat-routes/turn-routes.d.ts +5 -0
  4. package/dist/{chunk-ZHJ2TDWA.js → chunk-7O6UQJJA.js} +2 -2
  5. package/dist/{chunk-ZRUR3DRU.js → chunk-HA5USG2Z.js} +6 -3
  6. package/dist/chunk-HA5USG2Z.js.map +1 -0
  7. package/dist/{chunk-N56KDEC4.js → chunk-HPFROQZS.js} +36 -18
  8. package/dist/chunk-HPFROQZS.js.map +1 -0
  9. package/dist/{chunk-VQSDYGQ6.js → chunk-J4BZ6NPT.js} +3 -3
  10. package/dist/chunk-PIUYJTTK.js +63 -0
  11. package/dist/chunk-PIUYJTTK.js.map +1 -0
  12. package/dist/{chunk-JKQPKS4O.js → chunk-PMIM6DE7.js} +2 -2
  13. package/dist/{chunk-7XQIY2LU.js → chunk-T7CWWC3F.js} +5 -52
  14. package/dist/chunk-T7CWWC3F.js.map +1 -0
  15. package/dist/public-consultation/index.js +3 -2
  16. package/dist/public-consultation/index.js.map +1 -1
  17. package/dist/runtime/index.js +5 -4
  18. package/dist/runtime/index.js.map +1 -1
  19. package/dist/runtime/router-turn.js +4 -3
  20. package/dist/sandbox/index.js +3 -2
  21. package/dist/turn-trace/index.js +21 -7
  22. package/dist/turn-trace/index.js.map +1 -1
  23. package/dist/turn-trace/recorder.d.ts +2 -0
  24. package/dist/turn-trace/server-timing.d.ts +2 -0
  25. package/package.json +1 -1
  26. package/dist/chunk-7XQIY2LU.js.map +0 -1
  27. package/dist/chunk-N56KDEC4.js.map +0 -1
  28. package/dist/chunk-ZRUR3DRU.js.map +0 -1
  29. /package/dist/{chunk-ZHJ2TDWA.js.map → chunk-7O6UQJJA.js.map} +0 -0
  30. /package/dist/{chunk-VQSDYGQ6.js.map → chunk-J4BZ6NPT.js.map} +0 -0
  31. /package/dist/{chunk-JKQPKS4O.js.map → chunk-PMIM6DE7.js.map} +0 -0
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  nextCallIndex,
3
3
  tracedModelFetch
4
- } from "./chunk-7XQIY2LU.js";
4
+ } from "./chunk-T7CWWC3F.js";
5
5
  import {
6
6
  DEFAULT_TANGLE_ROUTER_BASE_URL
7
7
  } from "./chunk-D6UJEOZQ.js";
@@ -10,7 +10,7 @@ import {
10
10
  } from "./chunk-AMUXRAUO.js";
11
11
  import {
12
12
  turnTraceId
13
- } from "./chunk-ZRUR3DRU.js";
13
+ } from "./chunk-HA5USG2Z.js";
14
14
 
15
15
  // src/runtime/loop.ts
16
16
  import {
@@ -892,4 +892,4 @@ export {
892
892
  composeRouterTurnPrompt,
893
893
  runRouterTurn
894
894
  };
895
- //# sourceMappingURL=chunk-VQSDYGQ6.js.map
895
+ //# sourceMappingURL=chunk-J4BZ6NPT.js.map
@@ -0,0 +1,63 @@
1
+ // src/turn-trace/server-timing.ts
2
+ function splitEntries(header) {
3
+ const out = [];
4
+ let current = "";
5
+ let quoted = false;
6
+ for (const char of header) {
7
+ if (char === '"') quoted = !quoted;
8
+ if (char === "," && !quoted) {
9
+ out.push(current);
10
+ current = "";
11
+ } else {
12
+ current += char;
13
+ }
14
+ }
15
+ out.push(current);
16
+ return out;
17
+ }
18
+ function parseServerTiming(header) {
19
+ if (!header) return [];
20
+ const entries = [];
21
+ for (const raw of splitEntries(header.slice(0, 4096))) {
22
+ const [name, ...params] = raw.split(";").map((part) => part.trim());
23
+ if (!name || !/^[\w.!#$%&'*+^`|~-]+$/.test(name)) continue;
24
+ const entry = { name };
25
+ for (const param of params) {
26
+ const eq = param.indexOf("=");
27
+ if (eq < 0) continue;
28
+ const key = param.slice(0, eq).trim().toLowerCase();
29
+ const value = param.slice(eq + 1).trim();
30
+ if (key === "dur") {
31
+ const dur = Number(value);
32
+ if (Number.isFinite(dur) && dur >= 0) entry.durMs = dur;
33
+ } else if (key === "desc") {
34
+ entry.desc = value.replace(/^"|"$/g, "").slice(0, 120);
35
+ }
36
+ }
37
+ entries.push(entry);
38
+ }
39
+ return entries;
40
+ }
41
+ function serverTimingAttrs(entries) {
42
+ const out = {};
43
+ for (const entry of entries.slice(0, 32)) {
44
+ const key = entry.name.replace(/[^\w]/g, "_");
45
+ if (entry.durMs !== void 0) out[`server_timing.${key}_ms`] = Math.round(entry.durMs * 10) / 10;
46
+ else if (entry.desc !== void 0) out[`server_timing.${key}`] = entry.desc;
47
+ }
48
+ return out;
49
+ }
50
+ function formatServerTiming(entries) {
51
+ return entries.filter((entry) => /^[\w.!#$%&'*+^`|~-]+$/.test(entry.name)).slice(0, 32).map((entry) => [
52
+ entry.name,
53
+ ...entry.durMs === void 0 ? [] : [`dur=${Math.round(entry.durMs * 10) / 10}`],
54
+ ...entry.desc === void 0 ? [] : [`desc="${entry.desc.replace(/["\\]/g, "").slice(0, 120)}"`]
55
+ ].join(";")).join(", ");
56
+ }
57
+
58
+ export {
59
+ parseServerTiming,
60
+ serverTimingAttrs,
61
+ formatServerTiming
62
+ };
63
+ //# sourceMappingURL=chunk-PIUYJTTK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/turn-trace/server-timing.ts"],"sourcesContent":["/** One entry of a `Server-Timing` response header. */\nexport interface ServerTimingEntry {\n name: string\n /** Milliseconds, when the entry carries `dur`. */\n durMs?: number\n /** `desc`, unquoted: a count or a label, never a duration. */\n desc?: string\n}\n\n/** Split on commas that sit outside a quoted `desc`. */\nfunction splitEntries(header: string): string[] {\n const out: string[] = []\n let current = ''\n let quoted = false\n for (const char of header) {\n if (char === '\"') quoted = !quoted\n if (char === ',' && !quoted) {\n out.push(current)\n current = ''\n } else {\n current += char\n }\n }\n out.push(current)\n return out\n}\n\n/**\n * Parse a `Server-Timing` header, as the Router and the Sandbox orchestrator\n * send it. A malformed entry is skipped; the result never throws.\n */\nexport function parseServerTiming(header: string | null | undefined): ServerTimingEntry[] {\n if (!header) return []\n const entries: ServerTimingEntry[] = []\n for (const raw of splitEntries(header.slice(0, 4096))) {\n const [name, ...params] = raw.split(';').map((part) => part.trim())\n if (!name || !/^[\\w.!#$%&'*+^`|~-]+$/.test(name)) continue\n const entry: ServerTimingEntry = { name }\n for (const param of params) {\n const eq = param.indexOf('=')\n if (eq < 0) continue\n const key = param.slice(0, eq).trim().toLowerCase()\n const value = param.slice(eq + 1).trim()\n if (key === 'dur') {\n const dur = Number(value)\n if (Number.isFinite(dur) && dur >= 0) entry.durMs = dur\n } else if (key === 'desc') {\n entry.desc = value.replace(/^\"|\"$/g, '').slice(0, 120)\n }\n }\n entries.push(entry)\n }\n return entries\n}\n\n/** Flat span attributes for the entries: `server_timing.<name>_ms` for a duration, `server_timing.<name>` for a `desc`. */\nexport function serverTimingAttrs(entries: readonly ServerTimingEntry[]): Record<string, string | number> {\n const out: Record<string, string | number> = {}\n for (const entry of entries.slice(0, 32)) {\n const key = entry.name.replace(/[^\\w]/g, '_')\n if (entry.durMs !== undefined) out[`server_timing.${key}_ms`] = Math.round(entry.durMs * 10) / 10\n else if (entry.desc !== undefined) out[`server_timing.${key}`] = entry.desc\n }\n return out\n}\n\n/** Format entries as a `Server-Timing` header value, the inverse of {@link parseServerTiming}. Invalid names are skipped. */\nexport function formatServerTiming(entries: readonly ServerTimingEntry[]): string {\n return entries\n .filter((entry) => /^[\\w.!#$%&'*+^`|~-]+$/.test(entry.name))\n .slice(0, 32)\n .map((entry) => [\n entry.name,\n ...(entry.durMs === undefined ? [] : [`dur=${Math.round(entry.durMs * 10) / 10}`]),\n ...(entry.desc === undefined ? [] : [`desc=\"${entry.desc.replace(/[\"\\\\]/g, '').slice(0, 120)}\"`]),\n ].join(';'))\n .join(', ')\n}\n"],"mappings":";AAUA,SAAS,aAAa,QAA0B;AAC9C,QAAM,MAAgB,CAAC;AACvB,MAAI,UAAU;AACd,MAAI,SAAS;AACb,aAAW,QAAQ,QAAQ;AACzB,QAAI,SAAS,IAAK,UAAS,CAAC;AAC5B,QAAI,SAAS,OAAO,CAAC,QAAQ;AAC3B,UAAI,KAAK,OAAO;AAChB,gBAAU;AAAA,IACZ,OAAO;AACL,iBAAW;AAAA,IACb;AAAA,EACF;AACA,MAAI,KAAK,OAAO;AAChB,SAAO;AACT;AAMO,SAAS,kBAAkB,QAAwD;AACxF,MAAI,CAAC,OAAQ,QAAO,CAAC;AACrB,QAAM,UAA+B,CAAC;AACtC,aAAW,OAAO,aAAa,OAAO,MAAM,GAAG,IAAI,CAAC,GAAG;AACrD,UAAM,CAAC,MAAM,GAAG,MAAM,IAAI,IAAI,MAAM,GAAG,EAAE,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC;AAClE,QAAI,CAAC,QAAQ,CAAC,wBAAwB,KAAK,IAAI,EAAG;AAClD,UAAM,QAA2B,EAAE,KAAK;AACxC,eAAW,SAAS,QAAQ;AAC1B,YAAM,KAAK,MAAM,QAAQ,GAAG;AAC5B,UAAI,KAAK,EAAG;AACZ,YAAM,MAAM,MAAM,MAAM,GAAG,EAAE,EAAE,KAAK,EAAE,YAAY;AAClD,YAAM,QAAQ,MAAM,MAAM,KAAK,CAAC,EAAE,KAAK;AACvC,UAAI,QAAQ,OAAO;AACjB,cAAM,MAAM,OAAO,KAAK;AACxB,YAAI,OAAO,SAAS,GAAG,KAAK,OAAO,EAAG,OAAM,QAAQ;AAAA,MACtD,WAAW,QAAQ,QAAQ;AACzB,cAAM,OAAO,MAAM,QAAQ,UAAU,EAAE,EAAE,MAAM,GAAG,GAAG;AAAA,MACvD;AAAA,IACF;AACA,YAAQ,KAAK,KAAK;AAAA,EACpB;AACA,SAAO;AACT;AAGO,SAAS,kBAAkB,SAAwE;AACxG,QAAM,MAAuC,CAAC;AAC9C,aAAW,SAAS,QAAQ,MAAM,GAAG,EAAE,GAAG;AACxC,UAAM,MAAM,MAAM,KAAK,QAAQ,UAAU,GAAG;AAC5C,QAAI,MAAM,UAAU,OAAW,KAAI,iBAAiB,GAAG,KAAK,IAAI,KAAK,MAAM,MAAM,QAAQ,EAAE,IAAI;AAAA,aACtF,MAAM,SAAS,OAAW,KAAI,iBAAiB,GAAG,EAAE,IAAI,MAAM;AAAA,EACzE;AACA,SAAO;AACT;AAGO,SAAS,mBAAmB,SAA+C;AAChF,SAAO,QACJ,OAAO,CAAC,UAAU,wBAAwB,KAAK,MAAM,IAAI,CAAC,EAC1D,MAAM,GAAG,EAAE,EACX,IAAI,CAAC,UAAU;AAAA,IACd,MAAM;AAAA,IACN,GAAI,MAAM,UAAU,SAAY,CAAC,IAAI,CAAC,OAAO,KAAK,MAAM,MAAM,QAAQ,EAAE,IAAI,EAAE,EAAE;AAAA,IAChF,GAAI,MAAM,SAAS,SAAY,CAAC,IAAI,CAAC,SAAS,MAAM,KAAK,QAAQ,UAAU,EAAE,EAAE,MAAM,GAAG,GAAG,CAAC,GAAG;AAAA,EACjG,EAAE,KAAK,GAAG,CAAC,EACV,KAAK,IAAI;AACd;","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runRouterTurn
3
- } from "./chunk-VQSDYGQ6.js";
3
+ } from "./chunk-J4BZ6NPT.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-JKQPKS4O.js.map
222
+ //# sourceMappingURL=chunk-PMIM6DE7.js.map
@@ -1,52 +1,7 @@
1
- // src/turn-trace/server-timing.ts
2
- function splitEntries(header) {
3
- const out = [];
4
- let current = "";
5
- let quoted = false;
6
- for (const char of header) {
7
- if (char === '"') quoted = !quoted;
8
- if (char === "," && !quoted) {
9
- out.push(current);
10
- current = "";
11
- } else {
12
- current += char;
13
- }
14
- }
15
- out.push(current);
16
- return out;
17
- }
18
- function parseServerTiming(header) {
19
- if (!header) return [];
20
- const entries = [];
21
- for (const raw of splitEntries(header.slice(0, 4096))) {
22
- const [name, ...params] = raw.split(";").map((part) => part.trim());
23
- if (!name || !/^[\w.!#$%&'*+^`|~-]+$/.test(name)) continue;
24
- const entry = { name };
25
- for (const param of params) {
26
- const eq = param.indexOf("=");
27
- if (eq < 0) continue;
28
- const key = param.slice(0, eq).trim().toLowerCase();
29
- const value = param.slice(eq + 1).trim();
30
- if (key === "dur") {
31
- const dur = Number(value);
32
- if (Number.isFinite(dur) && dur >= 0) entry.durMs = dur;
33
- } else if (key === "desc") {
34
- entry.desc = value.replace(/^"|"$/g, "").slice(0, 120);
35
- }
36
- }
37
- entries.push(entry);
38
- }
39
- return entries;
40
- }
41
- function serverTimingAttrs(entries) {
42
- const out = {};
43
- for (const entry of entries.slice(0, 32)) {
44
- const key = entry.name.replace(/[^\w]/g, "_");
45
- if (entry.durMs !== void 0) out[`server_timing.${key}_ms`] = Math.round(entry.durMs * 10) / 10;
46
- else if (entry.desc !== void 0) out[`server_timing.${key}`] = entry.desc;
47
- }
48
- return out;
49
- }
1
+ import {
2
+ parseServerTiming,
3
+ serverTimingAttrs
4
+ } from "./chunk-PIUYJTTK.js";
50
5
 
51
6
  // src/turn-trace/traced-fetch.ts
52
7
  var counters = /* @__PURE__ */ new WeakMap();
@@ -143,9 +98,7 @@ function tracedModelFetch(trace, inner, options = {}) {
143
98
  }
144
99
 
145
100
  export {
146
- parseServerTiming,
147
- serverTimingAttrs,
148
101
  nextCallIndex,
149
102
  tracedModelFetch
150
103
  };
151
- //# sourceMappingURL=chunk-7XQIY2LU.js.map
104
+ //# sourceMappingURL=chunk-T7CWWC3F.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/turn-trace/traced-fetch.ts"],"sourcesContent":["import type { TurnSpan, TurnTrace } from './recorder'\nimport { parseServerTiming, serverTimingAttrs } from './server-timing'\n\nconst counters = new WeakMap<TurnTrace, Map<string, number>>()\n\n/** 1-based count of `name` spans started through {@link nextCallIndex} on this trace. */\nexport function nextCallIndex(trace: TurnTrace, name: string): number {\n const byName = counters.get(trace) ?? new Map<string, number>()\n counters.set(trace, byName)\n const next = (byName.get(name) ?? 0) + 1\n byName.set(name, next)\n return next\n}\n\nexport interface TracedModelFetchOptions {\n /** The span a model call hangs from. Default: the trace's root. */\n parent?: TurnSpan\n /** The model the request asks for. */\n model?: string\n /** Receives each model call's span as it starts, so the engine can add the token counts when usage arrives. */\n onSpan?: (span: TurnSpan) => void\n}\n\n/**\n * `fetch` for a model request, recording one `model.call` span per send: the\n * wall time from the request to the end of its body, the time to the first\n * byte, and the Router's own `Server-Timing` so the wait before the first byte\n * splits into network and server time.\n *\n * Children partition the call:\n * - `network`: the wait for headers, less the Router's server time;\n * - `router.pre_upstream`: the Router's work before its provider fetch\n * (`upstream_start`);\n * - `router.upstream_wait`: the provider's time to response headers\n * (`upstream_headers`);\n * - `model.stream`: headers to the end of the body.\n * Without a `Server-Timing` header the first byte's wait is one `model.wait`\n * span, because the split is not known.\n *\n * The response body is passed through unchanged; the span ends when the body\n * ends, errors or is cancelled.\n */\nexport function tracedModelFetch(trace: TurnTrace, inner: typeof fetch, options: TracedModelFetchOptions = {}): typeof fetch {\n return async (input, init) => {\n const sentAt = Date.now()\n const span = trace.start('model.call', 'model', {\n 'gen_ai.operation.name': 'chat',\n 'call.index': nextCallIndex(trace, 'model.call'),\n ...(options.model ? { 'gen_ai.request.model': options.model } : {}),\n }, options.parent)\n options.onSpan?.(span)\n let response: Response\n try {\n response = await inner(input, init)\n } catch (error) {\n span.end({ 'http.failed': true }, error)\n throw error\n }\n const headersAt = Date.now()\n const timing = parseServerTiming(response.headers.get('server-timing'))\n const stamp = (name: string) => timing.find((entry) => entry.name === name)?.durMs\n const preUpstreamMs = stamp('upstream_start')\n const upstreamWaitMs = stamp('upstream_headers')\n const waitMs = headersAt - sentAt\n span.set({ 'http.status_code': response.status, ttfb_ms: waitMs, ...serverTimingAttrs(timing) })\n\n let firstByteAt: number | undefined\n const finish = (endedAt: number, error?: unknown, cancelled?: boolean) => {\n const known = preUpstreamMs !== undefined || upstreamWaitMs !== undefined\n if (known) {\n const serverMs = Math.min(waitMs, (preUpstreamMs ?? 0) + (upstreamWaitMs ?? 0))\n const networkMs = waitMs - serverMs\n let at = sentAt\n span.record('network', 'network', at, at + networkMs, { 'network.ms': Math.round(networkMs) })\n at += networkMs\n if (preUpstreamMs !== undefined) {\n const end = Math.min(headersAt, at + preUpstreamMs)\n span.record('router.pre_upstream', 'router', at, end)\n at = end\n }\n if (upstreamWaitMs !== undefined) span.record('router.upstream_wait', 'model', at, headersAt)\n span.set({ network_ms: Math.round(networkMs), router_server_ms: Math.round(serverMs) })\n } else {\n span.record('model.wait', 'model', sentAt, headersAt, { split: 'unknown' })\n }\n span.record('model.stream', 'model', headersAt, Math.max(headersAt, endedAt))\n const failure = error ?? (response.status >= 400 ? `http_${response.status}` : undefined)\n span.end({\n ...(firstByteAt !== undefined ? { ttft_ms: firstByteAt - sentAt } : {}),\n ...(cancelled ? { cancelled: true } : {}),\n }, failure)\n }\n\n if (!response.body) {\n finish(headersAt)\n return response\n }\n const reader = response.body.getReader()\n let settled = false\n const settle = (error?: unknown, cancelled?: boolean) => {\n if (settled) return\n settled = true\n finish(Date.now(), error, cancelled)\n }\n const body = new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n const { done, value } = await reader.read()\n if (done) {\n settle()\n controller.close()\n return\n }\n firstByteAt ??= Date.now()\n controller.enqueue(value)\n } catch (error) {\n settle(error)\n controller.error(error)\n }\n },\n async cancel(reason) {\n settle(undefined, true)\n await reader.cancel(reason)\n },\n })\n return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers })\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createChatTurnRoutes,
3
3
  streamChatRouteAsSandboxEvents
4
- } from "../chunk-N56KDEC4.js";
4
+ } from "../chunk-HPFROQZS.js";
5
5
  import "../chunk-Q7WEEN7P.js";
6
6
  import "../chunk-UTAIRVOK.js";
7
7
  import "../chunk-RN4WPTBH.js";
@@ -17,7 +17,8 @@ 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-ZRUR3DRU.js";
20
+ import "../chunk-HA5USG2Z.js";
21
+ import "../chunk-PIUYJTTK.js";
21
22
 
22
23
  // src/public-consultation/index.ts
23
24
  import { searchKnowledge } from "@tangle-network/agent-knowledge";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/public-consultation/index.ts"],"sourcesContent":["import { searchKnowledge, type KnowledgeIndex, type KnowledgePage } from '@tangle-network/agent-knowledge'\nimport { createChatTurnRoutes, type ChatTurnExecutionLimits, type ChatTurnLock, type ChatTurnMessageStore, type ChatTurnRouteProducer } from '../chat-routes/turn-routes'\nimport { streamChatRouteAsSandboxEvents } from '../chat-routes/gateway-adapter'\nimport type { TurnEventStore } from '../stream/turn-buffer'\nimport type { McpToolDefinition } from '../tools/mcp-rpc'\n\nexport interface PublishedKnowledgePage {\n readonly id: string\n readonly title: string\n readonly text: string\n}\n\n/** A server-created publication. Every selected page's complete text is public. */\nexport interface KnowledgePublication {\n readonly id: string\n readonly revision: string\n readonly ownerId: string\n readonly systemPrompt: string\n read(pageId: string): PublishedKnowledgePage | null\n search(query: string): PublishedKnowledgePage[]\n}\n\n/** Select content at publication time, before any payer or model input exists. */\nexport function createKnowledgePublication(input: {\n id: string\n revision: string\n ownerId: string\n systemPrompt: string\n index: KnowledgeIndex\n pageIds: readonly string[]\n}): KnowledgePublication {\n for (const value of [input.id, input.revision, input.ownerId]) requireIdentity(value)\n const pages: KnowledgePage[] = []\n for (const id of new Set(input.pageIds)) {\n const matches = input.index.pages.filter(page => page.id === id)\n if (matches.length !== 1) throw new Error('Publication page must resolve exactly once')\n const page = matches[0]!\n // Owner metadata, paths, links and source records can refer to private resources.\n pages.push({ id: page.id, title: page.title, text: page.text, path: page.id,\n frontmatter: {}, sourceIds: [], tags: [], outLinks: [] })\n }\n const index: KnowledgeIndex = { root: '', generatedAt: '', sources: [], pages, graph: { nodes: [], edges: [] } }\n const publicPage = (page: KnowledgePage): PublishedKnowledgePage =>\n Object.freeze({ id: page.id, title: page.title, text: page.text })\n return Object.freeze({\n id: input.id, revision: input.revision, ownerId: input.ownerId, systemPrompt: input.systemPrompt,\n read: (id: string) => {\n const page = pages.find(candidate => candidate.id === id)\n return page ? publicPage(page) : null\n },\n search: (query: string) => searchKnowledge(index, query, { limit: 10 }).map(hit => publicPage(hit.page)),\n })\n}\n\n/** Authenticated server identity. Never construct this from model arguments or request JSON. */\nexport interface ConsultationConsumer {\n method: string\n consumerId: string\n keyId?: string\n ownerId?: string\n requestId: string\n threadId?: string\n}\n\nexport interface ConsultationAgent {\n id: string\n ownerId: string\n}\n\nexport interface ConsultationIdentity {\n readonly publicationId: string\n readonly publicationRevision: string\n readonly consumerId: string\n readonly paymentMethod: string\n readonly workspaceId: string\n readonly threadId: string\n}\n\nexport interface ConsultationExecutionControl {\n readonly identity: ConsultationIdentity\n readonly signal: AbortSignal\n readonly executionLimits?: Readonly<ChatTurnExecutionLimits>\n}\n\nexport type PreparedConsultationExecution =\n | { status: 'unsupported'; reason: string }\n | { status: 'prepared'; produce(input: ConsultationExecution): ChatTurnRouteProducer | Promise<ChatTurnRouteProducer> }\n\nexport interface ConsultationExecution extends ConsultationExecutionControl {\n readonly identity: ConsultationIdentity\n readonly systemPrompt: string\n readonly prompt: string\n readonly priorMessages: ReadonlyArray<{ role: string; content: string }>\n readonly tools: McpToolDefinition[]\n}\n\nexport interface PublicConsultationOptions {\n /** Resolve only deliberately published snapshots. Return null after revocation. */\n resolvePublication(agentId: string): Promise<KnowledgePublication | null>\n /** Payment authentication does not substitute for this admission decision. */\n allowConsumer(publication: KnowledgePublication, consumer: ConsultationConsumer): Promise<boolean>\n store: ChatTurnMessageStore\n turnStore: TurnEventStore\n /** Shared durable single-flight lock. Use createDurableTurnLock with thread scope. */\n turnLock: ChatTurnLock<void>\n /** Create or read the stored parent row. Return its actual persisted ownership. */\n ensureConversation(identity: ConsultationIdentity): Promise<{ workspaceId: string; threadId: string }>\n /** Prepare without compute. Refuse limits the isolated executor cannot enforce across all model and tool calls. */\n prepareExecution(input: ConsultationExecutionControl): Promise<PreparedConsultationExecution>\n}\n\ninterface GatewayConsultationContext {\n consumerId: string\n paymentMethod: string\n requestId: string\n threadId?: string\n keyInfo: { keyId: string; ownerId?: string } | null\n messages: Array<{ role: string; content: string }>\n}\n\n/** Compose gateway admission, scoped knowledge and the maintained persisted chat route. */\nexport function createPublicConsultation(options: PublicConsultationOptions) {\n if (typeof options.turnLock?.acquire !== 'function' || typeof options.turnLock.release !== 'function') {\n throw new Error('Public consultation requires a shared turn lock')\n }\n async function ownsConversation(identity: ConsultationIdentity) {\n const conversation = await options.ensureConversation(identity)\n return conversation?.workspaceId === identity.workspaceId && conversation.threadId === identity.threadId\n }\n async function resolve(agent: ConsultationAgent, consumer: ConsultationConsumer) {\n requireIdentity(consumer.consumerId)\n requireIdentity(consumer.method)\n requireIdentity(consumer.threadId)\n const publication = await options.resolvePublication(agent.id)\n if (!publication || publication.id !== agent.id || publication.ownerId !== agent.ownerId\n || await options.allowConsumer(publication, consumer) !== true) return null\n const workspaceId = await scopedId([publication.ownerId, publication.id, publication.revision, consumer.method, consumer.consumerId])\n const threadId = await scopedId([workspaceId, consumer.threadId!])\n const identity: ConsultationIdentity = Object.freeze({\n publicationId: publication.id, publicationRevision: publication.revision,\n consumerId: consumer.consumerId, paymentMethod: consumer.method, workspaceId, threadId,\n })\n return { publication, identity }\n }\n\n return {\n async authorizeConsumer(agent: ConsultationAgent, consumer: ConsultationConsumer) {\n const scope = await resolve(agent, consumer)\n return scope\n ? { allow: true as const }\n : { allow: false as const, reason: 'Consultation access denied', code: 'consultation_forbidden' }\n },\n async readKnowledge(agent: ConsultationAgent, consumer: ConsultationConsumer, pageId: string) {\n const scope = await resolve(agent, consumer)\n if (!scope) throw new Error('Consultation access denied')\n return scope.publication.read(pageId)\n },\n async readHistory(agent: ConsultationAgent, consumer: ConsultationConsumer) {\n const scope = await resolve(agent, consumer)\n if (!scope) throw new Error('Consultation access denied')\n if (!await ownsConversation(scope.identity)) throw new Error('Consultation conversation conflict')\n return (await options.store.listMessages(scope.identity.threadId))\n .map(({ role, content }) => ({ role, content }))\n },\n async getSandbox(agent: ConsultationAgent, context?: GatewayConsultationContext) {\n if (!context) throw new Error('Authenticated consultation identity is required')\n const consumer: ConsultationConsumer = {\n consumerId: context.consumerId, method: context.paymentMethod,\n requestId: context.requestId, threadId: context.threadId,\n keyId: context.keyInfo?.keyId, ownerId: context.keyInfo?.ownerId,\n }\n const scope = await resolve(agent, consumer)\n if (!scope) throw new Error('Consultation access denied')\n // Client-supplied history and owner system prompts never enter this producer.\n const prompt = context.messages.filter(message => message.role === 'user').at(-1)?.content\n if (!prompt) throw new Error('Consultation requires a user message')\n type PromptOptions = { signal?: AbortSignal; executionBudget?: ChatTurnExecutionLimits; maxOutputTokens?: number }\n const prepare = async (streamOptions?: PromptOptions) => {\n const signal = streamOptions?.signal ?? new AbortController().signal\n signal.throwIfAborted()\n const supplied = streamOptions?.executionBudget\n const executionLimits = supplied || streamOptions?.maxOutputTokens !== undefined\n ? Object.freeze({ ...supplied, ...(streamOptions?.maxOutputTokens !== undefined\n ? { maxOutputTokens: Math.min(streamOptions.maxOutputTokens, supplied?.maxOutputTokens ?? Infinity) } : {}) })\n : undefined\n if (executionLimits && Object.values(executionLimits).some(value => typeof value !== 'number' || !Number.isFinite(value) || value < 0)) {\n return { status: 'unsupported' as const, reason: 'Invalid consultation execution limits' }\n }\n const control = Object.freeze({ identity: scope.identity, signal, executionLimits })\n const prepared = await options.prepareExecution(control)\n signal.throwIfAborted()\n if (prepared?.status !== 'prepared' || typeof prepared.produce !== 'function') {\n return { status: 'unsupported' as const, reason: prepared?.status === 'unsupported'\n ? prepared.reason : 'Invalid consultation execution preparation' }\n }\n let started = false\n const start = () => {\n signal.throwIfAborted()\n if (started) throw new Error('Consultation execution already started')\n started = true\n const routes = createChatTurnRoutes({\n projectId: 'public-consultation', store: options.store, turnStore: options.turnStore, turnLock: options.turnLock,\n authorize: async ({ intent, body }) => {\n const current = await resolve(agent, consumer)\n if (intent !== 'turn' || !current\n || current.identity.workspaceId !== scope.identity.workspaceId\n || body?.workspaceId !== scope.identity.workspaceId || body.threadId !== scope.identity.threadId) {\n return { ok: false, response: Response.json({ error: 'Consultation access denied' }, { status: 403 }) }\n }\n if (!await ownsConversation(scope.identity)) {\n return { ok: false, response: Response.json({ error: 'Consultation conversation conflict' }, { status: 403 }) }\n }\n return { ok: true, tenantId: scope.identity.workspaceId,\n userId: scope.identity.consumerId, context: undefined }\n },\n produce: args => {\n signal.throwIfAborted()\n return prepared.produce({\n ...control,\n identity: scope.identity, systemPrompt: scope.publication.systemPrompt, prompt,\n priorMessages: args.priorMessages.map(({ role, content }) => ({ role, content })),\n tools: publicationTools(scope.publication, async () => {\n signal.throwIfAborted()\n const current = await resolve(agent, consumer)\n signal.throwIfAborted()\n if (!current || current.identity.workspaceId !== scope.identity.workspaceId) {\n throw new Error('Consultation access denied')\n }\n }),\n })\n },\n })\n return streamChatRouteAsSandboxEvents({\n routes, request: new Request('https://consultation.invalid/internal'),\n payload: { workspaceId: scope.identity.workspaceId, threadId: scope.identity.threadId,\n content: prompt, turnId: consumer.requestId }, signal, executionLimits,\n })\n }\n return { status: 'prepared' as const, start }\n }\n return {\n prepareBudgetedPrompt: (_message: string, streamOptions: PromptOptions) => prepare(streamOptions),\n streamPrompt: async function* (_message: string, streamOptions?: PromptOptions) {\n const prepared = await prepare(streamOptions)\n if (prepared.status !== 'prepared') throw new Error(prepared.reason)\n yield* prepared.start()\n },\n }\n },\n }\n}\n\nfunction publicationTools(publication: KnowledgePublication, authorize: () => Promise<void>): McpToolDefinition[] {\n return [\n { name: 'knowledge_search', description: 'Search published consultation knowledge.',\n inputSchema: { type: 'object', required: ['query'], properties: { query: { type: 'string' } }, additionalProperties: false },\n run: async args => { await authorize(); return publication.search(singleArgument(args, 'query')) } },\n { name: 'knowledge_read', description: 'Read a published page by its exact citation id.',\n inputSchema: { type: 'object', required: ['pageId'], properties: { pageId: { type: 'string' } }, additionalProperties: false },\n run: async args => { await authorize(); return publication.read(singleArgument(args, 'pageId')) } },\n ]\n}\n\nfunction singleArgument(args: Record<string, unknown>, name: string): string {\n const value = args[name]\n if (Object.keys(args).length !== 1 || typeof value !== 'string' || !value.trim() || value.length > 8192) {\n throw new Error('Invalid published knowledge arguments')\n }\n return value\n}\n\nfunction requireIdentity(value: unknown): asserts value is string {\n if (typeof value !== 'string' || !value.trim() || value.length > 512) throw new Error('Invalid consultation identity')\n}\n\nasync function scopedId(parts: string[]): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify(parts)))\n return `consultation:${Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('')}`\n}\n"],"mappings":"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","names":["page"]}
1
+ {"version":3,"sources":["../../src/public-consultation/index.ts"],"sourcesContent":["import { searchKnowledge, type KnowledgeIndex, type KnowledgePage } from '@tangle-network/agent-knowledge'\nimport { createChatTurnRoutes, type ChatTurnExecutionLimits, type ChatTurnLock, type ChatTurnMessageStore, type ChatTurnRouteProducer } from '../chat-routes/turn-routes'\nimport { streamChatRouteAsSandboxEvents } from '../chat-routes/gateway-adapter'\nimport type { TurnEventStore } from '../stream/turn-buffer'\nimport type { McpToolDefinition } from '../tools/mcp-rpc'\n\nexport interface PublishedKnowledgePage {\n readonly id: string\n readonly title: string\n readonly text: string\n}\n\n/** A server-created publication. Every selected page's complete text is public. */\nexport interface KnowledgePublication {\n readonly id: string\n readonly revision: string\n readonly ownerId: string\n readonly systemPrompt: string\n read(pageId: string): PublishedKnowledgePage | null\n search(query: string): PublishedKnowledgePage[]\n}\n\n/** Select content at publication time, before any payer or model input exists. */\nexport function createKnowledgePublication(input: {\n id: string\n revision: string\n ownerId: string\n systemPrompt: string\n index: KnowledgeIndex\n pageIds: readonly string[]\n}): KnowledgePublication {\n for (const value of [input.id, input.revision, input.ownerId]) requireIdentity(value)\n const pages: KnowledgePage[] = []\n for (const id of new Set(input.pageIds)) {\n const matches = input.index.pages.filter(page => page.id === id)\n if (matches.length !== 1) throw new Error('Publication page must resolve exactly once')\n const page = matches[0]!\n // Owner metadata, paths, links and source records can refer to private resources.\n pages.push({ id: page.id, title: page.title, text: page.text, path: page.id,\n frontmatter: {}, sourceIds: [], tags: [], outLinks: [] })\n }\n const index: KnowledgeIndex = { root: '', generatedAt: '', sources: [], pages, graph: { nodes: [], edges: [] } }\n const publicPage = (page: KnowledgePage): PublishedKnowledgePage =>\n Object.freeze({ id: page.id, title: page.title, text: page.text })\n return Object.freeze({\n id: input.id, revision: input.revision, ownerId: input.ownerId, systemPrompt: input.systemPrompt,\n read: (id: string) => {\n const page = pages.find(candidate => candidate.id === id)\n return page ? publicPage(page) : null\n },\n search: (query: string) => searchKnowledge(index, query, { limit: 10 }).map(hit => publicPage(hit.page)),\n })\n}\n\n/** Authenticated server identity. Never construct this from model arguments or request JSON. */\nexport interface ConsultationConsumer {\n method: string\n consumerId: string\n keyId?: string\n ownerId?: string\n requestId: string\n threadId?: string\n}\n\nexport interface ConsultationAgent {\n id: string\n ownerId: string\n}\n\nexport interface ConsultationIdentity {\n readonly publicationId: string\n readonly publicationRevision: string\n readonly consumerId: string\n readonly paymentMethod: string\n readonly workspaceId: string\n readonly threadId: string\n}\n\nexport interface ConsultationExecutionControl {\n readonly identity: ConsultationIdentity\n readonly signal: AbortSignal\n readonly executionLimits?: Readonly<ChatTurnExecutionLimits>\n}\n\nexport type PreparedConsultationExecution =\n | { status: 'unsupported'; reason: string }\n | { status: 'prepared'; produce(input: ConsultationExecution): ChatTurnRouteProducer | Promise<ChatTurnRouteProducer> }\n\nexport interface ConsultationExecution extends ConsultationExecutionControl {\n readonly identity: ConsultationIdentity\n readonly systemPrompt: string\n readonly prompt: string\n readonly priorMessages: ReadonlyArray<{ role: string; content: string }>\n readonly tools: McpToolDefinition[]\n}\n\nexport interface PublicConsultationOptions {\n /** Resolve only deliberately published snapshots. Return null after revocation. */\n resolvePublication(agentId: string): Promise<KnowledgePublication | null>\n /** Payment authentication does not substitute for this admission decision. */\n allowConsumer(publication: KnowledgePublication, consumer: ConsultationConsumer): Promise<boolean>\n store: ChatTurnMessageStore\n turnStore: TurnEventStore\n /** Shared durable single-flight lock. Use createDurableTurnLock with thread scope. */\n turnLock: ChatTurnLock<void>\n /** Create or read the stored parent row. Return its actual persisted ownership. */\n ensureConversation(identity: ConsultationIdentity): Promise<{ workspaceId: string; threadId: string }>\n /** Prepare without compute. Refuse limits the isolated executor cannot enforce across all model and tool calls. */\n prepareExecution(input: ConsultationExecutionControl): Promise<PreparedConsultationExecution>\n}\n\ninterface GatewayConsultationContext {\n consumerId: string\n paymentMethod: string\n requestId: string\n threadId?: string\n keyInfo: { keyId: string; ownerId?: string } | null\n messages: Array<{ role: string; content: string }>\n}\n\n/** Compose gateway admission, scoped knowledge and the maintained persisted chat route. */\nexport function createPublicConsultation(options: PublicConsultationOptions) {\n if (typeof options.turnLock?.acquire !== 'function' || typeof options.turnLock.release !== 'function') {\n throw new Error('Public consultation requires a shared turn lock')\n }\n async function ownsConversation(identity: ConsultationIdentity) {\n const conversation = await options.ensureConversation(identity)\n return conversation?.workspaceId === identity.workspaceId && conversation.threadId === identity.threadId\n }\n async function resolve(agent: ConsultationAgent, consumer: ConsultationConsumer) {\n requireIdentity(consumer.consumerId)\n requireIdentity(consumer.method)\n requireIdentity(consumer.threadId)\n const publication = await options.resolvePublication(agent.id)\n if (!publication || publication.id !== agent.id || publication.ownerId !== agent.ownerId\n || await options.allowConsumer(publication, consumer) !== true) return null\n const workspaceId = await scopedId([publication.ownerId, publication.id, publication.revision, consumer.method, consumer.consumerId])\n const threadId = await scopedId([workspaceId, consumer.threadId!])\n const identity: ConsultationIdentity = Object.freeze({\n publicationId: publication.id, publicationRevision: publication.revision,\n consumerId: consumer.consumerId, paymentMethod: consumer.method, workspaceId, threadId,\n })\n return { publication, identity }\n }\n\n return {\n async authorizeConsumer(agent: ConsultationAgent, consumer: ConsultationConsumer) {\n const scope = await resolve(agent, consumer)\n return scope\n ? { allow: true as const }\n : { allow: false as const, reason: 'Consultation access denied', code: 'consultation_forbidden' }\n },\n async readKnowledge(agent: ConsultationAgent, consumer: ConsultationConsumer, pageId: string) {\n const scope = await resolve(agent, consumer)\n if (!scope) throw new Error('Consultation access denied')\n return scope.publication.read(pageId)\n },\n async readHistory(agent: ConsultationAgent, consumer: ConsultationConsumer) {\n const scope = await resolve(agent, consumer)\n if (!scope) throw new Error('Consultation access denied')\n if (!await ownsConversation(scope.identity)) throw new Error('Consultation conversation conflict')\n return (await options.store.listMessages(scope.identity.threadId))\n .map(({ role, content }) => ({ role, content }))\n },\n async getSandbox(agent: ConsultationAgent, context?: GatewayConsultationContext) {\n if (!context) throw new Error('Authenticated consultation identity is required')\n const consumer: ConsultationConsumer = {\n consumerId: context.consumerId, method: context.paymentMethod,\n requestId: context.requestId, threadId: context.threadId,\n keyId: context.keyInfo?.keyId, ownerId: context.keyInfo?.ownerId,\n }\n const scope = await resolve(agent, consumer)\n if (!scope) throw new Error('Consultation access denied')\n // Client-supplied history and owner system prompts never enter this producer.\n const prompt = context.messages.filter(message => message.role === 'user').at(-1)?.content\n if (!prompt) throw new Error('Consultation requires a user message')\n type PromptOptions = { signal?: AbortSignal; executionBudget?: ChatTurnExecutionLimits; maxOutputTokens?: number }\n const prepare = async (streamOptions?: PromptOptions) => {\n const signal = streamOptions?.signal ?? new AbortController().signal\n signal.throwIfAborted()\n const supplied = streamOptions?.executionBudget\n const executionLimits = supplied || streamOptions?.maxOutputTokens !== undefined\n ? Object.freeze({ ...supplied, ...(streamOptions?.maxOutputTokens !== undefined\n ? { maxOutputTokens: Math.min(streamOptions.maxOutputTokens, supplied?.maxOutputTokens ?? Infinity) } : {}) })\n : undefined\n if (executionLimits && Object.values(executionLimits).some(value => typeof value !== 'number' || !Number.isFinite(value) || value < 0)) {\n return { status: 'unsupported' as const, reason: 'Invalid consultation execution limits' }\n }\n const control = Object.freeze({ identity: scope.identity, signal, executionLimits })\n const prepared = await options.prepareExecution(control)\n signal.throwIfAborted()\n if (prepared?.status !== 'prepared' || typeof prepared.produce !== 'function') {\n return { status: 'unsupported' as const, reason: prepared?.status === 'unsupported'\n ? prepared.reason : 'Invalid consultation execution preparation' }\n }\n let started = false\n const start = () => {\n signal.throwIfAborted()\n if (started) throw new Error('Consultation execution already started')\n started = true\n const routes = createChatTurnRoutes({\n projectId: 'public-consultation', store: options.store, turnStore: options.turnStore, turnLock: options.turnLock,\n authorize: async ({ intent, body }) => {\n const current = await resolve(agent, consumer)\n if (intent !== 'turn' || !current\n || current.identity.workspaceId !== scope.identity.workspaceId\n || body?.workspaceId !== scope.identity.workspaceId || body.threadId !== scope.identity.threadId) {\n return { ok: false, response: Response.json({ error: 'Consultation access denied' }, { status: 403 }) }\n }\n if (!await ownsConversation(scope.identity)) {\n return { ok: false, response: Response.json({ error: 'Consultation conversation conflict' }, { status: 403 }) }\n }\n return { ok: true, tenantId: scope.identity.workspaceId,\n userId: scope.identity.consumerId, context: undefined }\n },\n produce: args => {\n signal.throwIfAborted()\n return prepared.produce({\n ...control,\n identity: scope.identity, systemPrompt: scope.publication.systemPrompt, prompt,\n priorMessages: args.priorMessages.map(({ role, content }) => ({ role, content })),\n tools: publicationTools(scope.publication, async () => {\n signal.throwIfAborted()\n const current = await resolve(agent, consumer)\n signal.throwIfAborted()\n if (!current || current.identity.workspaceId !== scope.identity.workspaceId) {\n throw new Error('Consultation access denied')\n }\n }),\n })\n },\n })\n return streamChatRouteAsSandboxEvents({\n routes, request: new Request('https://consultation.invalid/internal'),\n payload: { workspaceId: scope.identity.workspaceId, threadId: scope.identity.threadId,\n content: prompt, turnId: consumer.requestId }, signal, executionLimits,\n })\n }\n return { status: 'prepared' as const, start }\n }\n return {\n prepareBudgetedPrompt: (_message: string, streamOptions: PromptOptions) => prepare(streamOptions),\n streamPrompt: async function* (_message: string, streamOptions?: PromptOptions) {\n const prepared = await prepare(streamOptions)\n if (prepared.status !== 'prepared') throw new Error(prepared.reason)\n yield* prepared.start()\n },\n }\n },\n }\n}\n\nfunction publicationTools(publication: KnowledgePublication, authorize: () => Promise<void>): McpToolDefinition[] {\n return [\n { name: 'knowledge_search', description: 'Search published consultation knowledge.',\n inputSchema: { type: 'object', required: ['query'], properties: { query: { type: 'string' } }, additionalProperties: false },\n run: async args => { await authorize(); return publication.search(singleArgument(args, 'query')) } },\n { name: 'knowledge_read', description: 'Read a published page by its exact citation id.',\n inputSchema: { type: 'object', required: ['pageId'], properties: { pageId: { type: 'string' } }, additionalProperties: false },\n run: async args => { await authorize(); return publication.read(singleArgument(args, 'pageId')) } },\n ]\n}\n\nfunction singleArgument(args: Record<string, unknown>, name: string): string {\n const value = args[name]\n if (Object.keys(args).length !== 1 || typeof value !== 'string' || !value.trim() || value.length > 8192) {\n throw new Error('Invalid published knowledge arguments')\n }\n return value\n}\n\nfunction requireIdentity(value: unknown): asserts value is string {\n if (typeof value !== 'string' || !value.trim() || value.length > 512) throw new Error('Invalid consultation identity')\n}\n\nasync function scopedId(parts: string[]): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify(parts)))\n return `consultation:${Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('')}`\n}\n"],"mappings":"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","names":["page"]}
@@ -2,7 +2,7 @@ import {
2
2
  ProtectedModelSettlementError,
3
3
  createRouterProtectedModelPort,
4
4
  runProtectedRouterTurn
5
- } from "../chunk-JKQPKS4O.js";
5
+ } from "../chunk-PMIM6DE7.js";
6
6
  import {
7
7
  DURABLE_HOP_BUDGET_MS,
8
8
  asksForLanguage,
@@ -24,8 +24,8 @@ import {
24
24
  turnKnowledgeInstructions,
25
25
  workspaceKnowledgeForTurn,
26
26
  workspaceTurnCheckpoints
27
- } from "../chunk-VQSDYGQ6.js";
28
- import "../chunk-7XQIY2LU.js";
27
+ } from "../chunk-J4BZ6NPT.js";
28
+ import "../chunk-T7CWWC3F.js";
29
29
  import {
30
30
  DEFAULT_TANGLE_BILLING_ENFORCEMENT_ENV_VAR,
31
31
  DEFAULT_TANGLE_ROUTER_BASE_URL,
@@ -52,7 +52,8 @@ import {
52
52
  resolveCatalogModelId,
53
53
  sortModelsByFreshness
54
54
  } from "../chunk-AMUXRAUO.js";
55
- import "../chunk-ZRUR3DRU.js";
55
+ import "../chunk-HA5USG2Z.js";
56
+ import "../chunk-PIUYJTTK.js";
56
57
 
57
58
  // src/runtime/certified-delivery.ts
58
59
  import {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/runtime/certified-delivery.ts","../../src/runtime/surface-profile.ts"],"sourcesContent":["/**\n * Application profile composition using Runtime's certified-guidance source.\n *\n * The shell adapts the prompt source to the application's resolved profile;\n * Runtime owns pulling, cache lifetime, concurrent refresh and retained guidance.\n * Only prompt-compatible guidance is folded here. Tool definitions, execution\n * permissions, MCP servers and files are not installed from certified material.\n * The complete profile remains available through `current()` for inspection and\n * the product's separately authorized materialization path.\n */\nimport {\n createCertifiedPromptSource,\n type CertifiedProfile,\n type CertifiedPromptSourceOptions,\n} from '@tangle-network/agent-runtime/intelligence'\nimport type { ResolvedAgentProfile } from './agent'\n\n/** Pull coordinates, refresh cadence and timeout owned by the shared source. */\nexport type CertifiedDeliveryConfig = CertifiedPromptSourceOptions\n\n/** Adapt certified prompt guidance without granting execution authority. */\nexport interface CertifiedDelivery {\n /** Refresh on the source's cadence and compose the current prompt guidance.\n * Other resolved profile fields pass through unchanged. */\n composeProfile(base: ResolvedAgentProfile): Promise<ResolvedAgentProfile>\n /** Force a pull now, coalescing with an in-flight pull. Best-effort; not a\n * post-activation revision barrier or a permission-revocation protocol. */\n refresh(): Promise<void>\n /** Last successfully pulled profile, or null before a successful pull. */\n current(): CertifiedProfile | null\n}\n\nexport function createCertifiedDelivery(config: CertifiedDeliveryConfig): CertifiedDelivery {\n const source = createCertifiedPromptSource(config)\n return {\n async composeProfile(base) {\n return { ...base, systemPrompt: await source.compose(base.systemPrompt) }\n },\n refresh: () => source.refresh({ force: true }),\n current: source.current,\n }\n}\n","/**\n * Surface-scoped profile overlay — the seam letting any product page (a\n * sequence editor, a brief composer, a dataset view) add MCP servers, a prompt\n * addendum, and permission tightening to the workspace agent profile for turns\n * initiated FROM that surface, without the chat orchestrator knowing any\n * surface's specifics. The orchestrator resolves `(kind, ctx)` through a\n * registry the REQUEST HANDLER constructs per request (construction is a Map\n * build — cheap) and merges the result into the base profile it was about to\n * send to the sandbox. Per-request construction is the trust mechanism, not an\n * optimization target: each `build()` closes over server-trusted request state\n * (env bindings, secrets, the AUTHENTICATED user/workspace), which on Workers\n * exists only per request — a startup-built registry would force identity\n * through the untrusted client `ctx`.\n *\n * SECURITY INVARIANT: the surface `kind` and the ids inside `ctx` arrive on\n * the client request and are pure ROUTING data — never trusted content, never\n * identity. Identity comes from the closure (see above). The registered\n * `build()` runs server-side only: it validates the routing ids against the\n * product's access control, then mints its own URLs from server configuration\n * (`buildHttpMcpServer` in ../tools). A client can therefore never inject an\n * arbitrary MCP url, header, or credential into the agent profile: the\n * overlay's `mcp` values are typed as {@link SurfaceMcpServer} (= the\n * server-built `AppToolMcpServer` entry shape), and only build() constructs\n * them.\n *\n * The credential itself never appears in the overlay. Since the tagged-config\n * contract (`agent-interface` 0.38.0) the `Authorization` header is a\n * `secret-ref` naming a box-environment variable, which the sandbox resolves\n * privately; `build()` supplies that key NAME from server configuration and the\n * product writes the value through `SandboxRuntimeConfig.env` at creation or\n * refreshes it through `SandboxRuntimeConfig.runtimeEnv` before retained-box\n * bootstrap. The value remains workspace-scoped because every member and turn\n * shares the box; runtime renewal does not create a per-user secret channel.\n */\n\nimport type { AppToolMcpServer } from '../tools/mcp'\n\n/** Sandbox permission posture values, ranked deny > ask > allow for merging. */\nexport type SurfacePermissionValue = 'allow' | 'ask' | 'deny'\n\n/** The only MCP entry shape an overlay may carry: the server-built bridge\n * entry from ../tools/mcp (transport, url, headers, and capability token all\n * assembled server-side). The alias exists so overlay authors reach for the\n * builders in ../tools rather than hand-rolling `{ url: ctx.url }` shapes\n * that would let request data become a dialable endpoint. */\nexport type SurfaceMcpServer = AppToolMcpServer\n\n/** What one surface contributes to the agent profile for a single turn. */\nexport interface SurfaceOverlay {\n /** MCP servers to mount for this turn, keyed by tool-routing name. Names\n * must not collide with the base profile's — see {@link mergeSurfaceOverlay}. */\n mcp?: Record<string, SurfaceMcpServer>\n /** Appended to the base system-prompt addendum with a blank-line separator. */\n promptAddendum?: string\n /** Per-key posture the surface wants for its turns. Merging is monotone\n * fail-closed: the stricter of base/overlay wins, so a surface can tighten\n * the workspace posture but never relax it. */\n permissions?: Record<string, SurfacePermissionValue>\n}\n\n/**\n * One registered surface kind. `TCtx` is the shape build() expects — a CLAIM\n * about the client payload, not a guarantee: the registry hands build() the\n * request's `ctx` unvalidated, so build() must treat every field as an\n * untrusted id (resolve it through access control that throws on a bad or\n * foreign id) before minting anything from it.\n */\nexport interface SurfaceKindDefinition<TCtx> {\n kind: string\n build: (ctx: TCtx) => SurfaceOverlay | Promise<SurfaceOverlay>\n}\n\n/** The variance-erased form a registry accepts (`build` is contravariant in\n * `TCtx`, so every concrete definition is assignable to this). */\nexport type AnySurfaceKind = SurfaceKindDefinition<never>\n\n/**\n * Declare one surface kind. The `kind` string is the client-visible routing\n * key (e.g. `'sequences'`); `build` is the server-side factory that turns a\n * validated ctx into the overlay for one turn.\n */\nexport function defineSurfaceKind<TCtx>(opts: {\n kind: string\n build: (ctx: TCtx) => SurfaceOverlay | Promise<SurfaceOverlay>\n}): SurfaceKindDefinition<TCtx> {\n if (typeof opts.kind !== 'string' || opts.kind.length === 0 || /\\s/.test(opts.kind)) {\n throw new Error(`surface kind must be a non-empty string without whitespace (got ${JSON.stringify(opts.kind)})`)\n }\n if (typeof opts.build !== 'function') {\n throw new Error(`surface kind '${opts.kind}' requires a build function`)\n }\n return { kind: opts.kind, build: opts.build }\n}\n\n/** Resolve and build the overlay for a given surface kind within a turn context */\nexport interface SurfaceRegistry {\n /** Build the overlay for one turn. Throws on an unknown kind — an unknown\n * surface is a routing bug (client and server registries drifted), and\n * silently returning an empty overlay would strip the surface's tools from\n * the turn with no signal anywhere. Build errors propagate unwrapped. */\n resolve(kind: string, ctx: unknown): Promise<SurfaceOverlay>\n}\n\n/**\n * Assemble the product's surface registry from its registered kinds. Duplicate\n * kinds throw at construction: two builders behind one routing key would make\n * the mounted toolset depend on registration order.\n */\nexport function createSurfaceRegistry(kinds: readonly AnySurfaceKind[]): SurfaceRegistry {\n const byKind = new Map<string, AnySurfaceKind>()\n for (const definition of kinds) {\n if (byKind.has(definition.kind)) {\n throw new Error(`duplicate surface kind '${definition.kind}' — each kind must be registered exactly once`)\n }\n byKind.set(definition.kind, definition)\n }\n\n return {\n async resolve(kind, ctx) {\n const definition = byKind.get(kind)\n if (!definition) {\n const known = [...byKind.keys()].join(', ') || '(none)'\n throw new Error(\n `unknown surface kind '${kind}' — registered kinds: ${known}. ` +\n 'An unknown surface is a routing bug: register the kind via defineSurfaceKind before clients can reference it.',\n )\n }\n // The trust boundary where static ctx typing ends: the request payload is\n // handed to build() as-is, and build() validates it (see SurfaceKindDefinition).\n const overlay = await definition.build(ctx as never)\n assertSurfaceOverlay(overlay, `surface kind '${kind}'`)\n return overlay\n },\n }\n}\n\n/** Base-profile slice the merge reads/writes. Real callers pass their full\n * profile object; every field outside this slice passes through untouched. */\nexport interface SurfaceMergeBase {\n mcp?: Record<string, unknown>\n systemPromptAddendum?: string\n permissions?: Record<string, SurfacePermissionValue>\n}\n\nconst PERMISSION_SEVERITY: Record<SurfacePermissionValue, number> = { allow: 0, ask: 1, deny: 2 }\n\n/**\n * Merge one surface overlay into a base profile, returning a new object\n * (the base is never mutated; untouched nested records are shared by\n * reference).\n *\n * - `mcp`: overlay servers are added under their own names. A name already\n * present on the base THROWS — a collision is two servers claiming one\n * routing name, and renaming either silently would corrupt tool routing for\n * whichever caller expected the original binding.\n * - `systemPromptAddendum`: the overlay's `promptAddendum` appends after a\n * blank-line separator (no separator when the base has no addendum).\n * - `permissions`: per key the STRICTER value wins (deny > ask > allow). A\n * surface can tighten the base posture for its turns; a base 'deny' survives\n * any overlay.\n */\nexport function mergeSurfaceOverlay<TBase extends SurfaceMergeBase>(\n base: TBase,\n overlay: SurfaceOverlay,\n): TBase & SurfaceMergeBase {\n assertSurfaceOverlay(overlay, 'surface overlay')\n const merged: SurfaceMergeBase = { ...base }\n\n if (overlay.mcp && Object.keys(overlay.mcp).length > 0) {\n const baseMcp = base.mcp ?? {}\n const collisions = Object.keys(overlay.mcp).filter((name) => name in baseMcp)\n if (collisions.length > 0) {\n throw new Error(\n `surface overlay MCP name collision: ${collisions.map((n) => `'${n}'`).join(', ')} already exist on the base profile. ` +\n 'Two servers cannot claim one name — give the surface server a distinct name.',\n )\n }\n merged.mcp = { ...baseMcp, ...overlay.mcp }\n }\n\n if (overlay.promptAddendum !== undefined) {\n merged.systemPromptAddendum = base.systemPromptAddendum\n ? `${base.systemPromptAddendum}\\n\\n${overlay.promptAddendum}`\n : overlay.promptAddendum\n }\n\n if (overlay.permissions && Object.keys(overlay.permissions).length > 0) {\n const permissions: Record<string, SurfacePermissionValue> = { ...(base.permissions ?? {}) }\n for (const [key, value] of Object.entries(overlay.permissions)) {\n const existing = permissions[key]\n permissions[key] =\n existing === undefined || PERMISSION_SEVERITY[value] > PERMISSION_SEVERITY[existing] ? value : existing\n }\n merged.permissions = permissions\n }\n\n // merged began as a shallow copy of base; only the three slice fields were\n // replaced, so the intersection type is the true shape.\n return merged as TBase & SurfaceMergeBase\n}\n\n/** Reject overlays a build() (or hand-rolled caller) malformed, with the exact\n * field named: a relative MCP url, a blank addendum, or an off-vocabulary\n * permission would otherwise surface only as an opaque sandbox failure. */\nfunction assertSurfaceOverlay(overlay: SurfaceOverlay, label: string): void {\n if (overlay.promptAddendum !== undefined) {\n if (typeof overlay.promptAddendum !== 'string' || overlay.promptAddendum.trim().length === 0) {\n throw new Error(`${label}: promptAddendum must be a non-blank string when provided`)\n }\n }\n if (overlay.mcp !== undefined) {\n for (const [name, server] of Object.entries(overlay.mcp)) {\n if (name.trim().length === 0) throw new Error(`${label}: MCP server names must be non-empty`)\n if (server.transport !== 'http') {\n throw new Error(`${label}: MCP server '${name}' must use transport 'http' (got ${JSON.stringify((server as { transport?: unknown }).transport)})`)\n }\n let parsed: URL\n try {\n parsed = new URL(server.url)\n } catch {\n throw new Error(`${label}: MCP server '${name}' url must be an absolute URL (got ${JSON.stringify(server.url)})`)\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new Error(`${label}: MCP server '${name}' url must be http(s) (got ${JSON.stringify(server.url)})`)\n }\n }\n }\n if (overlay.permissions !== undefined) {\n for (const [key, value] of Object.entries(overlay.permissions)) {\n if (!(value in PERMISSION_SEVERITY)) {\n throw new Error(`${label}: permission '${key}' must be 'allow' | 'ask' | 'deny' (got ${JSON.stringify(value)})`)\n }\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAUA;AAAA,EACE;AAAA,OAGK;AAkBA,SAAS,wBAAwB,QAAoD;AAC1F,QAAM,SAAS,4BAA4B,MAAM;AACjD,SAAO;AAAA,IACL,MAAM,eAAe,MAAM;AACzB,aAAO,EAAE,GAAG,MAAM,cAAc,MAAM,OAAO,QAAQ,KAAK,YAAY,EAAE;AAAA,IAC1E;AAAA,IACA,SAAS,MAAM,OAAO,QAAQ,EAAE,OAAO,KAAK,CAAC;AAAA,IAC7C,SAAS,OAAO;AAAA,EAClB;AACF;;;ACwCO,SAAS,kBAAwB,MAGR;AAC9B,MAAI,OAAO,KAAK,SAAS,YAAY,KAAK,KAAK,WAAW,KAAK,KAAK,KAAK,KAAK,IAAI,GAAG;AACnF,UAAM,IAAI,MAAM,mEAAmE,KAAK,UAAU,KAAK,IAAI,CAAC,GAAG;AAAA,EACjH;AACA,MAAI,OAAO,KAAK,UAAU,YAAY;AACpC,UAAM,IAAI,MAAM,iBAAiB,KAAK,IAAI,6BAA6B;AAAA,EACzE;AACA,SAAO,EAAE,MAAM,KAAK,MAAM,OAAO,KAAK,MAAM;AAC9C;AAgBO,SAAS,sBAAsB,OAAmD;AACvF,QAAM,SAAS,oBAAI,IAA4B;AAC/C,aAAW,cAAc,OAAO;AAC9B,QAAI,OAAO,IAAI,WAAW,IAAI,GAAG;AAC/B,YAAM,IAAI,MAAM,2BAA2B,WAAW,IAAI,oDAA+C;AAAA,IAC3G;AACA,WAAO,IAAI,WAAW,MAAM,UAAU;AAAA,EACxC;AAEA,SAAO;AAAA,IACL,MAAM,QAAQ,MAAM,KAAK;AACvB,YAAM,aAAa,OAAO,IAAI,IAAI;AAClC,UAAI,CAAC,YAAY;AACf,cAAM,QAAQ,CAAC,GAAG,OAAO,KAAK,CAAC,EAAE,KAAK,IAAI,KAAK;AAC/C,cAAM,IAAI;AAAA,UACR,yBAAyB,IAAI,8BAAyB,KAAK;AAAA,QAE7D;AAAA,MACF;AAGA,YAAM,UAAU,MAAM,WAAW,MAAM,GAAY;AACnD,2BAAqB,SAAS,iBAAiB,IAAI,GAAG;AACtD,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAUA,IAAM,sBAA8D,EAAE,OAAO,GAAG,KAAK,GAAG,MAAM,EAAE;AAiBzF,SAAS,oBACd,MACA,SAC0B;AAC1B,uBAAqB,SAAS,iBAAiB;AAC/C,QAAM,SAA2B,EAAE,GAAG,KAAK;AAE3C,MAAI,QAAQ,OAAO,OAAO,KAAK,QAAQ,GAAG,EAAE,SAAS,GAAG;AACtD,UAAM,UAAU,KAAK,OAAO,CAAC;AAC7B,UAAM,aAAa,OAAO,KAAK,QAAQ,GAAG,EAAE,OAAO,CAAC,SAAS,QAAQ,OAAO;AAC5E,QAAI,WAAW,SAAS,GAAG;AACzB,YAAM,IAAI;AAAA,QACR,uCAAuC,WAAW,IAAI,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,KAAK,IAAI,CAAC;AAAA,MAEnF;AAAA,IACF;AACA,WAAO,MAAM,EAAE,GAAG,SAAS,GAAG,QAAQ,IAAI;AAAA,EAC5C;AAEA,MAAI,QAAQ,mBAAmB,QAAW;AACxC,WAAO,uBAAuB,KAAK,uBAC/B,GAAG,KAAK,oBAAoB;AAAA;AAAA,EAAO,QAAQ,cAAc,KACzD,QAAQ;AAAA,EACd;AAEA,MAAI,QAAQ,eAAe,OAAO,KAAK,QAAQ,WAAW,EAAE,SAAS,GAAG;AACtE,UAAM,cAAsD,EAAE,GAAI,KAAK,eAAe,CAAC,EAAG;AAC1F,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,QAAQ,WAAW,GAAG;AAC9D,YAAM,WAAW,YAAY,GAAG;AAChC,kBAAY,GAAG,IACb,aAAa,UAAa,oBAAoB,KAAK,IAAI,oBAAoB,QAAQ,IAAI,QAAQ;AAAA,IACnG;AACA,WAAO,cAAc;AAAA,EACvB;AAIA,SAAO;AACT;AAKA,SAAS,qBAAqB,SAAyB,OAAqB;AAC1E,MAAI,QAAQ,mBAAmB,QAAW;AACxC,QAAI,OAAO,QAAQ,mBAAmB,YAAY,QAAQ,eAAe,KAAK,EAAE,WAAW,GAAG;AAC5F,YAAM,IAAI,MAAM,GAAG,KAAK,2DAA2D;AAAA,IACrF;AAAA,EACF;AACA,MAAI,QAAQ,QAAQ,QAAW;AAC7B,eAAW,CAAC,MAAM,MAAM,KAAK,OAAO,QAAQ,QAAQ,GAAG,GAAG;AACxD,UAAI,KAAK,KAAK,EAAE,WAAW,EAAG,OAAM,IAAI,MAAM,GAAG,KAAK,sCAAsC;AAC5F,UAAI,OAAO,cAAc,QAAQ;AAC/B,cAAM,IAAI,MAAM,GAAG,KAAK,iBAAiB,IAAI,oCAAoC,KAAK,UAAW,OAAmC,SAAS,CAAC,GAAG;AAAA,MACnJ;AACA,UAAI;AACJ,UAAI;AACF,iBAAS,IAAI,IAAI,OAAO,GAAG;AAAA,MAC7B,QAAQ;AACN,cAAM,IAAI,MAAM,GAAG,KAAK,iBAAiB,IAAI,sCAAsC,KAAK,UAAU,OAAO,GAAG,CAAC,GAAG;AAAA,MAClH;AACA,UAAI,OAAO,aAAa,WAAW,OAAO,aAAa,UAAU;AAC/D,cAAM,IAAI,MAAM,GAAG,KAAK,iBAAiB,IAAI,8BAA8B,KAAK,UAAU,OAAO,GAAG,CAAC,GAAG;AAAA,MAC1G;AAAA,IACF;AAAA,EACF;AACA,MAAI,QAAQ,gBAAgB,QAAW;AACrC,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,QAAQ,WAAW,GAAG;AAC9D,UAAI,EAAE,SAAS,sBAAsB;AACnC,cAAM,IAAI,MAAM,GAAG,KAAK,iBAAiB,GAAG,2CAA2C,KAAK,UAAU,KAAK,CAAC,GAAG;AAAA,MACjH;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/runtime/certified-delivery.ts","../../src/runtime/surface-profile.ts"],"sourcesContent":["/**\n * Application profile composition using Runtime's certified-guidance source.\n *\n * The shell adapts the prompt source to the application's resolved profile;\n * Runtime owns pulling, cache lifetime, concurrent refresh and retained guidance.\n * Only prompt-compatible guidance is folded here. Tool definitions, execution\n * permissions, MCP servers and files are not installed from certified material.\n * The complete profile remains available through `current()` for inspection and\n * the product's separately authorized materialization path.\n */\nimport {\n createCertifiedPromptSource,\n type CertifiedProfile,\n type CertifiedPromptSourceOptions,\n} from '@tangle-network/agent-runtime/intelligence'\nimport type { ResolvedAgentProfile } from './agent'\n\n/** Pull coordinates, refresh cadence and timeout owned by the shared source. */\nexport type CertifiedDeliveryConfig = CertifiedPromptSourceOptions\n\n/** Adapt certified prompt guidance without granting execution authority. */\nexport interface CertifiedDelivery {\n /** Refresh on the source's cadence and compose the current prompt guidance.\n * Other resolved profile fields pass through unchanged. */\n composeProfile(base: ResolvedAgentProfile): Promise<ResolvedAgentProfile>\n /** Force a pull now, coalescing with an in-flight pull. Best-effort; not a\n * post-activation revision barrier or a permission-revocation protocol. */\n refresh(): Promise<void>\n /** Last successfully pulled profile, or null before a successful pull. */\n current(): CertifiedProfile | null\n}\n\nexport function createCertifiedDelivery(config: CertifiedDeliveryConfig): CertifiedDelivery {\n const source = createCertifiedPromptSource(config)\n return {\n async composeProfile(base) {\n return { ...base, systemPrompt: await source.compose(base.systemPrompt) }\n },\n refresh: () => source.refresh({ force: true }),\n current: source.current,\n }\n}\n","/**\n * Surface-scoped profile overlay — the seam letting any product page (a\n * sequence editor, a brief composer, a dataset view) add MCP servers, a prompt\n * addendum, and permission tightening to the workspace agent profile for turns\n * initiated FROM that surface, without the chat orchestrator knowing any\n * surface's specifics. The orchestrator resolves `(kind, ctx)` through a\n * registry the REQUEST HANDLER constructs per request (construction is a Map\n * build — cheap) and merges the result into the base profile it was about to\n * send to the sandbox. Per-request construction is the trust mechanism, not an\n * optimization target: each `build()` closes over server-trusted request state\n * (env bindings, secrets, the AUTHENTICATED user/workspace), which on Workers\n * exists only per request — a startup-built registry would force identity\n * through the untrusted client `ctx`.\n *\n * SECURITY INVARIANT: the surface `kind` and the ids inside `ctx` arrive on\n * the client request and are pure ROUTING data — never trusted content, never\n * identity. Identity comes from the closure (see above). The registered\n * `build()` runs server-side only: it validates the routing ids against the\n * product's access control, then mints its own URLs from server configuration\n * (`buildHttpMcpServer` in ../tools). A client can therefore never inject an\n * arbitrary MCP url, header, or credential into the agent profile: the\n * overlay's `mcp` values are typed as {@link SurfaceMcpServer} (= the\n * server-built `AppToolMcpServer` entry shape), and only build() constructs\n * them.\n *\n * The credential itself never appears in the overlay. Since the tagged-config\n * contract (`agent-interface` 0.38.0) the `Authorization` header is a\n * `secret-ref` naming a box-environment variable, which the sandbox resolves\n * privately; `build()` supplies that key NAME from server configuration and the\n * product writes the value through `SandboxRuntimeConfig.env` at creation or\n * refreshes it through `SandboxRuntimeConfig.runtimeEnv` before retained-box\n * bootstrap. The value remains workspace-scoped because every member and turn\n * shares the box; runtime renewal does not create a per-user secret channel.\n */\n\nimport type { AppToolMcpServer } from '../tools/mcp'\n\n/** Sandbox permission posture values, ranked deny > ask > allow for merging. */\nexport type SurfacePermissionValue = 'allow' | 'ask' | 'deny'\n\n/** The only MCP entry shape an overlay may carry: the server-built bridge\n * entry from ../tools/mcp (transport, url, headers, and capability token all\n * assembled server-side). The alias exists so overlay authors reach for the\n * builders in ../tools rather than hand-rolling `{ url: ctx.url }` shapes\n * that would let request data become a dialable endpoint. */\nexport type SurfaceMcpServer = AppToolMcpServer\n\n/** What one surface contributes to the agent profile for a single turn. */\nexport interface SurfaceOverlay {\n /** MCP servers to mount for this turn, keyed by tool-routing name. Names\n * must not collide with the base profile's — see {@link mergeSurfaceOverlay}. */\n mcp?: Record<string, SurfaceMcpServer>\n /** Appended to the base system-prompt addendum with a blank-line separator. */\n promptAddendum?: string\n /** Per-key posture the surface wants for its turns. Merging is monotone\n * fail-closed: the stricter of base/overlay wins, so a surface can tighten\n * the workspace posture but never relax it. */\n permissions?: Record<string, SurfacePermissionValue>\n}\n\n/**\n * One registered surface kind. `TCtx` is the shape build() expects — a CLAIM\n * about the client payload, not a guarantee: the registry hands build() the\n * request's `ctx` unvalidated, so build() must treat every field as an\n * untrusted id (resolve it through access control that throws on a bad or\n * foreign id) before minting anything from it.\n */\nexport interface SurfaceKindDefinition<TCtx> {\n kind: string\n build: (ctx: TCtx) => SurfaceOverlay | Promise<SurfaceOverlay>\n}\n\n/** The variance-erased form a registry accepts (`build` is contravariant in\n * `TCtx`, so every concrete definition is assignable to this). */\nexport type AnySurfaceKind = SurfaceKindDefinition<never>\n\n/**\n * Declare one surface kind. The `kind` string is the client-visible routing\n * key (e.g. `'sequences'`); `build` is the server-side factory that turns a\n * validated ctx into the overlay for one turn.\n */\nexport function defineSurfaceKind<TCtx>(opts: {\n kind: string\n build: (ctx: TCtx) => SurfaceOverlay | Promise<SurfaceOverlay>\n}): SurfaceKindDefinition<TCtx> {\n if (typeof opts.kind !== 'string' || opts.kind.length === 0 || /\\s/.test(opts.kind)) {\n throw new Error(`surface kind must be a non-empty string without whitespace (got ${JSON.stringify(opts.kind)})`)\n }\n if (typeof opts.build !== 'function') {\n throw new Error(`surface kind '${opts.kind}' requires a build function`)\n }\n return { kind: opts.kind, build: opts.build }\n}\n\n/** Resolve and build the overlay for a given surface kind within a turn context */\nexport interface SurfaceRegistry {\n /** Build the overlay for one turn. Throws on an unknown kind — an unknown\n * surface is a routing bug (client and server registries drifted), and\n * silently returning an empty overlay would strip the surface's tools from\n * the turn with no signal anywhere. Build errors propagate unwrapped. */\n resolve(kind: string, ctx: unknown): Promise<SurfaceOverlay>\n}\n\n/**\n * Assemble the product's surface registry from its registered kinds. Duplicate\n * kinds throw at construction: two builders behind one routing key would make\n * the mounted toolset depend on registration order.\n */\nexport function createSurfaceRegistry(kinds: readonly AnySurfaceKind[]): SurfaceRegistry {\n const byKind = new Map<string, AnySurfaceKind>()\n for (const definition of kinds) {\n if (byKind.has(definition.kind)) {\n throw new Error(`duplicate surface kind '${definition.kind}' — each kind must be registered exactly once`)\n }\n byKind.set(definition.kind, definition)\n }\n\n return {\n async resolve(kind, ctx) {\n const definition = byKind.get(kind)\n if (!definition) {\n const known = [...byKind.keys()].join(', ') || '(none)'\n throw new Error(\n `unknown surface kind '${kind}' — registered kinds: ${known}. ` +\n 'An unknown surface is a routing bug: register the kind via defineSurfaceKind before clients can reference it.',\n )\n }\n // The trust boundary where static ctx typing ends: the request payload is\n // handed to build() as-is, and build() validates it (see SurfaceKindDefinition).\n const overlay = await definition.build(ctx as never)\n assertSurfaceOverlay(overlay, `surface kind '${kind}'`)\n return overlay\n },\n }\n}\n\n/** Base-profile slice the merge reads/writes. Real callers pass their full\n * profile object; every field outside this slice passes through untouched. */\nexport interface SurfaceMergeBase {\n mcp?: Record<string, unknown>\n systemPromptAddendum?: string\n permissions?: Record<string, SurfacePermissionValue>\n}\n\nconst PERMISSION_SEVERITY: Record<SurfacePermissionValue, number> = { allow: 0, ask: 1, deny: 2 }\n\n/**\n * Merge one surface overlay into a base profile, returning a new object\n * (the base is never mutated; untouched nested records are shared by\n * reference).\n *\n * - `mcp`: overlay servers are added under their own names. A name already\n * present on the base THROWS — a collision is two servers claiming one\n * routing name, and renaming either silently would corrupt tool routing for\n * whichever caller expected the original binding.\n * - `systemPromptAddendum`: the overlay's `promptAddendum` appends after a\n * blank-line separator (no separator when the base has no addendum).\n * - `permissions`: per key the STRICTER value wins (deny > ask > allow). A\n * surface can tighten the base posture for its turns; a base 'deny' survives\n * any overlay.\n */\nexport function mergeSurfaceOverlay<TBase extends SurfaceMergeBase>(\n base: TBase,\n overlay: SurfaceOverlay,\n): TBase & SurfaceMergeBase {\n assertSurfaceOverlay(overlay, 'surface overlay')\n const merged: SurfaceMergeBase = { ...base }\n\n if (overlay.mcp && Object.keys(overlay.mcp).length > 0) {\n const baseMcp = base.mcp ?? {}\n const collisions = Object.keys(overlay.mcp).filter((name) => name in baseMcp)\n if (collisions.length > 0) {\n throw new Error(\n `surface overlay MCP name collision: ${collisions.map((n) => `'${n}'`).join(', ')} already exist on the base profile. ` +\n 'Two servers cannot claim one name — give the surface server a distinct name.',\n )\n }\n merged.mcp = { ...baseMcp, ...overlay.mcp }\n }\n\n if (overlay.promptAddendum !== undefined) {\n merged.systemPromptAddendum = base.systemPromptAddendum\n ? `${base.systemPromptAddendum}\\n\\n${overlay.promptAddendum}`\n : overlay.promptAddendum\n }\n\n if (overlay.permissions && Object.keys(overlay.permissions).length > 0) {\n const permissions: Record<string, SurfacePermissionValue> = { ...(base.permissions ?? {}) }\n for (const [key, value] of Object.entries(overlay.permissions)) {\n const existing = permissions[key]\n permissions[key] =\n existing === undefined || PERMISSION_SEVERITY[value] > PERMISSION_SEVERITY[existing] ? value : existing\n }\n merged.permissions = permissions\n }\n\n // merged began as a shallow copy of base; only the three slice fields were\n // replaced, so the intersection type is the true shape.\n return merged as TBase & SurfaceMergeBase\n}\n\n/** Reject overlays a build() (or hand-rolled caller) malformed, with the exact\n * field named: a relative MCP url, a blank addendum, or an off-vocabulary\n * permission would otherwise surface only as an opaque sandbox failure. */\nfunction assertSurfaceOverlay(overlay: SurfaceOverlay, label: string): void {\n if (overlay.promptAddendum !== undefined) {\n if (typeof overlay.promptAddendum !== 'string' || overlay.promptAddendum.trim().length === 0) {\n throw new Error(`${label}: promptAddendum must be a non-blank string when provided`)\n }\n }\n if (overlay.mcp !== undefined) {\n for (const [name, server] of Object.entries(overlay.mcp)) {\n if (name.trim().length === 0) throw new Error(`${label}: MCP server names must be non-empty`)\n if (server.transport !== 'http') {\n throw new Error(`${label}: MCP server '${name}' must use transport 'http' (got ${JSON.stringify((server as { transport?: unknown }).transport)})`)\n }\n let parsed: URL\n try {\n parsed = new URL(server.url)\n } catch {\n throw new Error(`${label}: MCP server '${name}' url must be an absolute URL (got ${JSON.stringify(server.url)})`)\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new Error(`${label}: MCP server '${name}' url must be http(s) (got ${JSON.stringify(server.url)})`)\n }\n }\n }\n if (overlay.permissions !== undefined) {\n for (const [key, value] of Object.entries(overlay.permissions)) {\n if (!(value in PERMISSION_SEVERITY)) {\n throw new Error(`${label}: permission '${key}' must be 'allow' | 'ask' | 'deny' (got ${JSON.stringify(value)})`)\n }\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -11,11 +11,12 @@ import {
11
11
  turnKnowledgeInstructions,
12
12
  workspaceKnowledgeForTurn,
13
13
  workspaceTurnCheckpoints
14
- } from "../chunk-VQSDYGQ6.js";
15
- import "../chunk-7XQIY2LU.js";
14
+ } from "../chunk-J4BZ6NPT.js";
15
+ import "../chunk-T7CWWC3F.js";
16
16
  import "../chunk-D6UJEOZQ.js";
17
17
  import "../chunk-AMUXRAUO.js";
18
- import "../chunk-ZRUR3DRU.js";
18
+ import "../chunk-HA5USG2Z.js";
19
+ import "../chunk-PIUYJTTK.js";
19
20
  export {
20
21
  DURABLE_HOP_BUDGET_MS,
21
22
  asksForLanguage,
@@ -98,7 +98,7 @@ import {
98
98
  warmBinOf,
99
99
  warmWeekdayOf,
100
100
  writeProfileFilesToBox
101
- } from "../chunk-ZHJ2TDWA.js";
101
+ } from "../chunk-7O6UQJJA.js";
102
102
  import {
103
103
  EGRESS_PROXY_RECOVERY_PHASE,
104
104
  EGRESS_PROXY_RECOVERY_REQUIRED,
@@ -147,8 +147,9 @@ import "../chunk-S5SRJJQG.js";
147
147
  import "../chunk-IJAXOQJU.js";
148
148
  import "../chunk-UGWQLQDS.js";
149
149
  import "../chunk-E65YV7VV.js";
150
- import "../chunk-7XQIY2LU.js";
150
+ import "../chunk-T7CWWC3F.js";
151
151
  import "../chunk-D6UJEOZQ.js";
152
+ import "../chunk-PIUYJTTK.js";
152
153
  export {
153
154
  DEFAULT_ESCALATION_TOOLS,
154
155
  DEFAULT_FOREGROUND_PROVISION_CLAIM_TTL_SECONDS,
@@ -1,27 +1,40 @@
1
1
  import {
2
2
  nextCallIndex,
3
- parseServerTiming,
4
- serverTimingAttrs,
5
3
  tracedModelFetch
6
- } from "../chunk-7XQIY2LU.js";
4
+ } from "../chunk-T7CWWC3F.js";
7
5
  import {
8
6
  TURN_SPAN_KINDS,
9
7
  createTurnTrace,
10
8
  isTraceId,
11
9
  summarizeTurnSpans,
12
10
  turnTraceId
13
- } from "../chunk-ZRUR3DRU.js";
11
+ } from "../chunk-HA5USG2Z.js";
12
+ import {
13
+ formatServerTiming,
14
+ parseServerTiming,
15
+ serverTimingAttrs
16
+ } from "../chunk-PIUYJTTK.js";
14
17
 
15
18
  // src/turn-trace/otel.ts
16
- function attributes(record) {
19
+ function attributes(record, doubles = /* @__PURE__ */ new Set()) {
17
20
  return Object.entries(record).flatMap(([key, value]) => {
18
21
  if (typeof value === "number" && !Number.isFinite(value)) return [];
19
22
  return [{
20
23
  key,
21
- value: typeof value === "number" ? Number.isInteger(value) ? { intValue: value.toString() } : { doubleValue: value } : typeof value === "boolean" ? { boolValue: value } : { stringValue: value }
24
+ value: typeof value === "number" ? Number.isInteger(value) && !doubles.has(key) ? { intValue: value.toString() } : { doubleValue: value } : typeof value === "boolean" ? { boolValue: value } : { stringValue: value }
22
25
  }];
23
26
  });
24
27
  }
28
+ var ROOT_MEASUREMENTS = /* @__PURE__ */ new Set([
29
+ "tangle.duration_ms",
30
+ "tangle.turn.wall_ms",
31
+ "tangle.turn.covered_ms",
32
+ "tangle.turn.coverage_pct",
33
+ "tangle.turn.uncovered_ms",
34
+ "tangle.turn.largest_gap_ms",
35
+ "tangle.turn.first_text_ms",
36
+ ...TURN_SPAN_KINDS.map((kind) => `tangle.turn.${kind}_ms`)
37
+ ]);
25
38
  function otlpTraceId(traceId) {
26
39
  if (/^[0-9a-f]{32}$/.test(traceId)) return traceId;
27
40
  const dashless = traceId.replace(/-/g, "");
@@ -70,7 +83,7 @@ function buildTurnTraceOtelSpans(snapshot, labels = {}) {
70
83
  kind: 1,
71
84
  startTimeUnixNano: msToNs(snapshot.startedAt),
72
85
  endTimeUnixNano: msToNs(snapshot.endedAt),
73
- attributes: attributes(rootAttrs),
86
+ attributes: attributes(rootAttrs, ROOT_MEASUREMENTS),
74
87
  status: snapshot.status === "ok" ? { code: 1 } : { code: 2, ...snapshot.error ? { message: snapshot.error } : {} }
75
88
  }];
76
89
  for (const span of snapshot.spans) {
@@ -136,6 +149,7 @@ export {
136
149
  buildTurnTraceOtelSpans,
137
150
  createIntelligenceTurnTraceSink,
138
151
  createTurnTrace,
152
+ formatServerTiming,
139
153
  isTraceId,
140
154
  nextCallIndex,
141
155
  otlpTraceId,
@@ -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 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.firstTextMs === undefined ? {} : { 'tangle.turn.first_text_ms': summary.firstTextMs }),\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
+ {"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\n/** `doubles` names measurements that stay `doubleValue` when they land on a whole number, so a reader sees one type per key. */\nfunction attributes(record: Record<string, string | number | boolean>, doubles: ReadonlySet<string> = new Set()): 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) && !doubles.has(key) ? { intValue: value.toString() } : { doubleValue: value })\n : typeof value === 'boolean' ? { boolValue: value } : { stringValue: value },\n }]\n })\n}\n\n/** The root span's measured numbers: one decimal, so each is always a `doubleValue`. */\nconst ROOT_MEASUREMENTS: ReadonlySet<string> = new Set([\n 'tangle.duration_ms',\n 'tangle.turn.wall_ms',\n 'tangle.turn.covered_ms',\n 'tangle.turn.coverage_pct',\n 'tangle.turn.uncovered_ms',\n 'tangle.turn.largest_gap_ms',\n 'tangle.turn.first_text_ms',\n ...TURN_SPAN_KINDS.map((kind) => `tangle.turn.${kind}_ms`),\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.firstTextMs === undefined ? {} : { 'tangle.turn.first_text_ms': summary.firstTextMs }),\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, ROOT_MEASUREMENTS),\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":[]}
@@ -100,6 +100,8 @@ export interface TurnTrace {
100
100
  markFirstText(at?: number): void;
101
101
  /** Read the current numbers without ending the trace. */
102
102
  summary(now?: number): TurnTraceSummary;
103
+ /** Read the spans recorded so far without ending the trace; an open span reads as ending at `now`. */
104
+ spans(now?: number): TurnSpanRecord[];
103
105
  /** End the trace: open spans close at `endedAt`, and later spans are refused. A second call returns the first result. */
104
106
  finish(outcome?: {
105
107
  error?: unknown;
@@ -13,3 +13,5 @@ export interface ServerTimingEntry {
13
13
  export declare function parseServerTiming(header: string | null | undefined): ServerTimingEntry[];
14
14
  /** Flat span attributes for the entries: `server_timing.<name>_ms` for a duration, `server_timing.<name>` for a `desc`. */
15
15
  export declare function serverTimingAttrs(entries: readonly ServerTimingEntry[]): Record<string, string | number>;
16
+ /** Format entries as a `Server-Timing` header value, the inverse of {@link parseServerTiming}. Invalid names are skipped. */
17
+ export declare function formatServerTiming(entries: readonly ServerTimingEntry[]): string;