@nylorun/runtime 0.23.0-beta → 0.24.0-beta

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 (252) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/README.md +79 -28
  3. package/dist/adapters/execution/restate.js +1 -1
  4. package/dist/adapters/replication/pgoutput.d.ts +2 -0
  5. package/dist/adapters/replication/pgoutput.js +6 -3
  6. package/dist/adapters/sandbox/local.d.ts +9 -1
  7. package/dist/adapters/sandbox/local.js +16 -4
  8. package/dist/adapters/sandbox/virtual.js +4 -1
  9. package/dist/api/a2a/endpoint.js +5 -1
  10. package/dist/api/a2a/routes.d.ts +2 -0
  11. package/dist/api/a2a/routes.js +2 -2
  12. package/dist/api/a2a/tasks.js +2 -1
  13. package/dist/api/ag-ui/endpoint.js +4 -2
  14. package/dist/api/ag-ui/history.js +4 -1
  15. package/dist/api/ag-ui/routes.d.ts +4 -1
  16. package/dist/api/ag-ui/routes.js +11 -10
  17. package/dist/api/ag-ui/translate.js +3 -2
  18. package/dist/api/components.d.ts +4953 -1204
  19. package/dist/api/components.js +25 -16
  20. package/dist/api/gate/keys.js +7 -1
  21. package/dist/api/gate/routes.d.ts +6 -0
  22. package/dist/api/gate/routes.js +39 -3
  23. package/dist/api/http/app.js +4 -4
  24. package/dist/api/http/define.d.ts +6 -0
  25. package/dist/api/http/define.js +13 -1
  26. package/dist/api/http/respond.js +1 -1
  27. package/dist/api/http/routes/artifacts.js +35 -8
  28. package/dist/api/http/routes/coverage.d.ts +9 -0
  29. package/dist/api/http/routes/coverage.js +34 -0
  30. package/dist/api/http/routes/environments.d.ts +11 -0
  31. package/dist/api/http/routes/environments.js +283 -0
  32. package/dist/api/http/routes/mcp.d.ts +1 -1
  33. package/dist/api/http/routes/mcp.js +2 -2
  34. package/dist/api/http/routes/me.js +1 -1
  35. package/dist/api/http/routes/reads.d.ts +45 -9
  36. package/dist/api/http/routes/reads.js +54 -5
  37. package/dist/api/http/routes/sessions.js +11 -6
  38. package/dist/api/http/routes/tenant.js +32 -10
  39. package/dist/api/openapi.js +31 -26
  40. package/dist/artifacts/disk.d.ts +59 -0
  41. package/dist/artifacts/disk.js +563 -0
  42. package/dist/artifacts/export.d.ts +1 -1
  43. package/dist/artifacts/export.js +2 -3
  44. package/dist/artifacts/folders.d.ts +6 -3
  45. package/dist/artifacts/folders.js +17 -12
  46. package/dist/artifacts/service.d.ts +2 -1
  47. package/dist/artifacts/service.js +6 -1
  48. package/dist/artifacts/workspace.d.ts +1 -1
  49. package/dist/artifacts/workspace.js +1 -1
  50. package/dist/core/flow-host.d.ts +2 -3
  51. package/dist/core/flow-host.js +3 -0
  52. package/dist/core/provider.d.ts +3 -2
  53. package/dist/core/provider.js +5 -37
  54. package/dist/core/sandbox-routes.d.ts +2 -21
  55. package/dist/core/sandbox-routes.js +10 -43
  56. package/dist/execution/types.d.ts +1 -1
  57. package/dist/gates/contract.d.ts +3 -24
  58. package/dist/gates/contract.js +1 -36
  59. package/dist/gates/egress-credentials.d.ts +9 -0
  60. package/dist/gates/egress-credentials.js +11 -4
  61. package/dist/gates/egress.d.ts +28 -1
  62. package/dist/gates/egress.js +115 -10
  63. package/dist/gates/fixture-model.d.ts +28 -0
  64. package/dist/gates/fixture-model.js +92 -0
  65. package/dist/gates/handler.js +4 -4
  66. package/dist/gates/http-tool.d.ts +6 -2
  67. package/dist/gates/http-tool.js +3 -5
  68. package/dist/gates/in-process.d.ts +5 -0
  69. package/dist/gates/in-process.js +12 -1
  70. package/dist/gates/journal.d.ts +30 -0
  71. package/dist/gates/journal.js +52 -0
  72. package/dist/gates/mcp-handler.d.ts +4 -0
  73. package/dist/gates/mcp-handler.js +5 -1
  74. package/dist/gates/meter.d.ts +10 -2
  75. package/dist/gates/meter.js +20 -5
  76. package/dist/gates/policy.d.ts +49 -0
  77. package/dist/gates/policy.js +134 -0
  78. package/dist/gates/tenant-vaults.d.ts +7 -7
  79. package/dist/gates/tenant-vaults.js +5 -5
  80. package/dist/gates/tool-calls.d.ts +10 -5
  81. package/dist/gates/tool-calls.js +28 -10
  82. package/dist/gates/tool-client.d.ts +8 -0
  83. package/dist/gates/tool-client.js +27 -11
  84. package/dist/gates/tool-contract.d.ts +10 -68
  85. package/dist/gates/tool-contract.js +1 -85
  86. package/dist/gates/tool-gate.d.ts +1 -1
  87. package/dist/harness/calls.d.ts +12 -5
  88. package/dist/harness/calls.js +21 -12
  89. package/dist/harness/executors.js +12 -9
  90. package/dist/harness/main.js +18 -5
  91. package/dist/harness/pod.d.ts +34 -0
  92. package/dist/harness/pod.js +41 -0
  93. package/dist/harness/service.d.ts +0 -1
  94. package/dist/harness/service.js +13 -5
  95. package/dist/harness/setup.d.ts +27 -0
  96. package/dist/harness/setup.js +150 -0
  97. package/dist/harness/ws-client.js +3 -2
  98. package/dist/harness/ws-port.d.ts +1 -5
  99. package/dist/harness/ws-port.js +1 -5
  100. package/dist/harness-api/in-process.js +3 -3
  101. package/dist/harness-api/record.d.ts +15 -1
  102. package/dist/harness-api/record.js +174 -44
  103. package/dist/harness-api/redact.d.ts +3 -0
  104. package/dist/harness-api/redact.js +45 -0
  105. package/dist/harness-api/server.d.ts +8 -0
  106. package/dist/harness-api/server.js +93 -13
  107. package/dist/harness-api/start.d.ts +10 -3
  108. package/dist/harness-api/start.js +40 -10
  109. package/dist/harness-api/tool-journal.d.ts +96 -0
  110. package/dist/harness-api/tool-journal.js +102 -0
  111. package/dist/harness-api/tool-status.d.ts +13 -0
  112. package/dist/harness-api/tool-status.js +26 -0
  113. package/dist/harness-api/workspace.d.ts +6 -3
  114. package/dist/harness-api/workspace.js +27 -11
  115. package/dist/harness-api/ws-server.d.ts +11 -3
  116. package/dist/harness-api/ws-server.js +94 -15
  117. package/dist/host/gates.d.ts +2 -1
  118. package/dist/host/gates.js +10 -2
  119. package/dist/host/main.js +54 -5
  120. package/dist/host/operate.js +67 -5
  121. package/dist/host/stack-config.d.ts +64 -0
  122. package/dist/host/stack-config.js +61 -1
  123. package/dist/keys/client.js +4 -1
  124. package/dist/keys/keys.d.ts +5 -5
  125. package/dist/keys/keys.js +5 -7
  126. package/dist/management-openapi.json +999 -2211
  127. package/dist/mcp/connect.d.ts +25 -20
  128. package/dist/mcp/connect.js +29 -30
  129. package/dist/mcp/pool.d.ts +2 -2
  130. package/dist/mcp/pool.js +1 -1
  131. package/dist/mcp/preview.d.ts +3 -3
  132. package/dist/mcp/preview.js +10 -20
  133. package/dist/mcp/snapshot.d.ts +5 -11
  134. package/dist/mcp/snapshot.js +6 -7
  135. package/dist/model/pi-model.js +8 -0
  136. package/dist/openapi.json +6955 -2526
  137. package/dist/reads/types.d.ts +16 -3
  138. package/dist/record/append.d.ts +3 -1
  139. package/dist/record/append.js +1 -0
  140. package/dist/record/envelope.d.ts +5 -1
  141. package/dist/record/envelope.js +7 -3
  142. package/dist/record/environment.d.ts +25 -0
  143. package/dist/record/environment.js +44 -0
  144. package/dist/record/index.d.ts +1 -1
  145. package/dist/record/index.js +1 -1
  146. package/dist/sandbox/disk-fs.d.ts +95 -0
  147. package/dist/sandbox/disk-fs.js +206 -0
  148. package/dist/sandbox/join.d.ts +17 -18
  149. package/dist/sandbox/join.js +42 -15
  150. package/dist/sandbox/manager.d.ts +16 -8
  151. package/dist/sandbox/manager.js +33 -17
  152. package/dist/sandbox/placement.d.ts +13 -12
  153. package/dist/sandbox/placement.js +29 -12
  154. package/dist/sandbox/pods/client.d.ts +6 -1
  155. package/dist/sandbox/pods/lifecycle.d.ts +28 -27
  156. package/dist/sandbox/pods/lifecycle.js +14 -26
  157. package/dist/sandbox/pods/reconcile.js +18 -12
  158. package/dist/sandbox/pods/spec.d.ts +15 -13
  159. package/dist/sandbox/pods/spec.js +16 -20
  160. package/dist/sandbox/records.d.ts +2 -2
  161. package/dist/sandbox/records.js +3 -3
  162. package/dist/sandbox/resolve.d.ts +35 -27
  163. package/dist/sandbox/resolve.js +66 -41
  164. package/dist/sandbox/session-sandbox.js +1 -1
  165. package/dist/sandbox/setup.d.ts +61 -0
  166. package/dist/sandbox/setup.js +123 -0
  167. package/dist/sandbox/share.d.ts +11 -21
  168. package/dist/sandbox/share.js +12 -41
  169. package/dist/sandbox/tenant-config.d.ts +12 -14
  170. package/dist/sandbox/tenant-config.js +12 -17
  171. package/dist/sandbox/types.d.ts +6 -0
  172. package/dist/store/postgres/drizzle/0020_environments.sql +63 -0
  173. package/dist/store/postgres/drizzle/0021_disk_versions.sql +7 -0
  174. package/dist/store/postgres/drizzle/0022_model_usage_timing.sql +5 -0
  175. package/dist/store/postgres/drizzle/0023_artifact_pages.sql +1 -0
  176. package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
  177. package/dist/store/postgres/reads.js +117 -17
  178. package/dist/store/postgres/record-writer.d.ts +4 -4
  179. package/dist/store/postgres/record-writer.js +9 -9
  180. package/dist/store/postgres/record.d.ts +7 -0
  181. package/dist/store/postgres/record.js +23 -2
  182. package/dist/store/postgres/schema.d.ts +934 -761
  183. package/dist/store/postgres/schema.js +102 -73
  184. package/dist/store/postgres/store.js +119 -66
  185. package/dist/store/types.d.ts +109 -48
  186. package/dist/tenant/advance.d.ts +7 -6
  187. package/dist/tenant/advance.js +115 -42
  188. package/dist/tenant/artifact-tool.js +3 -4
  189. package/dist/tenant/commands.d.ts +15 -2
  190. package/dist/tenant/commands.js +93 -12
  191. package/dist/tenant/context.d.ts +48 -22
  192. package/dist/tenant/context.js +4 -15
  193. package/dist/tenant/effects.d.ts +1 -1
  194. package/dist/tenant/effects.js +37 -20
  195. package/dist/tenant/environments.d.ts +167 -0
  196. package/dist/tenant/environments.js +823 -0
  197. package/dist/tenant/harness-tokens.d.ts +21 -0
  198. package/dist/tenant/harness-tokens.js +61 -0
  199. package/dist/tenant/host-token.d.ts +1 -1
  200. package/dist/tenant/host-token.js +2 -4
  201. package/dist/tenant/identity-file.d.ts +4 -4
  202. package/dist/tenant/identity-file.js +17 -8
  203. package/dist/tenant/issuers.js +3 -3
  204. package/dist/tenant/principals.js +1 -1
  205. package/dist/tenant/reset.js +3 -3
  206. package/dist/tenant/runtime.d.ts +6 -1
  207. package/dist/tenant/runtime.js +7 -1
  208. package/dist/tenant/session.d.ts +3 -3
  209. package/dist/tenant/session.js +0 -14
  210. package/dist/tenant/sessions.d.ts +13 -13
  211. package/dist/tenant/sessions.js +61 -138
  212. package/dist/tenant/signing-keys.d.ts +5 -1
  213. package/dist/tenant/status.js +3 -3
  214. package/dist/tenant/sweep.js +12 -6
  215. package/dist/tenant/transcript.d.ts +210 -20
  216. package/dist/tenant/transcript.js +62 -11
  217. package/dist/tenant/turn-timing.d.ts +43 -0
  218. package/dist/tenant/turn-timing.js +39 -0
  219. package/dist/tenant/types.d.ts +2 -0
  220. package/dist/traces/ids.d.ts +42 -0
  221. package/dist/traces/ids.js +85 -0
  222. package/dist/traces/key.d.ts +6 -0
  223. package/dist/traces/key.js +37 -0
  224. package/dist/traces/map.d.ts +113 -0
  225. package/dist/traces/map.js +317 -0
  226. package/dist/traces/options.d.ts +7 -0
  227. package/dist/traces/options.js +1 -0
  228. package/dist/traces/otlp.d.ts +42 -0
  229. package/dist/traces/otlp.js +84 -0
  230. package/dist/traces/projector.d.ts +58 -0
  231. package/dist/traces/projector.js +200 -0
  232. package/dist/traces/read.d.ts +23 -0
  233. package/dist/traces/read.js +16 -0
  234. package/dist/vault/coverage.d.ts +26 -0
  235. package/dist/vault/coverage.js +64 -0
  236. package/dist/vault/error.d.ts +11 -1
  237. package/dist/vault/error.js +5 -1
  238. package/dist/vault/service.d.ts +47 -48
  239. package/dist/vault/service.js +321 -382
  240. package/dist/vault/sources.d.ts +5 -16
  241. package/dist/vault/sources.js +6 -8
  242. package/dist/version.d.ts +1 -1
  243. package/dist/version.js +2 -2
  244. package/package.json +13 -13
  245. package/dist/api/http/routes/sandboxes.d.ts +0 -9
  246. package/dist/api/http/routes/sandboxes.js +0 -153
  247. package/dist/api/http/routes/vaults.d.ts +0 -10
  248. package/dist/api/http/routes/vaults.js +0 -139
  249. package/dist/record/sandbox.d.ts +0 -25
  250. package/dist/record/sandbox.js +0 -43
  251. package/dist/tenant/sandboxes.d.ts +0 -123
  252. package/dist/tenant/sandboxes.js +0 -566
