@nylorun/runtime 0.23.0-beta → 0.25.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 (301) hide show
  1. package/CHANGELOG.md +252 -0
  2. package/README.md +87 -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/card.d.ts +11 -1
  10. package/dist/api/a2a/card.js +26 -2
  11. package/dist/api/a2a/endpoint.js +9 -3
  12. package/dist/api/a2a/protocol.d.ts +25 -2
  13. package/dist/api/a2a/protocol.js +20 -3
  14. package/dist/api/a2a/routes.d.ts +6 -1
  15. package/dist/api/a2a/routes.js +67 -9
  16. package/dist/api/a2a/tasks.js +2 -1
  17. package/dist/api/ag-ui/endpoint.js +8 -3
  18. package/dist/api/ag-ui/history.d.ts +6 -1
  19. package/dist/api/ag-ui/history.js +39 -3
  20. package/dist/api/ag-ui/routes.d.ts +4 -1
  21. package/dist/api/ag-ui/routes.js +63 -20
  22. package/dist/api/ag-ui/translate.js +3 -2
  23. package/dist/api/components.d.ts +5327 -1091
  24. package/dist/api/components.js +40 -16
  25. package/dist/api/gate/keys.d.ts +2 -1
  26. package/dist/api/gate/keys.js +14 -1
  27. package/dist/api/gate/routes.d.ts +6 -0
  28. package/dist/api/gate/routes.js +52 -3
  29. package/dist/api/http/app.js +17 -6
  30. package/dist/api/http/body.d.ts +3 -3
  31. package/dist/api/http/body.js +7 -7
  32. package/dist/api/http/define.d.ts +6 -0
  33. package/dist/api/http/define.js +13 -1
  34. package/dist/api/http/respond.js +1 -1
  35. package/dist/api/http/routes/artifacts.js +35 -8
  36. package/dist/api/http/routes/coverage.d.ts +9 -0
  37. package/dist/api/http/routes/coverage.js +34 -0
  38. package/dist/api/http/routes/environment-manifests.d.ts +9 -0
  39. package/dist/api/http/routes/environment-manifests.js +118 -0
  40. package/dist/api/http/routes/environments.d.ts +11 -0
  41. package/dist/api/http/routes/environments.js +332 -0
  42. package/dist/api/http/routes/mcp.d.ts +1 -1
  43. package/dist/api/http/routes/mcp.js +2 -2
  44. package/dist/api/http/routes/me.js +1 -1
  45. package/dist/api/http/routes/reads.d.ts +94 -9
  46. package/dist/api/http/routes/reads.js +54 -5
  47. package/dist/api/http/routes/sessions.js +24 -9
  48. package/dist/api/http/routes/tenant.js +65 -15
  49. package/dist/api/inbound-files.d.ts +65 -0
  50. package/dist/api/inbound-files.js +115 -0
  51. package/dist/api/openapi.js +48 -28
  52. package/dist/artifacts/disk.d.ts +59 -0
  53. package/dist/artifacts/disk.js +569 -0
  54. package/dist/artifacts/export.d.ts +1 -1
  55. package/dist/artifacts/export.js +2 -3
  56. package/dist/artifacts/folders.d.ts +6 -3
  57. package/dist/artifacts/folders.js +17 -12
  58. package/dist/artifacts/media-types.d.ts +15 -0
  59. package/dist/artifacts/media-types.js +56 -4
  60. package/dist/artifacts/parts.d.ts +12 -3
  61. package/dist/artifacts/parts.js +15 -4
  62. package/dist/artifacts/service.d.ts +2 -1
  63. package/dist/artifacts/service.js +6 -1
  64. package/dist/artifacts/workspace.d.ts +5 -5
  65. package/dist/artifacts/workspace.js +7 -19
  66. package/dist/core/flow-host.d.ts +3 -4
  67. package/dist/core/flow-host.js +5 -2
  68. package/dist/core/provider.d.ts +3 -2
  69. package/dist/core/provider.js +5 -37
  70. package/dist/core/sandbox-routes.d.ts +2 -21
  71. package/dist/core/sandbox-routes.js +11 -44
  72. package/dist/core/turn-manifest.d.ts +3 -2
  73. package/dist/core/turn-manifest.js +29 -5
  74. package/dist/execution/types.d.ts +1 -1
  75. package/dist/gates/contract.d.ts +3 -24
  76. package/dist/gates/contract.js +1 -36
  77. package/dist/gates/egress-credentials.d.ts +9 -0
  78. package/dist/gates/egress-credentials.js +11 -4
  79. package/dist/gates/egress.d.ts +22 -1
  80. package/dist/gates/egress.js +116 -10
  81. package/dist/gates/fixture-model.d.ts +28 -0
  82. package/dist/gates/fixture-model.js +92 -0
  83. package/dist/gates/handler.js +4 -4
  84. package/dist/gates/http-tool.d.ts +9 -6
  85. package/dist/gates/http-tool.js +11 -17
  86. package/dist/gates/in-process.d.ts +5 -0
  87. package/dist/gates/in-process.js +12 -1
  88. package/dist/gates/journal.d.ts +30 -0
  89. package/dist/gates/journal.js +51 -0
  90. package/dist/gates/mcp-handler.d.ts +4 -0
  91. package/dist/gates/mcp-handler.js +7 -4
  92. package/dist/gates/meter.d.ts +10 -2
  93. package/dist/gates/meter.js +20 -5
  94. package/dist/gates/policy.d.ts +49 -0
  95. package/dist/gates/policy.js +132 -0
  96. package/dist/gates/tenant-vaults.d.ts +7 -7
  97. package/dist/gates/tenant-vaults.js +5 -5
  98. package/dist/gates/tool-calls.d.ts +10 -5
  99. package/dist/gates/tool-calls.js +28 -10
  100. package/dist/gates/tool-client.d.ts +8 -0
  101. package/dist/gates/tool-client.js +27 -11
  102. package/dist/gates/tool-contract.d.ts +11 -69
  103. package/dist/gates/tool-contract.js +1 -85
  104. package/dist/gates/tool-gate.d.ts +1 -1
  105. package/dist/harness/calls.d.ts +14 -7
  106. package/dist/harness/calls.js +30 -24
  107. package/dist/harness/executors.js +12 -9
  108. package/dist/harness/main.js +24 -5
  109. package/dist/harness/pod.d.ts +39 -0
  110. package/dist/harness/pod.js +46 -0
  111. package/dist/harness/service.d.ts +0 -1
  112. package/dist/harness/service.js +17 -9
  113. package/dist/harness/setup.d.ts +29 -0
  114. package/dist/harness/setup.js +174 -0
  115. package/dist/harness/ws-client.js +3 -2
  116. package/dist/harness/ws-port.d.ts +1 -5
  117. package/dist/harness/ws-port.js +1 -5
  118. package/dist/harness-api/in-process.js +3 -3
  119. package/dist/harness-api/record.d.ts +15 -1
  120. package/dist/harness-api/record.js +180 -50
  121. package/dist/harness-api/redact.d.ts +3 -0
  122. package/dist/harness-api/redact.js +45 -0
  123. package/dist/harness-api/server.d.ts +35 -4
  124. package/dist/harness-api/server.js +130 -17
  125. package/dist/harness-api/start.d.ts +10 -3
  126. package/dist/harness-api/start.js +40 -10
  127. package/dist/harness-api/tool-journal.d.ts +105 -0
  128. package/dist/harness-api/tool-journal.js +132 -0
  129. package/dist/harness-api/tool-status.d.ts +13 -0
  130. package/dist/harness-api/tool-status.js +26 -0
  131. package/dist/harness-api/workspace.d.ts +12 -7
  132. package/dist/harness-api/workspace.js +47 -25
  133. package/dist/harness-api/ws-server.d.ts +11 -3
  134. package/dist/harness-api/ws-server.js +94 -15
  135. package/dist/host/gates.d.ts +2 -1
  136. package/dist/host/gates.js +10 -2
  137. package/dist/host/main.js +54 -5
  138. package/dist/host/operate.js +67 -5
  139. package/dist/host/stack-config.d.ts +64 -0
  140. package/dist/host/stack-config.js +62 -2
  141. package/dist/keys/client.js +4 -1
  142. package/dist/keys/keys.d.ts +13 -6
  143. package/dist/keys/keys.js +9 -8
  144. package/dist/management-openapi.json +1264 -2171
  145. package/dist/mcp/connect.d.ts +26 -22
  146. package/dist/mcp/connect.js +30 -34
  147. package/dist/mcp/pool.d.ts +3 -6
  148. package/dist/mcp/pool.js +14 -28
  149. package/dist/mcp/preview.d.ts +3 -3
  150. package/dist/mcp/preview.js +10 -21
  151. package/dist/mcp/snapshot.d.ts +16 -24
  152. package/dist/mcp/snapshot.js +33 -45
  153. package/dist/model/capabilities.d.ts +26 -0
  154. package/dist/model/capabilities.js +44 -0
  155. package/dist/model/catalog.d.ts +34 -3
  156. package/dist/model/catalog.js +38 -2
  157. package/dist/model/models.d.ts +2 -0
  158. package/dist/model/models.js +1 -1
  159. package/dist/model/native-blocks.d.ts +35 -0
  160. package/dist/model/native-blocks.js +170 -0
  161. package/dist/model/pi-model.js +114 -94
  162. package/dist/openapi.json +12822 -3943
  163. package/dist/reads/types.d.ts +16 -3
  164. package/dist/record/append.d.ts +3 -1
  165. package/dist/record/append.js +1 -0
  166. package/dist/record/envelope.d.ts +5 -1
  167. package/dist/record/envelope.js +7 -3
  168. package/dist/record/environment.d.ts +25 -0
  169. package/dist/record/environment.js +44 -0
  170. package/dist/record/index.d.ts +1 -1
  171. package/dist/record/index.js +1 -1
  172. package/dist/sandbox/disk-fs.d.ts +95 -0
  173. package/dist/sandbox/disk-fs.js +206 -0
  174. package/dist/sandbox/join.d.ts +17 -18
  175. package/dist/sandbox/join.js +46 -15
  176. package/dist/sandbox/manager.d.ts +47 -21
  177. package/dist/sandbox/manager.js +162 -107
  178. package/dist/sandbox/placement.d.ts +14 -13
  179. package/dist/sandbox/placement.js +31 -14
  180. package/dist/sandbox/pods/client.d.ts +9 -2
  181. package/dist/sandbox/pods/lifecycle.d.ts +36 -27
  182. package/dist/sandbox/pods/lifecycle.js +29 -26
  183. package/dist/sandbox/pods/reconcile.js +18 -12
  184. package/dist/sandbox/pods/spec.d.ts +15 -13
  185. package/dist/sandbox/pods/spec.js +17 -20
  186. package/dist/sandbox/records.d.ts +2 -2
  187. package/dist/sandbox/records.js +3 -3
  188. package/dist/sandbox/resolve.d.ts +37 -27
  189. package/dist/sandbox/resolve.js +73 -41
  190. package/dist/sandbox/runtime-tools.d.ts +9 -0
  191. package/dist/sandbox/runtime-tools.js +16 -0
  192. package/dist/sandbox/session-sandbox.d.ts +4 -6
  193. package/dist/sandbox/session-sandbox.js +47 -94
  194. package/dist/sandbox/setup.d.ts +64 -0
  195. package/dist/sandbox/setup.js +160 -0
  196. package/dist/sandbox/share.d.ts +21 -26
  197. package/dist/sandbox/share.js +36 -63
  198. package/dist/sandbox/skills.d.ts +8 -6
  199. package/dist/sandbox/skills.js +24 -23
  200. package/dist/sandbox/tenant-config.d.ts +12 -14
  201. package/dist/sandbox/tenant-config.js +12 -17
  202. package/dist/sandbox/types.d.ts +6 -0
  203. package/dist/store/postgres/drizzle/0020_environments.sql +63 -0
  204. package/dist/store/postgres/drizzle/0021_disk_versions.sql +7 -0
  205. package/dist/store/postgres/drizzle/0022_model_usage_timing.sql +5 -0
  206. package/dist/store/postgres/drizzle/0023_artifact_pages.sql +1 -0
  207. package/dist/store/postgres/drizzle/0024_flat_manifest.sql +7 -0
  208. package/dist/store/postgres/drizzle/0025_environment_manifests.sql +14 -0
  209. package/dist/store/postgres/drizzle/0026_environment_forks.sql +1 -0
  210. package/dist/store/postgres/drizzle/0027_environment_image_digest.sql +1 -0
  211. package/dist/store/postgres/drizzle/meta/_journal.json +56 -0
  212. package/dist/store/postgres/reads.js +123 -17
  213. package/dist/store/postgres/record-writer.d.ts +4 -4
  214. package/dist/store/postgres/record-writer.js +9 -9
  215. package/dist/store/postgres/record.d.ts +7 -0
  216. package/dist/store/postgres/record.js +23 -2
  217. package/dist/store/postgres/schema.d.ts +1126 -736
  218. package/dist/store/postgres/schema.js +136 -73
  219. package/dist/store/postgres/store.js +195 -67
  220. package/dist/store/types.d.ts +165 -50
  221. package/dist/tenant/advance.d.ts +7 -6
  222. package/dist/tenant/advance.js +118 -42
  223. package/dist/tenant/artifact-tool.d.ts +1 -1
  224. package/dist/tenant/artifact-tool.js +46 -26
  225. package/dist/tenant/commands.d.ts +15 -2
  226. package/dist/tenant/commands.js +115 -14
  227. package/dist/tenant/context.d.ts +56 -24
  228. package/dist/tenant/context.js +4 -15
  229. package/dist/tenant/effects.d.ts +1 -1
  230. package/dist/tenant/effects.js +44 -29
  231. package/dist/tenant/environment-manifests.d.ts +57 -0
  232. package/dist/tenant/environment-manifests.js +200 -0
  233. package/dist/tenant/environments.d.ts +174 -0
  234. package/dist/tenant/environments.js +1044 -0
  235. package/dist/tenant/harness-tokens.d.ts +21 -0
  236. package/dist/tenant/harness-tokens.js +61 -0
  237. package/dist/tenant/host-token.d.ts +1 -1
  238. package/dist/tenant/host-token.js +2 -4
  239. package/dist/tenant/identity-file.d.ts +4 -4
  240. package/dist/tenant/identity-file.js +17 -8
  241. package/dist/tenant/issuers.js +3 -3
  242. package/dist/tenant/model-setting.d.ts +14 -0
  243. package/dist/tenant/model-setting.js +51 -0
  244. package/dist/tenant/principals.js +1 -1
  245. package/dist/tenant/reset.js +3 -3
  246. package/dist/tenant/runtime.d.ts +6 -1
  247. package/dist/tenant/runtime.js +7 -1
  248. package/dist/tenant/session.d.ts +3 -3
  249. package/dist/tenant/session.js +0 -14
  250. package/dist/tenant/sessions.d.ts +18 -14
  251. package/dist/tenant/sessions.js +99 -143
  252. package/dist/tenant/signing-keys.d.ts +5 -1
  253. package/dist/tenant/skill-tool.d.ts +3 -3
  254. package/dist/tenant/skill-tool.js +6 -11
  255. package/dist/tenant/status.js +3 -3
  256. package/dist/tenant/sweep.js +13 -7
  257. package/dist/tenant/tool-results.js +1 -1
  258. package/dist/tenant/tool-search.d.ts +7 -11
  259. package/dist/tenant/tool-search.js +8 -5
  260. package/dist/tenant/transcript.d.ts +245 -36
  261. package/dist/tenant/transcript.js +64 -12
  262. package/dist/tenant/turn-timing.d.ts +43 -0
  263. package/dist/tenant/turn-timing.js +39 -0
  264. package/dist/tenant/types.d.ts +2 -0
  265. package/dist/traces/ids.d.ts +41 -0
  266. package/dist/traces/ids.js +81 -0
  267. package/dist/traces/key.d.ts +6 -0
  268. package/dist/traces/key.js +37 -0
  269. package/dist/traces/map.d.ts +113 -0
  270. package/dist/traces/map.js +289 -0
  271. package/dist/traces/options.d.ts +7 -0
  272. package/dist/traces/options.js +1 -0
  273. package/dist/traces/otlp.d.ts +42 -0
  274. package/dist/traces/otlp.js +84 -0
  275. package/dist/traces/projector.d.ts +58 -0
  276. package/dist/traces/projector.js +200 -0
  277. package/dist/traces/read.d.ts +23 -0
  278. package/dist/traces/read.js +16 -0
  279. package/dist/vault/coverage.d.ts +26 -0
  280. package/dist/vault/coverage.js +62 -0
  281. package/dist/vault/error.d.ts +11 -1
  282. package/dist/vault/error.js +5 -1
  283. package/dist/vault/host-model.d.ts +4 -1
  284. package/dist/vault/host-model.js +10 -2
  285. package/dist/vault/service.d.ts +92 -48
  286. package/dist/vault/service.js +462 -376
  287. package/dist/vault/sources.d.ts +5 -16
  288. package/dist/vault/sources.js +6 -8
  289. package/dist/version.d.ts +1 -1
  290. package/dist/version.js +2 -2
  291. package/package.json +13 -13
  292. package/dist/api/http/routes/sandboxes.d.ts +0 -9
  293. package/dist/api/http/routes/sandboxes.js +0 -153
  294. package/dist/api/http/routes/vaults.d.ts +0 -10
  295. package/dist/api/http/routes/vaults.js +0 -139
  296. package/dist/record/sandbox.d.ts +0 -25
  297. package/dist/record/sandbox.js +0 -43
  298. package/dist/sandbox/capability.d.ts +0 -3
  299. package/dist/sandbox/capability.js +0 -10
  300. package/dist/tenant/sandboxes.d.ts +0 -123
  301. package/dist/tenant/sandboxes.js +0 -566
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Native file blocks (multimodal messages, M4): a PDF, an office document, audio or video the
3
+ * model reads, carried to the provider in its own block. pi-ai's messages hold only text and
4
+ * images, so a file goes in as a marker text part (`add`), which pi-ai never merges or escapes,
5
+ * and the provider payload pi-ai builds is rewritten before it is sent (`rewrite`, from the
6
+ * call's `onPayload`): each marker block becomes the API's native block. Images never come
7
+ * here: pi-ai carries them itself.
8
+ *
9
+ * One instance serves one model call. pi-ai builds the payload again on every attempt, so
10
+ * `rewrite` can run more than once; each run sets `error` afresh. A file whose kind or format
11
+ * the API has no block for, an API with no rewriter, or a marker left in the payload (pi-ai
12
+ * folded it into a longer string, or dropped it) is `error`: the call fails with it rather than
13
+ * sending a marker to the model.
14
+ */
15
+ import { randomUUID } from "node:crypto";
16
+ import { essence } from "../artifacts/media-types.js";
17
+ import { NATIVE_INPUTS } from "./capabilities.js";
18
+ /**
19
+ * The kinds each pi-ai API has a native block for here: the model inputs an operator may add
20
+ * (`NATIVE_INPUTS`), images aside, so the two tables never disagree.
21
+ */
22
+ export const NATIVE_FILE_KINDS = Object.freeze(Object.fromEntries(Object.entries(NATIVE_INPUTS).map(([api, inputs]) => [
23
+ api,
24
+ Object.freeze(inputs.filter((input) => input !== "text" && input !== "image")),
25
+ ])));
26
+ /** The kinds of file `api` carries in a native block; none for an API without a rewriter. */
27
+ export function nativeKindsOf(api) {
28
+ return Object.hasOwn(NATIVE_FILE_KINDS, api) ? NATIVE_FILE_KINDS[api] : [];
29
+ }
30
+ const MARKER = /^\[\[nylorun-file:([0-9a-f-]{36})\]\]$/;
31
+ const MARKER_ANYWHERE = /\[\[nylorun-file:[0-9a-f-]{36}\]\]/;
32
+ const dataUrl = (file) => `data:${essence(file.mediaType)};base64,${file.data}`;
33
+ /** `cache_control` the marker block carried (pi-ai marks the last block of a request). */
34
+ const cacheOf = (block) => block.cache_control === undefined ? {} : { cache_control: block.cache_control };
35
+ const textOfType = (type) => (block) => block.type === type ? block.text : undefined;
36
+ /** `input_audio.format` for a media type: OpenAI Chat Completions takes WAV and MP3 only. */
37
+ function audioFormat(mediaType) {
38
+ switch (essence(mediaType)) {
39
+ case "audio/wav":
40
+ case "audio/x-wav":
41
+ case "audio/wave":
42
+ return "wav";
43
+ case "audio/mpeg":
44
+ case "audio/mp3":
45
+ return "mp3";
46
+ default:
47
+ return undefined;
48
+ }
49
+ }
50
+ const anthropic = (file, block) => ({
51
+ type: "document",
52
+ source: { type: "base64", media_type: essence(file.mediaType), data: file.data },
53
+ title: file.name,
54
+ ...cacheOf(block),
55
+ });
56
+ const responses = (file) => ({
57
+ type: "input_file",
58
+ filename: file.name,
59
+ file_data: dataUrl(file),
60
+ });
61
+ const completions = (file, block) => {
62
+ switch (file.kind) {
63
+ case "pdf":
64
+ return { type: "file", file: { filename: file.name, file_data: dataUrl(file) }, ...cacheOf(block) };
65
+ case "audio": {
66
+ const format = audioFormat(file.mediaType);
67
+ if (!format)
68
+ return "this API reads WAV and MP3 audio only";
69
+ return { type: "input_audio", input_audio: { data: file.data, format }, ...cacheOf(block) };
70
+ }
71
+ default:
72
+ return { type: "video_url", video_url: { url: dataUrl(file) }, ...cacheOf(block) };
73
+ }
74
+ };
75
+ const google = (file) => ({
76
+ inlineData: { mimeType: essence(file.mediaType), data: file.data },
77
+ });
78
+ const REWRITERS = {
79
+ "anthropic-messages": { marker: textOfType("text"), native: anthropic },
80
+ "openai-responses": { marker: textOfType("input_text"), native: responses },
81
+ "azure-openai-responses": { marker: textOfType("input_text"), native: responses },
82
+ "openai-completions": { marker: textOfType("text"), native: completions },
83
+ "google-generative-ai": { marker: (block) => (block.type === undefined ? block.text : undefined), native: google },
84
+ "google-vertex": { marker: (block) => (block.type === undefined ? block.text : undefined), native: google },
85
+ };
86
+ const isBlock = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
87
+ export class NativeBlocks {
88
+ files = new Map();
89
+ /** Why the last `rewrite` could not carry every file; undefined when it did. */
90
+ error;
91
+ /** How many files this call carries. */
92
+ get size() {
93
+ return this.files.size;
94
+ }
95
+ /** A text part standing for `file` in pi-ai's context, replaced by `rewrite`. */
96
+ add(file) {
97
+ const id = randomUUID();
98
+ this.files.set(id, file);
99
+ return { type: "text", text: `[[nylorun-file:${id}]]` };
100
+ }
101
+ /**
102
+ * `payload` (an `onPayload` request body for `api`) with each marker block replaced by its
103
+ * native block. Rewrites in place and returns the same payload; sets `error` when a file
104
+ * could not be carried.
105
+ */
106
+ rewrite(payload, api) {
107
+ this.error = undefined;
108
+ if (this.files.size === 0)
109
+ return payload;
110
+ const rewriter = Object.hasOwn(REWRITERS, api) ? REWRITERS[api] : undefined;
111
+ if (!rewriter) {
112
+ this.error = `The model's API (${api}) cannot carry files other than images`;
113
+ return payload;
114
+ }
115
+ const kinds = nativeKindsOf(api);
116
+ const placed = new Set();
117
+ let leftover = false;
118
+ const visit = (value) => {
119
+ if (Array.isArray(value)) {
120
+ for (let index = 0; index < value.length; index++) {
121
+ const item = value[index];
122
+ const replaced = isBlock(item) ? this.replace(item, rewriter, kinds, api) : undefined;
123
+ if (replaced) {
124
+ value[index] = replaced.block;
125
+ placed.add(replaced.id);
126
+ }
127
+ else
128
+ visit(item);
129
+ }
130
+ }
131
+ else if (isBlock(value)) {
132
+ for (const item of Object.values(value))
133
+ visit(item);
134
+ }
135
+ else if (typeof value === "string" && MARKER_ANYWHERE.test(value)) {
136
+ leftover = true;
137
+ }
138
+ };
139
+ visit(payload);
140
+ if (this.error)
141
+ return payload;
142
+ if (leftover || placed.size < this.files.size) {
143
+ const missing = [...this.files].find(([id]) => !placed.has(id))?.[1];
144
+ this.error = missing
145
+ ? `File "${missing.name}" did not reach the ${api} request as its own block`
146
+ : `A file marker was left in the ${api} request`;
147
+ }
148
+ return payload;
149
+ }
150
+ /** The native block for a marker block of ours; undefined for any other block. */
151
+ replace(block, rewriter, kinds, api) {
152
+ const text = rewriter.marker(block);
153
+ if (typeof text !== "string")
154
+ return undefined;
155
+ const id = MARKER.exec(text)?.[1];
156
+ const file = id === undefined ? undefined : this.files.get(id);
157
+ if (!file)
158
+ return undefined;
159
+ const refuse = (reason) => {
160
+ this.error ??= `File "${file.name}" (${essence(file.mediaType)}) cannot be sent to ${api}: ${reason}`;
161
+ return undefined;
162
+ };
163
+ if (!kinds.includes(file.kind))
164
+ return refuse(`this API has no block for ${file.kind} files`);
165
+ const native = rewriter.native(file, block);
166
+ if (typeof native === "string")
167
+ return refuse(native);
168
+ return { id: id, block: native };
169
+ }
170
+ }
@@ -1,9 +1,12 @@
1
1
  import { parseJsonWithRepair, retryAssistantCall, } from "@earendil-works/pi-ai";
2
+ import { mediaReferenceOf } from "@nylorun/core/define";
2
3
  import { FileUnavailableError, } from "../artifacts/files.js";
3
- import { essence, isModelImage, isText } from "../artifacts/media-types.js";
4
+ import { essence, inputLabel, isModelImage, isText, modelInputOf, unreadableReason, } from "../artifacts/media-types.js";
4
5
  import { scrub } from "../redact.js";
6
+ import { capabilitiesOf } from "./capabilities.js";
5
7
  import { classifyAssistantError, failure } from "./classify.js";
6
8
  import { modelsFor } from "./models.js";
9
+ import { NativeBlocks, nativeKindsOf } from "./native-blocks.js";
7
10
  import { projectSecrets } from "./settings.js";
8
11
  const OPENAI_COMPATIBLE_REASONING = /^reasoning(_content|_text)?$/;
9
12
  const emptyUsage = () => ({
@@ -23,8 +26,6 @@ const noAmbientAuth = {
23
26
  return false;
24
27
  },
25
28
  };
26
- class InvalidRequest extends Error {
27
- }
28
29
  /**
29
30
  * Node model adapter. Local provider configuration is read only when invoked.
30
31
  * A provider failure is returned as a failure outcome; only an abort is thrown.
@@ -38,10 +39,13 @@ export function piModel(options = {}) {
38
39
  };
39
40
  return async (call, context) => {
40
41
  context.signal.throwIfAborted();
42
+ // The first stream event's time, from here (R7.1): the meter keeps it on the usage row.
43
+ const calledAt = Date.now();
44
+ let firstEventAt;
41
45
  const root = options.root ?? "";
42
46
  const stored = await options.readHostModel?.();
43
47
  if (!stored)
44
- return failure("auth", "Model provider is not configured. Set it in Studio, or run nylo configure (npx @nylorun/cli configure).", false);
48
+ return failure("auth", "Model provider is not configured. Set it in Studio, or run nylorun tenant models set.", false);
45
49
  const secrets = [
46
50
  ...projectSecrets(root),
47
51
  ...credentialSecrets(stored.credential),
@@ -61,18 +65,22 @@ export function piModel(options = {}) {
61
65
  : {}),
62
66
  };
63
67
  const registry = modelsFor(selection, hostCredentialStore(stored, options.writeHostCredential), { environment: false, authContext: noAmbientAuth });
64
- const selected = registry.getModel(selection.provider, selection.model);
65
- if (!selected)
66
- return failure("invalid_request", "Unknown model. Run nylo configure (npx @nylorun/cli configure).", false);
67
- let request;
68
- try {
69
- request = await buildContext(call, selected, options);
70
- }
71
- catch (error) {
72
- if (error instanceof InvalidRequest)
73
- return failure("invalid_request", error.message, false);
74
- throw error;
75
- }
68
+ const found = registry.getModel(selection.provider, selection.model);
69
+ if (!found)
70
+ return failure("invalid_request", "Unknown model. Run nylorun tenant models set.", false);
71
+ // What the model reads (protocol 12): its catalog's inputs and the operator's additions.
72
+ // pi-ai drops an image the model's `input` lacks, so the call's model carries an added one.
73
+ const { inputs } = capabilitiesOf(found, selection.custom
74
+ ? stored.settings?.inputs
75
+ : stored.addedInputs?.[`${selection.provider}/${selection.model}`]);
76
+ const selected = {
77
+ ...found,
78
+ input: inputs.includes("image") ? ["text", "image"] : ["text"],
79
+ };
80
+ // PDFs, Office documents, audio and video go in the API's own blocks: pi-ai builds the
81
+ // request with a marker in each one's place and `onPayload` swaps it for the bytes.
82
+ const natives = new NativeBlocks();
83
+ const request = await buildContext(call, selected, inputs, options, natives);
76
84
  // Publish only portable references, never the materialized provider image bytes.
77
85
  context.reportPreparedCall?.({
78
86
  adapter: "runtime.pi-ai",
@@ -90,7 +98,20 @@ export function piModel(options = {}) {
90
98
  timeoutMs: settings.requestTimeoutMs,
91
99
  maxRetries: 2,
92
100
  maxRetryDelayMs: 60_000,
101
+ ...(natives.size > 0
102
+ ? {
103
+ // A file that cannot be carried stops the request before it is sent.
104
+ onPayload: (payload) => {
105
+ const rewritten = natives.rewrite(payload, selected.api);
106
+ if (natives.error)
107
+ throw new Error(natives.error);
108
+ return rewritten;
109
+ },
110
+ }
111
+ : {}),
93
112
  };
113
+ // A file the request could not carry fails the call at once: no message, so never retried.
114
+ const nativeFailure = () => failedMessage(selected);
94
115
  const attempt = async () => {
95
116
  const idle = new AbortController();
96
117
  const signal = AbortSignal.any([context.signal, idle.signal]);
@@ -100,6 +121,8 @@ export function piModel(options = {}) {
100
121
  timer = setTimeout(() => idle.abort(), settings.idleTimeoutMs);
101
122
  };
102
123
  arm();
124
+ // Only the answer's own attempt counts: a retried attempt's first event is not its.
125
+ firstEventAt = undefined;
103
126
  try {
104
127
  const stream = registry.streamSimple(selected, request, {
105
128
  ...invocationOptions,
@@ -107,6 +130,7 @@ export function piModel(options = {}) {
107
130
  });
108
131
  for await (const event of stream) {
109
132
  arm();
133
+ firstEventAt ??= Date.now();
110
134
  if (event.type === "text_delta" && options.onPreview) {
111
135
  try {
112
136
  void Promise.resolve(options.onPreview({
@@ -120,6 +144,8 @@ export function piModel(options = {}) {
120
144
  }
121
145
  }
122
146
  const response = await stream.result();
147
+ if (natives.error)
148
+ return nativeFailure();
123
149
  if (idle.signal.aborted && !context.signal.aborted)
124
150
  return idleTimeout(selected, settings.idleTimeoutMs);
125
151
  return response;
@@ -127,6 +153,8 @@ export function piModel(options = {}) {
127
153
  catch (error) {
128
154
  if (context.signal.aborted)
129
155
  throw error;
156
+ if (natives.error)
157
+ return nativeFailure();
130
158
  if (idle.signal.aborted)
131
159
  return idleTimeout(selected, settings.idleTimeoutMs);
132
160
  return {
@@ -145,6 +173,8 @@ export function piModel(options = {}) {
145
173
  }, context.signal);
146
174
  // An abort is never a failure outcome: the host decides what it means.
147
175
  context.signal.throwIfAborted();
176
+ if (natives.error)
177
+ return redacted(failure("invalid_request", natives.error, false));
148
178
  if (response.stopReason === "aborted")
149
179
  return redacted(failure("transient", "The provider stream was aborted.", true));
150
180
  if (response.stopReason === "error") {
@@ -152,7 +182,7 @@ export function piModel(options = {}) {
152
182
  return redacted(outcome.code === "auth"
153
183
  ? {
154
184
  ...outcome,
155
- message: `${outcome.message} Update the provider credential in Studio's Model Settings, or run nylo configure.`,
185
+ message: `${outcome.message} Update the provider credential in Studio's Model Settings, or run nylorun tenant models set.`,
156
186
  }
157
187
  : outcome);
158
188
  }
@@ -218,92 +248,81 @@ export function piModel(options = {}) {
218
248
  // The engine keeps the next prompt inside this window (Model Calls §7).
219
249
  contextWindow: selected.contextWindow,
220
250
  maxOutputTokens: selected.maxTokens,
251
+ // From the call to the provider's first stream event (R7.1); absent when none came.
252
+ ...(firstEventAt !== undefined ? { firstTokenMs: firstEventAt - calledAt } : {}),
221
253
  },
222
254
  },
223
255
  };
224
256
  };
225
257
  }
226
- async function buildContext(call, selected, options) {
258
+ async function buildContext(call, selected, inputs, options, natives) {
227
259
  const messages = [];
228
260
  const instructions = [];
229
- const content = async (parts, toolResult = false) => {
261
+ /**
262
+ * A file a media part names (protocol 6, 12), as the model gets it: a text file as a `<file>`
263
+ * block, an image the model reads as an image, a PDF, Office document, audio or video the model
264
+ * reads as a marker for its native block (`natives`), and anything else, or a file that cannot
265
+ * be read, as a note. Its bytes go only into the provider request built here, never into the
266
+ * transcript or the journal, and no file fails the call here.
267
+ */
268
+ const materialize = async (part) => {
269
+ let name = mediaReferenceOf(part.reference)?.name ?? "file";
270
+ let type = essence(part.mediaType);
271
+ const note = (why) => ({
272
+ type: "text",
273
+ text: `(File ${JSON.stringify(name)}, ${type}, is not shown: ${why}.)`,
274
+ });
275
+ const refused = unreadableReason(part.mediaType, inputs);
276
+ if (refused)
277
+ return note(refused);
278
+ if (!options.files)
279
+ return note("files are unavailable");
280
+ let file;
281
+ try {
282
+ file = await options.files(part.reference);
283
+ }
284
+ catch (error) {
285
+ if (!(error instanceof FileUnavailableError))
286
+ throw error;
287
+ return note(error.message.replace(/\.$/u, ""));
288
+ }
289
+ name = file.name;
290
+ type = essence(file.mediaType);
291
+ const stored = unreadableReason(file.mediaType, inputs);
292
+ if (stored)
293
+ return note(stored);
294
+ if (isModelImage(file.mediaType))
295
+ return { type: "image", data: Buffer.from(file.bytes).toString("base64"), mimeType: type };
296
+ if (isText(file.mediaType)) {
297
+ let text;
298
+ try {
299
+ text = new TextDecoder("utf-8", { fatal: true }).decode(file.bytes);
300
+ }
301
+ catch {
302
+ return note("it is not UTF-8 text");
303
+ }
304
+ return {
305
+ type: "text",
306
+ text: `<file name=${JSON.stringify(file.name)} type=${JSON.stringify(type)}>\n${text}\n</file>`,
307
+ };
308
+ }
309
+ const kind = modelInputOf(file.mediaType);
310
+ if (isNativeKind(kind) && nativeKindsOf(selected.api).includes(kind))
311
+ return natives.add({
312
+ kind,
313
+ mediaType: type,
314
+ name: file.name,
315
+ data: Buffer.from(file.bytes).toString("base64"),
316
+ });
317
+ return note(`the Runtime does not send ${inputLabel(kind)} to this model's API (${selected.api})`);
318
+ };
319
+ const content = async (parts) => {
230
320
  const result = [];
231
321
  for (const part of parts) {
232
322
  if (part.type === "text")
233
323
  result.push({ type: "text", text: part.text });
234
- else if (part.type === "media" && toolResult) {
235
- // An image a tool returned, stored as an artifact (R2b C11): shown to a model that
236
- // reads images. Otherwise, or when it cannot be read, a note: the call still goes.
237
- const note = (why) => ({
238
- type: "text",
239
- text: `(The tool returned an image, ${essence(part.mediaType)}, that is not shown: ${why}.)`,
240
- });
241
- if (!selected.input.includes("image") || !isModelImage(part.mediaType)) {
242
- result.push(note(selected.input.includes("image")
243
- ? "the model reads PNG, JPEG, GIF and WebP images only"
244
- : "this model does not accept images"));
245
- continue;
246
- }
247
- if (!options.files) {
248
- result.push(note("files are unavailable"));
249
- continue;
250
- }
251
- try {
252
- const file = await options.files(part.reference);
253
- if (!isModelImage(file.mediaType))
254
- result.push(note(`it is ${essence(file.mediaType)}`));
255
- else
256
- result.push({
257
- type: "image",
258
- data: Buffer.from(file.bytes).toString("base64"),
259
- mimeType: essence(file.mediaType),
260
- });
261
- }
262
- catch (error) {
263
- if (!(error instanceof FileUnavailableError))
264
- throw error;
265
- result.push(note(error.message.replace(/\.$/u, "")));
266
- }
267
- }
268
- else if (part.type === "media") {
269
- // A file the message named by artifact (protocol 6): its bytes go only into the
270
- // provider request built here, never into the transcript or the journal.
271
- if (!options.files)
272
- throw new InvalidRequest("Files are unavailable; pass a file resolver to piModel({ files }).");
273
- let file;
274
- try {
275
- file = await options.files(part.reference);
276
- }
277
- catch (error) {
278
- if (error instanceof FileUnavailableError)
279
- throw new InvalidRequest(error.message);
280
- throw error;
281
- }
282
- if (isModelImage(file.mediaType)) {
283
- if (!selected.input.includes("image"))
284
- throw new InvalidRequest("The configured model does not accept images.");
285
- result.push({
286
- type: "image",
287
- data: Buffer.from(file.bytes).toString("base64"),
288
- mimeType: essence(file.mediaType),
289
- });
290
- }
291
- else if (isText(file.mediaType)) {
292
- let text;
293
- try {
294
- text = new TextDecoder("utf-8", { fatal: true }).decode(file.bytes);
295
- }
296
- catch {
297
- throw new InvalidRequest(`${file.name} is not UTF-8 text.`);
298
- }
299
- result.push({
300
- type: "text",
301
- text: `<file name=${JSON.stringify(file.name)} type=${JSON.stringify(essence(file.mediaType))}>\n${text}\n</file>`,
302
- });
303
- }
304
- else
305
- throw new InvalidRequest(`The model reads images and text files; ${file.name} is ${essence(file.mediaType)}.`);
306
- }
324
+ else if (part.type === "media")
325
+ result.push(await materialize(part));
307
326
  }
308
327
  return result;
309
328
  };
@@ -318,7 +337,7 @@ async function buildContext(call, selected, options) {
318
337
  toolName: item.toolName,
319
338
  isError: item.status !== "completed",
320
339
  timestamp: Date.now(),
321
- content: await content(item.content, true),
340
+ content: await content(item.content),
322
341
  });
323
342
  }
324
343
  else if (item.kind === "message" && item.role === "assistant") {
@@ -349,6 +368,7 @@ async function buildContext(call, selected, options) {
349
368
  tools,
350
369
  };
351
370
  }
371
+ const isNativeKind = (input) => input === "pdf" || input === "office" || input === "audio" || input === "video";
352
372
  /**
353
373
  * A replayed assistant message keeps the model that produced it, so pi-ai converts
354
374
  * history correctly after a model switch (tool-call ids, foreign reasoning). Messages
@@ -493,7 +513,7 @@ function hostCredentialStore(stored, write) {
493
513
  return next;
494
514
  },
495
515
  async delete() {
496
- throw new Error("Replace the model provider in Studio, or with nylo configure.");
516
+ throw new Error("Replace the model provider in Studio, or with nylorun tenant models set.");
497
517
  },
498
518
  };
499
519
  }