@@ -0,0 +1,85 @@
1
+ /**
2
+ * A turn's trace ids (R7.2, Q3): derived from the ids the log already holds, so every reader of
3
+ * the log (the projector, a replay of its slot, the trace route) gives the same ids, and keyed
4
+ * with the Tenant's trace key, so nobody computes them from a session id. Each is HMAC-SHA256
5
+ * under the key, cut to 16 bytes (a trace id) or 8 (a span id), as lowercase hex.
6
+ *
7
+ * - **trace**: `trace|<tenant>|<session>|<turn>`, unless the turn arrived with a `traceparent`
8
+ * (R7.4): then that trace id.
9
+ * - **turn span**: `span|turn|<turn>`; every crossing's span is its child.
10
+ * - **effect span**: `span|effect|<effect id>`, for a model or tool call or a flow stage.
11
+ * - **egress span**: `span|egress|<session>|<seq>`, for one `egress.connection` row.
12
+ * - **event span**: `span|event|<session>|<seq>`, for a marker or a row without an effect id.
13
+ */
14
+ import { createHmac } from "node:crypto";
15
+ function hmacHex(key, input, bytes) {
16
+ const hex = createHmac("sha256", key).update(input).digest().subarray(0, bytes).toString("hex");
17
+ // An all-zero id is invalid in W3C trace context; one chance in 2^64 or worse.
18
+ return /^0+$/.test(hex) ? `${hex.slice(0, -1)}1` : hex;
19
+ }
20
+ export function traceIdOf(key, tenantId, sessionId, turnId) {
21
+ return hmacHex(key, `trace|${tenantId}|${sessionId}|${turnId}`, 16);
22
+ }
23
+ export function turnSpanOf(key, turnId) {
24
+ return hmacHex(key, `span|turn|${turnId}`, 8);
25
+ }
26
+ export function effectSpanOf(key, effectId) {
27
+ return hmacHex(key, `span|effect|${effectId}`, 8);
28
+ }
29
+ export function egressSpanOf(key, sessionId, seq) {
30
+ return hmacHex(key, `span|egress|${sessionId}|${seq}`, 8);
31
+ }
32
+ export function eventSpanOf(key, sessionId, seq) {
33
+ return hmacHex(key, `span|event|${sessionId}|${seq}`, 8);
34
+ }
35
+ const TRACEPARENT = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})(-.*)?$/;
36
+ /**
37
+ * A `traceparent` as W3C trace context reads it: its trace id and parent span id, or undefined
38
+ * when it is malformed (version `ff`, an all-zero id, or another shape), which a receiver
39
+ * ignores. A version above `00` may carry more fields after the flags.
40
+ */
41
+ export function parseTraceparent(value) {
42
+ const match = TRACEPARENT.exec(value.trim());
43
+ if (!match)
44
+ return undefined;
45
+ const [, version, traceId, spanId, , rest] = match;
46
+ if (version === "ff" || (version === "00" && rest !== undefined))
47
+ return undefined;
48
+ if (/^0+$/.test(traceId) || /^0+$/.test(spanId))
49
+ return undefined;
50
+ return { traceId: traceId, spanId: spanId };
51
+ }
52
+ /** The most `tracestate` the Runtime keeps (W3C: 32 members, 512 characters is enough). */
53
+ const TRACESTATE_MAX = 512;
54
+ /**
55
+ * The trace context a request's headers carry (R7.4), or undefined without a valid
56
+ * `traceparent`. A `tracestate` too long or with control characters is dropped, never the
57
+ * `traceparent`. Kept normalized: `00-<trace>-<span>-<flags>`.
58
+ */
59
+ export function traceContextOf(headers) {
60
+ const raw = headers.get("traceparent");
61
+ if (!raw)
62
+ return undefined;
63
+ const parsed = parseTraceparent(raw);
64
+ if (!parsed)
65
+ return undefined;
66
+ const flags = raw.trim().slice(53, 55);
67
+ const traceparent = `00-${parsed.traceId}-${parsed.spanId}-${flags}`;
68
+ const state = headers.get("tracestate")?.trim();
69
+ return {
70
+ traceparent,
71
+ ...(state && state.length <= TRACESTATE_MAX && /^[\x20-\x7e]+$/.test(state) ? { tracestate: state } : {}),
72
+ };
73
+ }
74
+ /** `00-<trace>-<span>-01`: the context a turn or a crossing hands on (sampled). */
75
+ export function traceparentOf(traceId, spanId) {
76
+ return `00-${traceId}-${spanId}-01`;
77
+ }
78
+ /**
79
+ * The trace id of turn `turnId`: the one its message arrived with (`inbound`, R7.4), else
80
+ * derived. The same as `turnTraceOf` reads from the turn's `command.message`.
81
+ */
82
+ export function turnTraceIdOf(key, ids, inbound) {
83
+ return ((inbound ? parseTraceparent(inbound.traceparent)?.traceId : undefined) ??
84
+ traceIdOf(key, ids.tenantId, ids.sessionId, ids.turnId));
85
+ }
@@ -0,0 +1,6 @@
1
+ import type { SessionStore, Tx } from "../store/types.js";
2
+ import type { TraceKey } from "./ids.js";
3
+ /** The Tenant's trace key, creating it when the Tenant has none. Call outside a transaction. */
4
+ export declare function traceKey(store: SessionStore): Promise<TraceKey>;
5
+ /** The trace key inside `t`, creating it when the Tenant has none. */
6
+ export declare function traceKeyIn(t: Tx): Promise<TraceKey>;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The Tenant's trace key (R7.2, Q3): 32 random bytes in the Tenant setting `trace.key` (base64),
3
+ * created on first use under the signing keys' lock, as the egress CA is. It is not sealed: it
4
+ * keys trace ids so nobody computes them from a session id, and anyone who can read the setting
5
+ * can read the log the ids point at. Core reads it in both process modes; the gateway never
6
+ * needs it (a tool call's `traceparent` is on its journal row, R7.4).
7
+ *
8
+ * Read once per store and kept: a Tenant reset keeps its settings.
9
+ */
10
+ import { randomBytes } from "node:crypto";
11
+ const SETTING = "trace.key";
12
+ const keys = new WeakMap();
13
+ /** The Tenant's trace key, creating it when the Tenant has none. Call outside a transaction. */
14
+ export function traceKey(store) {
15
+ let key = keys.get(store);
16
+ if (!key) {
17
+ key = store.tx((t) => traceKeyIn(t));
18
+ // A failed read is tried again by the next caller.
19
+ key.catch(() => keys.delete(store));
20
+ keys.set(store, key);
21
+ }
22
+ return key;
23
+ }
24
+ /** The trace key inside `t`, creating it when the Tenant has none. */
25
+ export async function traceKeyIn(t) {
26
+ const existing = await t.getSetting(SETTING);
27
+ if (existing !== undefined)
28
+ return Buffer.from(existing, "base64");
29
+ // Concurrent first uses would both create one: lock, then read what the winner made.
30
+ await t.lockSigningKeys();
31
+ const won = await t.getSetting(SETTING);
32
+ if (won !== undefined)
33
+ return Buffer.from(won, "base64");
34
+ const created = randomBytes(32);
35
+ await t.putSetting(SETTING, created.toString("base64"));
36
+ return created;
37
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Row to span (R7.2): a pure function from one committed session event and its turn's trace to
3
+ * at most one OTLP span, so the projector, a replay of its slot and the trace route (R7.5) give
4
+ * the same spans. Nothing outside the row and the turn is read.
5
+ *
6
+ * | Row | Span |
7
+ * | --- | --- |
8
+ * | `turn.completed`, `turn.failed`, `turn.cancelled` | `invoke_agent {agentId}`, the turn's root (INTERNAL) |
9
+ * | `message.assistant`, `model.failed`, `context.compacted` | `chat {model}` (CLIENT) |
10
+ * | `tool.completed` | `execute_tool {toolName}` (INTERNAL) |
11
+ * | `node.completed` | `execute_tool {key}`, a flow's HTTP stage (INTERNAL) |
12
+ * | `effect.uncertain` | the call's span, with an error status |
13
+ * | `egress.connection` | `CONNECT {host}` (CLIENT) |
14
+ * | `turn.paused`, `command.approve`, `command.respond` | a marker with no length |
15
+ *
16
+ * Every other row, and every row without a turn, makes none. Every child's parent is the turn's
17
+ * span, so a child exported before its turn ends still joins it.
18
+ *
19
+ * Timing: a span with a gate's own measurement (`gate`, R7.1) is that measurement; any other ends
20
+ * at the row's time and starts `durationMs` before it (the journal's window, exported as
21
+ * `nylorun.effect.window_ms`). An egress span is the gate's: `startedAt` plus `durationMs`.
22
+ *
23
+ * Attribute names follow the OTel GenAI conventions at `GENAI_SEMCONV` (still in Development
24
+ * status), recorded on the resource as `nylorun.semconv.genai`; the rest are `nylorun.*`. A span
25
+ * never carries a person's id (`ownerUserId`, a subject, who approved), an identity header, an
26
+ * idempotency key or a credential value (the log holds none). Content (what a person said, the
27
+ * model's output, tool results and error messages) is left out unless the installation opts in
28
+ * per kind (`TraceContent`), each value cut at 8 KiB. Prompts are not in the log, so never here.
29
+ */
30
+ import type { LiveEvent } from "@nylorun/core/contracts";
31
+ import { type TraceKey } from "./ids.js";
32
+ /** The commit of open-telemetry/semantic-conventions-genai whose attribute names spans use. */
33
+ export declare const GENAI_SEMCONV = "open-telemetry/semantic-conventions-genai@6fd0d763a092db245a54104fc1124031eb3c51d2";
34
+ /**
35
+ * The content an installation exports (Q4): `user` (what a person sent, on the turn's span),
36
+ * `outputs` (the model's text and tool calls, and a turn's output), `tool-results` and `errors`
37
+ * (error messages; their codes are always exported).
38
+ */
39
+ export declare const TRACE_CONTENT_KINDS: readonly ["user", "outputs", "tool-results", "errors"];
40
+ export type TraceContentKind = (typeof TRACE_CONTENT_KINDS)[number];
41
+ export type TraceContent = ReadonlySet<TraceContentKind>;
42
+ /** The most bytes of one content attribute or status message. */
43
+ export declare const CONTENT_MAX_BYTES: number;
44
+ /** OTLP span kinds and status codes (opentelemetry-proto `trace.proto`). */
45
+ export declare const SPAN_KIND: {
46
+ readonly internal: 1;
47
+ readonly client: 3;
48
+ };
49
+ export declare const STATUS: {
50
+ readonly unset: 0;
51
+ readonly ok: 1;
52
+ readonly error: 2;
53
+ };
54
+ export interface OtlpAnyValue {
55
+ stringValue?: string;
56
+ boolValue?: boolean;
57
+ intValue?: string;
58
+ doubleValue?: number;
59
+ arrayValue?: {
60
+ values: OtlpAnyValue[];
61
+ };
62
+ }
63
+ export interface OtlpKeyValue {
64
+ key: string;
65
+ value: OtlpAnyValue;
66
+ }
67
+ /** One span as OTLP/HTTP JSON encodes it: hex ids, times as decimal nanosecond strings. */
68
+ export interface OtlpSpan {
69
+ traceId: string;
70
+ spanId: string;
71
+ parentSpanId?: string;
72
+ name: string;
73
+ kind: number;
74
+ startTimeUnixNano: string;
75
+ endTimeUnixNano: string;
76
+ attributes: OtlpKeyValue[];
77
+ status: {
78
+ code: number;
79
+ message?: string;
80
+ };
81
+ }
82
+ /**
83
+ * A turn's trace: its trace id, its span, and the context it arrived with (R7.4). Built from the
84
+ * turn's `command.message` (`turnTraceOf`), so it is the same wherever the log is read.
85
+ */
86
+ export interface TurnTrace {
87
+ readonly traceId: string;
88
+ readonly spanId: string;
89
+ /** The span of the caller that sent the turn's message with a `traceparent`. */
90
+ readonly parentSpanId?: string;
91
+ /** Who sent the turn's message: its principal's kind, never its id. */
92
+ readonly principalKind?: string;
93
+ /** What a person sent, as `gen_ai.input.messages` (only with `user` content). */
94
+ readonly input?: string;
95
+ }
96
+ /** How spans are made: the trace key and the content the installation exports. */
97
+ export interface SpanSettings {
98
+ readonly key: TraceKey;
99
+ readonly content: TraceContent;
100
+ }
101
+ /**
102
+ * The trace of turn `turnId` of session `sessionId`, from its `command.message` when the log has
103
+ * it: the inbound `traceparent`'s trace id and parent, else derived ids.
104
+ */
105
+ export declare function turnTraceOf(settings: SpanSettings, ids: {
106
+ tenantId: string;
107
+ sessionId: string;
108
+ turnId: string;
109
+ }, message?: Pick<LiveEvent, "payload" | "principal"> | undefined): TurnTrace;
110
+ /** The span `row` makes in its turn's trace, or undefined when it makes none. */
111
+ export declare function spanOf(row: LiveEvent, turn: TurnTrace, settings: SpanSettings): OtlpSpan | undefined;
112
+ /** `value` cut to `CONTENT_MAX_BYTES` of UTF-8, never in the middle of a character. */
113
+ export declare function truncate(value: string): string;
@@ -0,0 +1,317 @@
1
+ import { effectSpanOf, egressSpanOf, eventSpanOf, parseTraceparent, traceIdOf, turnSpanOf } from "./ids.js";
2
+ /** The commit of open-telemetry/semantic-conventions-genai whose attribute names spans use. */
3
+ export const GENAI_SEMCONV = "open-telemetry/semantic-conventions-genai@6fd0d763a092db245a54104fc1124031eb3c51d2";
4
+ /**
5
+ * The content an installation exports (Q4): `user` (what a person sent, on the turn's span),
6
+ * `outputs` (the model's text and tool calls, and a turn's output), `tool-results` and `errors`
7
+ * (error messages; their codes are always exported).
8
+ */
9
+ export const TRACE_CONTENT_KINDS = ["user", "outputs", "tool-results", "errors"];
10
+ /** The most bytes of one content attribute or status message. */
11
+ export const CONTENT_MAX_BYTES = 8 * 1024;
12
+ /** OTLP span kinds and status codes (opentelemetry-proto `trace.proto`). */
13
+ export const SPAN_KIND = { internal: 1, client: 3 };
14
+ export const STATUS = { unset: 0, ok: 1, error: 2 };
15
+ /**
16
+ * The trace of turn `turnId` of session `sessionId`, from its `command.message` when the log has
17
+ * it: the inbound `traceparent`'s trace id and parent, else derived ids.
18
+ */
19
+ export function turnTraceOf(settings, ids, message) {
20
+ const payload = (message?.payload ?? {});
21
+ const inbound = typeof payload.trace?.traceparent === "string" ? parseTraceparent(payload.trace.traceparent) : undefined;
22
+ const input = settings.content.has("user") && message ? userMessages(payload) : undefined;
23
+ return {
24
+ traceId: inbound?.traceId ?? traceIdOf(settings.key, ids.tenantId, ids.sessionId, ids.turnId),
25
+ spanId: turnSpanOf(settings.key, ids.turnId),
26
+ ...(inbound ? { parentSpanId: inbound.spanId } : {}),
27
+ ...(message?.principal ? { principalKind: message.principal.kind } : {}),
28
+ ...(input === undefined ? {} : { input }),
29
+ };
30
+ }
31
+ /** The span `row` makes in its turn's trace, or undefined when it makes none. */
32
+ export function spanOf(row, turn, settings) {
33
+ if (row.turnId === null || row.visibility === "internal")
34
+ return undefined;
35
+ const p = (row.payload ?? {});
36
+ const base = new Attributes()
37
+ .str("gen_ai.conversation.id", row.sessionId)
38
+ .str("nylorun.turn.id", row.turnId)
39
+ .str("nylorun.event.cursor", row.cursor)
40
+ .str("nylorun.source.kind", row.source.kind)
41
+ // A client's source id is its principal, a person's id when it acts for one: never exported.
42
+ .str("nylorun.source.id", row.source.kind === "client" ? undefined : row.source.id)
43
+ .str("nylorun.evidence", row.evidence)
44
+ .str("nylorun.run.id", row.runId ?? undefined);
45
+ const child = (spanId, name, kind) => ({
46
+ traceId: turn.traceId,
47
+ spanId,
48
+ parentSpanId: turn.spanId,
49
+ name,
50
+ kind,
51
+ });
52
+ const effectSpan = (effectId) => typeof effectId === "string" && effectId.length > 0
53
+ ? effectSpanOf(settings.key, effectId)
54
+ : eventSpanOf(settings.key, row.sessionId, row.seq);
55
+ const content = (kind, value) => settings.content.has(kind) && value !== undefined ? truncate(typeof value === "string" ? value : JSON.stringify(value)) : undefined;
56
+ switch (row.type) {
57
+ case "turn.completed":
58
+ case "turn.failed":
59
+ case "turn.cancelled": {
60
+ const status = row.type.slice("turn.".length);
61
+ const totals = p.totals;
62
+ const error = row.type === "turn.failed" ? (p.error ?? {}) : undefined;
63
+ const attributes = base
64
+ .str("gen_ai.operation.name", "invoke_agent")
65
+ .str("gen_ai.agent.id", p.agentId)
66
+ .str("nylorun.turn.status", status)
67
+ .int("nylorun.turn.paused_ms", p.pausedMs)
68
+ .int("nylorun.turn.steps", totals?.steps)
69
+ .int("nylorun.turn.tokens", totals?.tokens)
70
+ .double("nylorun.usage.cost_usd", totals?.costUsd)
71
+ .str("nylorun.principal.kind", turn.principalKind)
72
+ .str("error.type", error ? (error.code ?? "turn.failed") : undefined)
73
+ .str("gen_ai.input.messages", turn.input)
74
+ .str("gen_ai.output.messages", row.type === "turn.completed" && p.output !== undefined && p.output !== null
75
+ ? content("outputs", [{ role: "assistant", parts: [textPart(p.output)] }])
76
+ : undefined);
77
+ return {
78
+ traceId: turn.traceId,
79
+ spanId: turn.spanId,
80
+ ...(turn.parentSpanId ? { parentSpanId: turn.parentSpanId } : {}),
81
+ name: typeof p.agentId === "string" ? `invoke_agent ${p.agentId}` : "invoke_agent",
82
+ kind: SPAN_KIND.internal,
83
+ ...window(row.time, p.durationMs),
84
+ attributes: attributes.list,
85
+ status: row.type === "turn.failed"
86
+ ? errorStatus(content("errors", error?.message ?? p.message))
87
+ : row.type === "turn.completed"
88
+ ? { code: STATUS.ok }
89
+ : { code: STATUS.unset },
90
+ };
91
+ }
92
+ case "message.assistant":
93
+ case "model.failed":
94
+ case "context.compacted": {
95
+ const model = p.model;
96
+ const metered = p.meteredUsage;
97
+ const reported = p.usage;
98
+ const usage = metered ?? reported;
99
+ const mismatch = metered !== undefined &&
100
+ reported !== undefined &&
101
+ (metered.inputTokens !== reported.inputTokens || metered.outputTokens !== reported.outputTokens);
102
+ const output = row.type === "message.assistant"
103
+ ? content("outputs", [
104
+ {
105
+ role: "assistant",
106
+ parts: [
107
+ ...(typeof p.text === "string" && p.text !== "" ? [{ type: "text", content: p.text }] : []),
108
+ ...(p.toolCalls ?? []).map((call) => ({
109
+ type: "tool_call",
110
+ id: call.callId,
111
+ name: call.name,
112
+ arguments: call.input,
113
+ })),
114
+ ],
115
+ ...(typeof p.finishReason === "string" ? { finish_reason: p.finishReason } : {}),
116
+ },
117
+ ])
118
+ : undefined;
119
+ const attributes = base
120
+ .str("gen_ai.operation.name", "chat")
121
+ .str("gen_ai.provider.name", model?.provider)
122
+ .str("gen_ai.response.model", model?.model)
123
+ .strings("gen_ai.response.finish_reasons", typeof p.finishReason === "string" ? [p.finishReason] : undefined)
124
+ .int("gen_ai.usage.input_tokens", usage?.inputTokens)
125
+ .int("gen_ai.usage.output_tokens", usage?.outputTokens)
126
+ .int("gen_ai.usage.cache_read.input_tokens", metered?.cachedTokens)
127
+ .double("nylorun.usage.cost_usd", metered?.costUsd)
128
+ .str("nylorun.usage.source", metered ? "gate" : reported ? "harness" : undefined)
129
+ .bool("nylorun.usage.mismatch", mismatch ? true : undefined)
130
+ .double("gen_ai.response.time_to_first_chunk", typeof p.firstTokenMs === "number" ? p.firstTokenMs / 1000 : undefined)
131
+ .str("gen_ai.agent.id", p.agent?.id)
132
+ .str("nylorun.effect.id", p.effectId)
133
+ .int("nylorun.effect.window_ms", p.durationMs)
134
+ .str("nylorun.compaction.trigger", row.type === "context.compacted" ? p.trigger : undefined)
135
+ .bool("nylorun.compaction.partial", row.type === "context.compacted" && p.partial === true ? true : undefined)
136
+ .str("error.type", row.type === "model.failed" ? p.code : undefined)
137
+ .str("gen_ai.output.messages", output);
138
+ return {
139
+ ...child(effectSpan(p.effectId), model?.model ? `chat ${model.model}` : "chat", SPAN_KIND.client),
140
+ ...timesOf(row.time, p.gate, p.durationMs),
141
+ attributes: attributes.list,
142
+ status: row.type === "model.failed" ? errorStatus(content("errors", p.message)) : { code: STATUS.ok },
143
+ };
144
+ }
145
+ case "tool.completed":
146
+ case "node.completed": {
147
+ const node = row.type === "node.completed";
148
+ const name = String(node ? p.key : p.toolName);
149
+ const kind = p.kind;
150
+ const error = p.error;
151
+ const failed = p.status === "failed" || p.status === "cancelled" || error !== undefined;
152
+ const policy = p.policy;
153
+ const attributes = base
154
+ .str("gen_ai.operation.name", "execute_tool")
155
+ .str("gen_ai.tool.name", name)
156
+ .str("gen_ai.tool.call.id", node ? undefined : p.callId)
157
+ .str("gen_ai.tool.type", kind === "mcp" || kind === "http" ? "extension" : kind ? "function" : undefined)
158
+ .str("gen_ai.agent.id", p.agent?.id)
159
+ .str("nylorun.tool.kind", kind)
160
+ .str("nylorun.tool.server", p.server)
161
+ .str("nylorun.tool.status", p.status)
162
+ .int("nylorun.tool.attempts", p.attempts)
163
+ .int("nylorun.tool.redacted", p.redacted)
164
+ .str("nylorun.flow.path", node ? p.path : undefined)
165
+ .str("nylorun.policy.decision", policy?.decision)
166
+ .str("nylorun.policy.reason", policy?.reason)
167
+ .str("nylorun.effect.id", p.effectId)
168
+ .int("nylorun.effect.window_ms", p.durationMs)
169
+ .str("error.type", failed ? (error?.code ?? `tool.${p.status}`) : undefined)
170
+ .str("gen_ai.tool.call.result", node || p.output === undefined ? undefined : content("tool-results", p.output));
171
+ return {
172
+ ...child(effectSpan(p.effectId), `execute_tool ${name}`, SPAN_KIND.internal),
173
+ ...timesOf(row.time, p.gate, p.durationMs),
174
+ attributes: attributes.list,
175
+ status: failed ? errorStatus(content("errors", error?.message)) : { code: STATUS.ok },
176
+ };
177
+ }
178
+ case "effect.uncertain": {
179
+ const effectId = String(p.effectId ?? "");
180
+ const operation = effectId.includes(":tool:") ? "execute_tool" : "chat";
181
+ return {
182
+ ...child(effectSpan(effectId), operation, operation === "chat" ? SPAN_KIND.client : SPAN_KIND.internal),
183
+ ...window(row.time, undefined),
184
+ attributes: base
185
+ .str("gen_ai.operation.name", operation)
186
+ .str("nylorun.effect.id", effectId)
187
+ .str("error.type", "effect.uncertain").list,
188
+ status: errorStatus(content("errors", p.message)),
189
+ };
190
+ }
191
+ case "egress.connection": {
192
+ const refused = p.outcome === "refused";
193
+ const started = Date.parse(String(p.startedAt));
194
+ const attributes = base
195
+ .str("server.address", p.host)
196
+ .int("server.port", p.port)
197
+ .str("nylorun.environment.id", p.environmentId)
198
+ .str("nylorun.egress.outcome", p.outcome)
199
+ .str("nylorun.egress.mode", p.mode)
200
+ .bool("nylorun.egress.credential_bound", p.credentialBound)
201
+ .strings("nylorun.egress.credential_ids", p.credentialIds)
202
+ .int("nylorun.egress.requests", p.requests)
203
+ .int("nylorun.egress.bytes_up", p.bytesUp)
204
+ .int("nylorun.egress.bytes_down", p.bytesDown)
205
+ .int("nylorun.egress.status", p.status)
206
+ .str("error.type", refused ? (p.reason ?? "refused") : undefined);
207
+ const times = Number.isFinite(started)
208
+ ? { start: started, end: started + Math.max(0, Math.round(Number(p.durationMs) || 0)) }
209
+ : undefined;
210
+ return {
211
+ ...child(egressSpanOf(settings.key, row.sessionId, row.seq), `CONNECT ${String(p.host)}`, SPAN_KIND.client),
212
+ ...(times
213
+ ? { startTimeUnixNano: nanos(times.start), endTimeUnixNano: nanos(times.end) }
214
+ : window(row.time, undefined)),
215
+ attributes: attributes.list,
216
+ status: refused ? errorStatus(undefined) : { code: STATUS.unset },
217
+ };
218
+ }
219
+ case "turn.paused":
220
+ case "command.approve":
221
+ case "command.respond": {
222
+ const interactions = (p.interactions ?? []);
223
+ const attributes = base
224
+ .str("nylorun.interaction.id", row.type === "turn.paused" ? undefined : p.interactionId)
225
+ .bool("nylorun.interaction.approved", row.type === "command.approve" ? p.approved === true : undefined)
226
+ .strings("nylorun.interaction.kinds", row.type === "turn.paused" ? interactions.map((entry) => String(entry.interaction?.kind ?? "")) : undefined)
227
+ .str("nylorun.principal.kind", row.type === "turn.paused" ? undefined : row.principal?.kind);
228
+ return {
229
+ ...child(eventSpanOf(settings.key, row.sessionId, row.seq), row.type, SPAN_KIND.internal),
230
+ ...window(row.time, undefined),
231
+ attributes: attributes.list,
232
+ status: { code: STATUS.unset },
233
+ };
234
+ }
235
+ default:
236
+ return undefined;
237
+ }
238
+ }
239
+ /** Builds a span's attributes in order, leaving out absent values. */
240
+ class Attributes {
241
+ list = [];
242
+ str(key, value) {
243
+ if (typeof value === "string" && value !== "")
244
+ this.list.push({ key, value: { stringValue: value } });
245
+ return this;
246
+ }
247
+ int(key, value) {
248
+ if (typeof value === "number" && Number.isFinite(value))
249
+ this.list.push({ key, value: { intValue: String(Math.round(value)) } });
250
+ return this;
251
+ }
252
+ double(key, value) {
253
+ if (typeof value === "number" && Number.isFinite(value))
254
+ this.list.push({ key, value: { doubleValue: value } });
255
+ return this;
256
+ }
257
+ bool(key, value) {
258
+ if (typeof value === "boolean")
259
+ this.list.push({ key, value: { boolValue: value } });
260
+ return this;
261
+ }
262
+ strings(key, value) {
263
+ if (Array.isArray(value) && value.length > 0)
264
+ this.list.push({ key, value: { arrayValue: { values: value.map((item) => ({ stringValue: String(item) })) } } });
265
+ return this;
266
+ }
267
+ }
268
+ /** Unix milliseconds as OTLP's decimal nanoseconds. */
269
+ function nanos(ms) {
270
+ return (BigInt(Math.round(ms)) * 1000000n).toString();
271
+ }
272
+ /** Ends at the row's time and starts `durationMs` before it (no length without one). */
273
+ function window(time, durationMs) {
274
+ const end = Date.parse(time);
275
+ const length = typeof durationMs === "number" && durationMs > 0 ? durationMs : 0;
276
+ return { startTimeUnixNano: nanos(end - length), endTimeUnixNano: nanos(end) };
277
+ }
278
+ /** The gate's own start and end when it measured the call, else the journal's window. */
279
+ function timesOf(time, gate, durationMs) {
280
+ const measured = gate;
281
+ const start = Date.parse(String(measured?.startedAt));
282
+ const end = Date.parse(String(measured?.endedAt));
283
+ if (Number.isFinite(start) && Number.isFinite(end) && end >= start)
284
+ return { startTimeUnixNano: nanos(start), endTimeUnixNano: nanos(end) };
285
+ return window(time, durationMs);
286
+ }
287
+ function errorStatus(message) {
288
+ return { code: STATUS.error, ...(message ? { message } : {}) };
289
+ }
290
+ /** A GenAI message part for a value: text as is, anything else as JSON text. */
291
+ function textPart(value) {
292
+ return { type: "text", content: typeof value === "string" ? value : JSON.stringify(value) };
293
+ }
294
+ /** What a person sent (`command.message`), as `gen_ai.input.messages`. */
295
+ function userMessages(payload) {
296
+ const parts = typeof payload.content === "string"
297
+ ? [{ type: "text", content: payload.content }]
298
+ : Array.isArray(payload.parts)
299
+ ? payload.parts.map((part) => part.type === "text"
300
+ ? { type: "text", content: part.text ?? "" }
301
+ : { type: "file", artifact_id: part.artifactId, ...(part.version ? { version: part.version } : {}) })
302
+ : payload.data !== undefined
303
+ ? [textPart(payload.data)]
304
+ : undefined;
305
+ return parts ? truncate(JSON.stringify([{ role: "user", parts }])) : undefined;
306
+ }
307
+ /** `value` cut to `CONTENT_MAX_BYTES` of UTF-8, never in the middle of a character. */
308
+ export function truncate(value) {
309
+ const bytes = Buffer.from(value, "utf8");
310
+ if (bytes.length <= CONTENT_MAX_BYTES)
311
+ return value;
312
+ let end = CONTENT_MAX_BYTES;
313
+ // Back up over continuation bytes (10xxxxxx) to the start of the cut character.
314
+ while (end > 0 && (bytes[end] & 0xc0) === 0x80)
315
+ end -= 1;
316
+ return bytes.subarray(0, end).toString("utf8");
317
+ }
@@ -0,0 +1,7 @@
1
+ /** How spans are made beyond the log (R7.2): what they say of their source and what content they carry. */
2
+ import type { TraceContent } from "./map.js";
3
+ import type { TraceResource } from "./otlp.js";
4
+ export interface TraceOptions {
5
+ readonly resource: TraceResource;
6
+ readonly content: TraceContent;
7
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,42 @@
1
+ import { type OtlpKeyValue, type OtlpSpan } from "./map.js";
2
+ /** What every batch says about its source: `OTEL_SERVICE_NAME` and `OTEL_RESOURCE_ATTRIBUTES`. */
3
+ export interface TraceResource {
4
+ /** `service.name`; default `nylorun`. */
5
+ readonly serviceName: string;
6
+ /** More resource attributes, as `OTEL_RESOURCE_ATTRIBUTES` sets them. */
7
+ readonly attributes?: Readonly<Record<string, string>>;
8
+ }
9
+ export declare const DEFAULT_SERVICE_NAME = "nylorun";
10
+ /** A batch of spans as an OTLP `ExportTraceServiceRequest` in JSON. */
11
+ export declare function resourceSpans(resource: TraceResource, spans: readonly OtlpSpan[]): {
12
+ resourceSpans: {
13
+ resource: {
14
+ attributes: OtlpKeyValue[];
15
+ };
16
+ scopeSpans: {
17
+ scope: {
18
+ name: string;
19
+ version: string;
20
+ };
21
+ spans: OtlpSpan[];
22
+ }[];
23
+ }[];
24
+ };
25
+ export interface OtlpPosterOptions {
26
+ /** The traces URL: `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`, or the endpoint plus `/v1/traces`. */
27
+ readonly url: string;
28
+ /** `OTEL_EXPORTER_OTLP_HEADERS`. */
29
+ readonly headers?: Readonly<Record<string, string>>;
30
+ readonly resource: TraceResource;
31
+ /** Tries after the first, for a `429`, a `5xx` or a lost connection. Default 3. */
32
+ readonly retries?: number;
33
+ /** The wait before the first retry, doubled for each one after. Default 500 ms. */
34
+ readonly backoffMs?: number;
35
+ /** How long one request may take. Default 10 s. */
36
+ readonly timeoutMs?: number;
37
+ readonly fetch?: typeof fetch;
38
+ readonly log?: (message: string, fields?: Record<string, unknown>) => void;
39
+ }
40
+ /** Posts one batch; resolves `posted` or `dropped`, never rejects. */
41
+ export type OtlpPoster = (spans: readonly OtlpSpan[], signal?: AbortSignal) => Promise<"posted" | "dropped">;
42
+ export declare function createOtlpPoster(options: OtlpPosterOptions): OtlpPoster;