@nylorun/runtime 0.24.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 (157) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/README.md +10 -2
  3. package/dist/api/a2a/card.d.ts +11 -1
  4. package/dist/api/a2a/card.js +26 -2
  5. package/dist/api/a2a/endpoint.js +4 -2
  6. package/dist/api/a2a/protocol.d.ts +25 -2
  7. package/dist/api/a2a/protocol.js +20 -3
  8. package/dist/api/a2a/routes.d.ts +4 -1
  9. package/dist/api/a2a/routes.js +65 -7
  10. package/dist/api/ag-ui/endpoint.js +5 -2
  11. package/dist/api/ag-ui/history.d.ts +6 -1
  12. package/dist/api/ag-ui/history.js +35 -2
  13. package/dist/api/ag-ui/routes.js +53 -11
  14. package/dist/api/components.d.ts +1165 -678
  15. package/dist/api/components.js +16 -1
  16. package/dist/api/gate/keys.d.ts +2 -1
  17. package/dist/api/gate/keys.js +7 -0
  18. package/dist/api/gate/routes.js +13 -0
  19. package/dist/api/http/app.js +13 -2
  20. package/dist/api/http/body.d.ts +3 -3
  21. package/dist/api/http/body.js +7 -7
  22. package/dist/api/http/routes/environment-manifests.d.ts +9 -0
  23. package/dist/api/http/routes/environment-manifests.js +118 -0
  24. package/dist/api/http/routes/environments.js +61 -12
  25. package/dist/api/http/routes/reads.d.ts +52 -3
  26. package/dist/api/http/routes/sessions.js +13 -3
  27. package/dist/api/http/routes/tenant.js +35 -7
  28. package/dist/api/inbound-files.d.ts +65 -0
  29. package/dist/api/inbound-files.js +115 -0
  30. package/dist/api/openapi.js +18 -3
  31. package/dist/artifacts/disk.js +20 -14
  32. package/dist/artifacts/media-types.d.ts +15 -0
  33. package/dist/artifacts/media-types.js +56 -4
  34. package/dist/artifacts/parts.d.ts +12 -3
  35. package/dist/artifacts/parts.js +15 -4
  36. package/dist/artifacts/workspace.d.ts +4 -4
  37. package/dist/artifacts/workspace.js +6 -18
  38. package/dist/core/flow-host.d.ts +1 -1
  39. package/dist/core/flow-host.js +2 -2
  40. package/dist/core/sandbox-routes.js +2 -2
  41. package/dist/core/turn-manifest.d.ts +3 -2
  42. package/dist/core/turn-manifest.js +29 -5
  43. package/dist/gates/egress-credentials.js +1 -1
  44. package/dist/gates/egress.d.ts +13 -19
  45. package/dist/gates/egress.js +30 -29
  46. package/dist/gates/http-tool.d.ts +3 -4
  47. package/dist/gates/http-tool.js +8 -12
  48. package/dist/gates/journal.js +0 -1
  49. package/dist/gates/mcp-handler.js +2 -3
  50. package/dist/gates/policy.js +1 -3
  51. package/dist/gates/tool-contract.d.ts +1 -1
  52. package/dist/harness/calls.d.ts +2 -2
  53. package/dist/harness/calls.js +9 -12
  54. package/dist/harness/main.js +7 -1
  55. package/dist/harness/pod.d.ts +6 -1
  56. package/dist/harness/pod.js +5 -0
  57. package/dist/harness/service.js +5 -5
  58. package/dist/harness/setup.d.ts +2 -0
  59. package/dist/harness/setup.js +25 -1
  60. package/dist/harness-api/record.d.ts +1 -1
  61. package/dist/harness-api/record.js +13 -13
  62. package/dist/harness-api/server.d.ts +27 -4
  63. package/dist/harness-api/server.js +41 -8
  64. package/dist/harness-api/tool-journal.d.ts +17 -8
  65. package/dist/harness-api/tool-journal.js +41 -11
  66. package/dist/harness-api/workspace.d.ts +7 -5
  67. package/dist/harness-api/workspace.js +25 -19
  68. package/dist/host/gates.js +2 -2
  69. package/dist/host/stack-config.js +1 -1
  70. package/dist/keys/keys.d.ts +9 -2
  71. package/dist/keys/keys.js +4 -1
  72. package/dist/management-openapi.json +366 -61
  73. package/dist/mcp/connect.d.ts +1 -2
  74. package/dist/mcp/connect.js +1 -4
  75. package/dist/mcp/pool.d.ts +1 -4
  76. package/dist/mcp/pool.js +13 -27
  77. package/dist/mcp/preview.js +0 -1
  78. package/dist/mcp/snapshot.d.ts +11 -13
  79. package/dist/mcp/snapshot.js +27 -38
  80. package/dist/model/capabilities.d.ts +26 -0
  81. package/dist/model/capabilities.js +44 -0
  82. package/dist/model/catalog.d.ts +34 -3
  83. package/dist/model/catalog.js +38 -2
  84. package/dist/model/models.d.ts +2 -0
  85. package/dist/model/models.js +1 -1
  86. package/dist/model/native-blocks.d.ts +35 -0
  87. package/dist/model/native-blocks.js +170 -0
  88. package/dist/model/pi-model.js +106 -94
  89. package/dist/openapi.json +9554 -5104
  90. package/dist/record/environment.js +1 -1
  91. package/dist/sandbox/join.js +4 -0
  92. package/dist/sandbox/manager.d.ts +32 -14
  93. package/dist/sandbox/manager.js +132 -93
  94. package/dist/sandbox/placement.d.ts +1 -1
  95. package/dist/sandbox/placement.js +2 -2
  96. package/dist/sandbox/pods/client.d.ts +3 -1
  97. package/dist/sandbox/pods/lifecycle.d.ts +8 -0
  98. package/dist/sandbox/pods/lifecycle.js +15 -0
  99. package/dist/sandbox/pods/spec.js +2 -1
  100. package/dist/sandbox/resolve.d.ts +5 -3
  101. package/dist/sandbox/resolve.js +9 -2
  102. package/dist/sandbox/runtime-tools.d.ts +9 -0
  103. package/dist/sandbox/runtime-tools.js +16 -0
  104. package/dist/sandbox/session-sandbox.d.ts +4 -6
  105. package/dist/sandbox/session-sandbox.js +47 -94
  106. package/dist/sandbox/setup.d.ts +4 -1
  107. package/dist/sandbox/setup.js +46 -9
  108. package/dist/sandbox/share.d.ts +12 -7
  109. package/dist/sandbox/share.js +27 -25
  110. package/dist/sandbox/skills.d.ts +8 -6
  111. package/dist/sandbox/skills.js +24 -23
  112. package/dist/store/postgres/drizzle/0024_flat_manifest.sql +7 -0
  113. package/dist/store/postgres/drizzle/0025_environment_manifests.sql +14 -0
  114. package/dist/store/postgres/drizzle/0026_environment_forks.sql +1 -0
  115. package/dist/store/postgres/drizzle/0027_environment_image_digest.sql +1 -0
  116. package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
  117. package/dist/store/postgres/reads.js +8 -2
  118. package/dist/store/postgres/schema.d.ts +217 -0
  119. package/dist/store/postgres/schema.js +36 -2
  120. package/dist/store/postgres/store.js +79 -4
  121. package/dist/store/types.d.ts +68 -14
  122. package/dist/tenant/advance.js +3 -0
  123. package/dist/tenant/artifact-tool.d.ts +1 -1
  124. package/dist/tenant/artifact-tool.js +43 -22
  125. package/dist/tenant/commands.js +24 -4
  126. package/dist/tenant/context.d.ts +10 -4
  127. package/dist/tenant/effects.js +7 -9
  128. package/dist/tenant/environment-manifests.d.ts +57 -0
  129. package/dist/tenant/environment-manifests.js +200 -0
  130. package/dist/tenant/environments.d.ts +54 -47
  131. package/dist/tenant/environments.js +304 -83
  132. package/dist/tenant/model-setting.d.ts +14 -0
  133. package/dist/tenant/model-setting.js +51 -0
  134. package/dist/tenant/sessions.d.ts +5 -1
  135. package/dist/tenant/sessions.js +45 -12
  136. package/dist/tenant/skill-tool.d.ts +3 -3
  137. package/dist/tenant/skill-tool.js +6 -11
  138. package/dist/tenant/sweep.js +1 -1
  139. package/dist/tenant/tool-results.js +1 -1
  140. package/dist/tenant/tool-search.d.ts +7 -11
  141. package/dist/tenant/tool-search.js +8 -5
  142. package/dist/tenant/transcript.d.ts +51 -32
  143. package/dist/tenant/transcript.js +2 -1
  144. package/dist/traces/ids.d.ts +0 -1
  145. package/dist/traces/ids.js +0 -4
  146. package/dist/traces/map.d.ts +2 -2
  147. package/dist/traces/map.js +1 -29
  148. package/dist/vault/coverage.js +11 -13
  149. package/dist/vault/host-model.d.ts +4 -1
  150. package/dist/vault/host-model.js +10 -2
  151. package/dist/vault/service.d.ts +49 -4
  152. package/dist/vault/service.js +153 -6
  153. package/dist/version.d.ts +1 -1
  154. package/dist/version.js +1 -1
  155. package/package.json +3 -3
  156. package/dist/sandbox/capability.d.ts +0 -3
  157. package/dist/sandbox/capability.js +0 -10
package/CHANGELOG.md CHANGED
@@ -1,5 +1,109 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.25.0-beta
4
+
5
+ ### Minor Changes
6
+
7
+ - 3a32a57: **Files by hash and images by digest (Q6, Q14).** An Environment's `files` entry may name files by content hash, and a pod Environment runs its image by digest once a pod ran it.
8
+
9
+ - **Files by hash.** `{ path, files: { "<relative path>": "sha256:<hex>" } }` copies each definition file the Tenant holds (`PUT /v1/files/sha256:<hex>`, at most 10 MiB each, at most 1000 per entry) under `path`, before `setup`. A file the Tenant does not hold is refused when the manifest is saved or the Environment made. The engine reads them as a folder through `host/files/{index}`. `@nylorun/agents`: `environmentFiles(path, contents)` hashes them, and `manifests.save(manifest, { files })` and `plan()` upload those the Runtime lacks.
10
+ - **Images by digest.** The sandboxes service reports the workload container's `imageID`; the first time a pod is ready the Runtime records it (`pod.imageDigest` in the view) and every later apply runs that digest, so a stop, relaunch or reset never pulls another image under the same tag. A fork pulls afresh. Migration 0027.
11
+ - **Studio** lists files by hash on the Environment page.
12
+
13
+ - af90d1e: **Forks: how an Environment gets a new manifest version (Q15, Q17).** `POST /v1/environments/{id}/fork` makes a new Environment `id` from a manifest version (default the source's manifest at its latest), its disk started from a version of the source's (`disk`: `latest`, the default, snapshots first; a version; or `empty`) and the secrets the new manifest declares copied inside the Runtime (`secrets: "none"` copies none): each value is unsealed and sealed again under the new vault, with the slot's binding. The source keeps what it runs, its disk and its sessions, and gets `environment.forked`; the new Environment's view has `forkedFrom`. `dryRun: true` answers the plan: the disk, the secrets copied and left behind, required slots still missing, the changes and the sessions that stay. Migration 0026.
14
+
15
+ - **Setup on a forked disk.** A pod fork onto other setup runs the new version's setup on the copied disk before its first turn: the engine connects first (the join answer's `afterSeed`), core writes the disk version through it, then the engine runs the setup, and turns wait until it reports it done. A fork onto the same setup, a restore and a lost-volume recovery skip it, as before.
16
+ - `@nylorun/core`: `ForkEnvironmentRequestSchema`, `EnvironmentForkPlanSchema`, `ForkEnvironmentResponseSchema`, `EnvironmentView.forkedFrom`, the `environment.forked` event and `forkedFrom` on `environment.created`.
17
+ - `@nylorun/agents`: `client.environments.fork(id, { id, manifest?, disk?, secrets?, labels?, ownerUserId? })` and `planFork()`.
18
+
19
+ - 104f24f: **Environment manifests, and Environments that never change (protocol 14, D63 to D65).** An Environment's definition can now be saved like an agent: a named, versioned environment manifest, written as a JSON file with a published schema and checked with a plan before it is saved. An Environment is made from one version and keeps it for life; a save changes no Environment. The Runtime speaks protocol 14 only; sessions, Environments and vaults carry over. See MIGRATION.md.
20
+
21
+ - **Environment manifests.** `PUT /v1/environment-manifests/{name}` saves the next version, or answers the latest `unchanged`; `If-Match: <version>` refuses a save over a newer one (`412 manifest_changed`), and `?dryRun=true` answers the plan (changes, errors, warnings, the Environments behind) and saves nothing. Each version records `savedBy`. `GET` (latest), `GET /v1/environment-manifests`, `.../versions`, `.../versions/{n}` and `DELETE` (refused while an Environment names it, `409 manifest_in_use`). Migration 0025.
22
+ - **Pinned Environments.** `PUT /v1/environments/{id}` takes `manifest: "name"` (the latest at creation) or `"name@n"`. On an existing Environment only `labels` and `ownerUserId` may change; fields that match are accepted, and anything else is `409 environment_immutable` with `details.fields`. The view's `recipe` is now `manifest`, with `manifestName`, `manifestVersion`, `manifestHash` and `slots`; `version` and `appliedVersion` no longer move. `environment.created` names the manifest.
23
+ - **Secret slots.** A manifest's (or inline) `secrets` declares each secret's name, type, binding, `required` and `description`, never a value. On such an Environment a secret write sends only `{ value }` (or `{ headers }`), the binding comes from the slot, and an undeclared name is `409 secret_not_in_manifest`.
24
+ - **`requires`.** Agent manifest v7 takes `requires: { sandbox?: "none" | "any" | "pod", secrets? }` (`.requires()` in `@nylorun/core/define`), checked when a session opens (`409 environment_unsuitable`, `details.missing`). Turn variants keep it.
25
+ - **Dry runs.** `PUT /v1/agents/{id}?dryRun=true` checks a definition and saves nothing (`dryRun: true` in the answer).
26
+ - `@nylorun/core`: `PROTOCOL_VERSION` 14, `HOST_PROTOCOL` `{ min: 14, max: 14 }`, required feature `environment-manifests`; `EnvironmentManifestSchema`, `EnvironmentSecretSlotSchema`, `parseEnvironmentManifestRef` and the view, plan and save schemas; JSON Schemas at `@nylorun/core/schemas/environment-manifest.json` and `@nylorun/core/schemas/agent-manifest.json` (`$id` `https://docs.nylorun.com/schemas/environment-manifest/v1.json` and `.../agent-manifest/v7.json`).
27
+ - `@nylorun/agents`: `client.environments.manifests` (`save`, `plan`, `get`, `list`, `versions`, `delete`), `environmentManifest()`, `EnvironmentInput` (was `EnvironmentRecipeInput`), value-only `secrets.put`, and `saveAgent(agent, { dryRun })`.
28
+ - **Studio** edits only an Environment's labels and owner guard, shows the manifest it was made from, and its setup and files are read-only.
29
+ - `nylorun environments put` help says what it runs never changes.
30
+
31
+ - c96b9a8: **An agent manifest is flat: manifest v7, a `runtime` block the Runtime writes, and tools known by name (protocol 13, Harness API 3).** A manifest listed capabilities, each with its own instructions, tools, skills and servers, and the Runtime added its own as more capabilities. Now the agent's parts sit at the top level, a plugin's skills and servers say which plugin brought them, and what the Runtime adds to a session's agent is one `runtime` block. A fresh start: the Runtime speaks protocol 13 only, so save your agents again; sessions from protocol 12 do not carry over. See MIGRATION.md.
32
+
33
+ - **Manifest v7.** `{ manifestSchemaVersion: 7, id, name?, description?, metadata?, outputSchema?, instructions?, tools?, skills?, mcpServers?, runtime? }`. Versions 3 to 6 and a `capabilities` field are refused with a message to rebuild. `SkillManifest.plugin` and `McpServerManifest.plugin` are `{ name, version? }`, set by `.plugin(dir)` from `plugin.json`.
34
+ - **The `runtime` block.** `{ sandbox?, tools?, instructions? }`, written only by the Runtime when a session pins the agent: the sandbox spec and tools, `save_artifact`, `read_artifact`, `load_skill` and `read_skill_resource`, the skill catalog and the notes that go with them. A definition or a turn variant that sets it is refused. `runtimeManifestFor` in `@nylorun/core/define` replaces `sandboxCapabilityManifest`, `artifactsCapabilityManifest` and `toolsCapabilityManifest`. `.skills()` and `.plugin()` no longer add the skill tools or the catalog.
35
+ - **Names.** An agent's tool may not take a Runtime tool's name (`RUNTIME_TOOL_NAMES`) or contain `__`, and names are unique across tools, skills and servers.
36
+ - **Turn variants** may remove tools, tighten MCP servers and change instructions, name, description and metadata. Skills are fixed, and a variant never sets `runtime` or changes `outputSchema`.
37
+ - **Tools by name.** `capabilityId` is gone from `tool.completed`, `mcp.discovered`, tool call rows, `EffectIntent`, gate requests, checkpoints and `SessionToolRef`. An HTTP tool's `server` is its URL's host. `Implementations` is `{ tools }`, keyed by tool name (`Agent.from(json, { tools })`), `AgentBinding.declarations` is `parts`, and observe events say `partId`.
38
+ - **Harness API 3.** `hello.capabilities` is `features`, and the `harness` pseudo-capability is gone: a native tool is known by its name, and one whose name is taken fails the turn at its start. `createHarness({ features })` in `@nylorun/harness`. `examples/harness-minimal` speaks it.
39
+ - **Studio.** The Agent tab shows instructions, tools (HTTP and agent badges), skills and MCP servers, each skill and server with its plugin's badge and a filter by plugin, and a session's Runtime section: its sandbox, tools and instructions.
40
+ - `@nylorun/core`: `PROTOCOL_VERSION` 13, `HOST_PROTOCOL` `{ min: 13, max: 13 }`, required feature `flat-manifest`, `HARNESS_API_VERSION` 3; `PluginLabel`, `RuntimeManifest`, `RUNTIME_TOOL_NAMES`, `isRuntimeToolName`, `runtimeToolOf`, `isSkillTool`, `SKILLS_USAGE` and `formatSkillCatalog` in `/define`.
41
+ - `@nylorun/agents`, `@nylorun/admin`, `@nylorun/studio` and `nylorun` send protocol 13.
42
+
43
+ - 94bf329: **Each model says what it reads, a file it does not read is refused before a turn starts, and a file is an artifact the model reads again after its turn (protocol 12).** A message could name any file, and a model that could not read it failed the turn at its model call; every later turn sent the file's bytes again. Now the Runtime knows what the Tenant's model reads, checks a message's files when it is accepted, and sends a file's bytes only in the turn that brought it. Protocol 12 only adds to 11: the Runtime serves both.
44
+
45
+ - **Model inputs.** A model reads `text`, and `image`, `pdf`, `office`, `audio` or `video` when its catalog says so or the operator adds it where its API carries it natively (images everywhere; PDFs on the Anthropic, OpenAI, Azure and Google APIs; Office documents on the OpenAI Responses APIs; audio and video on Google's APIs and OpenAI-compatible completions). `GET /v1/tenant/models` gives each model `inputs` and `addableInputs`, `GET /v1/tenant/providers` each provider's `inputs`, and **`PUT /v1/tenant/models/inputs`** (the Management API; `admin.models.setInputs({ provider, model, inputs })`) sets the added ones, kept in the Tenant setting `model.inputs`. The Model Gate gives the provider adapter a model with an added image, so it is sent.
46
+ - **A custom endpoint reads text only by default.** It used to be sent images. Set `settings.inputs` on `PUT /v1/tenant/model` (`image`, `pdf`, `audio`, `video`) for what yours reads; `office` is refused there. See MIGRATION.md.
47
+ - **The accept check.** A message naming a file of a type the Tenant's model does not read is `422 attachment_unsupported`, with `details.parts` (`artifactId`, `name`, `mediaType`, `reason`), and no turn starts. Only the type is checked, never the size. Without a model in the vault nothing is refused.
48
+ - **Files as artifacts.** After its turn a file is a reference line, `[Uploaded artifact "name" · <artifactId> v<n> · read_artifact to view]` (a tool's: `[Tool <name> returned artifact …]`). Our harness sends the turn's own files once more, as media, in one user message after the conversation, so a message reads the same in every later call. At the gate, an image the model reads is an image, a text file a `<file>` block, and any other file, or one that cannot be read, a note: no file fails a call. PDFs, Office documents, audio and video are notes until the gate sends them natively.
49
+ - **`read_artifact` for every agent.** Every agent's session has it (an agent that declares its own keeps it), so every agent session pins its definition with it added. It takes a `version`, reads the Tenant-wide artifacts the session's messages named, and shows any file the Tenant's model reads; one it does not read fails `artifact.unsupported`.
50
+ - **Compaction** keeps the summarized part's reference lines (`files` on the compaction entry), and counts the turn's files by type and size in its token estimate.
51
+ - `@nylorun/core`: `PROTOCOL_VERSION` 12, required feature `model-inputs`, `HOST_PROTOCOL` `{ min: 11, max: 12 }`, error code `attachment_unsupported`; `MODEL_INPUTS`, `ModelInputSchema`, `PutModelInputsRequestSchema`, `ModelInputsViewSchema`, `CustomModelSettings.inputs`; `mediaReferenceLine` and `mediaReferenceOf` in `/define`; `TranscriptCompactionEntry.files`.
52
+ - `@nylorun/agents`, `@nylorun/studio` and `nylorun` send protocol 12.
53
+
54
+ - bd80e14: **PDFs, Office documents, audio and video reach the model, and files in AG-UI and A2A messages.**
55
+
56
+ - **Native blocks.** A file the Tenant's model reads is sent in its API's own block, no longer as a note: a PDF as an Anthropic `document`, an OpenAI Responses `input_file`, an OpenAI-compatible completions `file` part or Google `inlineData`; Office documents as an OpenAI Responses `input_file`; audio as completions `input_audio` (WAV and MP3 only) or Google `inlineData`; video as completions `video_url` or Google `inlineData`. The Runtime converts and resizes nothing. A file the request cannot carry (audio other than WAV or MP3 on completions, say) fails the model call with `invalid_request` before anything is sent, and the call is not retried. The bytes are only in the provider request, never in the reported call or the transcript.
57
+ - **Files in AG-UI messages.** A user message's image, audio, video and document parts are files: bytes sent inline (a `data` source) are stored as an artifact of the thread's session, and a `file` source with provider `nylorun` names an artifact the caller may read. A thread's messages give a file back as a media part whose source is `{ type: "file", provider: "nylorun", value: <artifact id> }`.
58
+ - **Files in A2A messages.** A message may carry `raw` file parts beside its text, stored the same way in the context's session. The Agent Card's `defaultInputModes` add the media types the Tenant's model reads (`image/png`, `image/jpeg`, `image/webp`, `image/gif`, `application/pdf`, the three Office Open XML types, `audio/*`, `video/*`).
59
+ - Each file holds at most 20 MiB, and the two endpoints take bodies up to 28 MiB. The message then goes through the session's usual command path as text and file parts, so a file the model does not read is refused like any other (`attachment_unsupported`): a `RUN_ERROR` in the AG-UI stream, `ContentTypeNotSupportedError` over A2A, and the files that message stored are deleted. A retried message (the same AG-UI message id or A2A `messageId`) reuses the files it stored the first time. A file by URL is refused (AG-UI `400`, A2A `ContentTypeNotSupportedError`): the Runtime fetches nothing.
60
+
61
+ - aff9bb8: **Sessions on one Environment run their sandbox commands at the same time, and each version of a skill has its own folder (D66).** The SandboxManager ran every call on a workspace one at a time, whichever session sent it, so parallel turns would still have queued at every command. Now each session's calls run in the order it sends them and different sessions' calls overlap; opening, stopping and removing a workspace run alone, after the calls running.
62
+
63
+ - **Skill folders.** A skill's files are mounted at `/skills/<name>@<hash>/` (`skillFolderOf` in `@nylorun/core/define`, the hash from its canonical file map), written once and never replaced, so agents with different versions of one skill share an Environment. An agent used as a tool gets its own version too. The instructions, `load_skill`'s `sandboxPath` and skill attribution on `tool.completed` follow the folder.
64
+
65
+ - f44c018: **Sessions on one Environment run their turns at the same time, and egress is recorded on the Environment (protocol 14, D66, D67).** A second session's turn on an Environment was refused with `409 environment_busy` until the first ended. Now any session on it may start a turn while others run; one turn per session stays. Which sessions work on one disk, and when, is the app's call. See MIGRATION.md, "Parallel turns, and egress on the Environment".
66
+
67
+ - **Turns.** `checkEnvironmentTurn` no longer refuses a turn because another session's turn runs, and the flow-tree exception is gone. `environment_busy` still answers stop, reset, snapshot and restore while any turn runs, and a turn while a disk copy runs.
68
+ - **Egress.** Every connection through egress-gate, allowed or refused, is `environment.egress` on the Environment's stream, with `runningTurns: [{ sessionId, turnId }]`, the turns running when it opened (at most 16). `egress.connection` leaves the session event catalog, and turn traces carry no `CONNECT` span.
69
+ - **Reading it.** `GET /v1/environments/{id}/events` takes `types` (comma-separated), `since` and `until` (ISO times); `client.environments.events(id, { types, since, until })` in `@nylorun/agents`, and `nylorun environments events <id> --types --since --until`.
70
+ - `@nylorun/core`: `EgressRunningTurnSchema`, `EgressRunningTurn`; `EgressConnectionPayloadSchema` gains `runningTurns`.
71
+
72
+ - c8e640b: **A tool call is traced from the log alone to its skill or MCP server and the plugin that brought it.** No protocol change beyond 13. See MIGRATION.md.
73
+
74
+ - **`tool.completed`** gains `skill` and `plugin` (`{ name, version? }`), and tool call rows (`GET /v1/sessions/{id}/calls/tool`, `session.toolCalls()`) gain `skill` and `plugin`, `null` when there is none. An MCP call gets its server's plugin. `load_skill` and `read_skill_resource` get the skill they name, and a sandbox tool whose input names `/skills/<name>/` gets that skill; both get the skill's plugin.
75
+ - **`kind: "harness"`** (`TOOL_CALL_KINDS`): a tool the turn's harness declared in `hello.features.tools`. Such a call said `platform` before.
76
+ - **`mcp.discovered`** names each server's `plugin`.
77
+ - `@nylorun/core`: `skillOfToolCall`, `pluginOf` and `ToolCallTrace` in `/define`.
78
+ - **Studio** shows each tool call's kind, server or skill, and a plugin badge, in the chat and the event details and summaries, and the plugin of each server `mcp.discovered` lists.
79
+
80
+ ### Patch Changes
81
+
82
+ - 912336a: **A `500 internal_error` is logged.** The Tenant API, the gates listener and the keys service log an unexpected error through the Tenant's or the gate's logger as `request_failed` (`keys_request_failed` for a keys operation), with the method, the route's pattern and the error's message and stack, never a request body, header or argument. The answers are unchanged.
83
+ - a4c431a: **One command, `nylorun`, grouped by the API each command uses.** `nylo` became part of `nylorun`, and the commands that are not the installation's lifecycle moved into groups: `environments` (the Runtime API, an application key), `tenant` (the Management API, a management key) and `local` (Docker and kubectl on this machine). See MIGRATION.md.
84
+
85
+ - **`nylorun`:** `up`, `down`, `start`, `stop`, `status`, `studio` and `doctor` stay at the top. `tenant reset|keys|signing-keys|models set|mcp inspect`; `environments list|delete`; `local ls|delete|logs|reset|sandbox|telemetry`; `doctor sandbox`. The old spellings (`nylorun logs`, `reset`, `key`, `mcp`, `sandbox`, `configure`, …) exit 2 naming their replacement.
86
+ - **Remote installations:** without `--tenant`, `NYLORUN_RUNTIME_URL` with `NYLORUN_MANAGEMENT_KEY` points `status`, `doctor sandbox` and `tenant` at a remote installation, and with `NYLORUN_SERVER_KEY` points `environments` there. `tenant keys` then manages application keys through the Management API; `tenant mcp inspect` and `environments` no longer need a local Tenant.
87
+ - **`nylo`:** for one release, each command exits 2 naming the `nylorun` command that replaced it. `nylo env` has no replacement: the SDKs read the Project link themselves.
88
+ - **`@nylorun/cli`:** its `nylo` still runs nylorun's, which now names the replacement.
89
+ - **`@nylorun/runtime`:** messages that named `nylo configure`, `nylorun sandbox enable|status` or `nylorun key put` name `nylorun tenant models set`, `nylorun local sandbox enable|status` and `nylorun tenant keys put`.
90
+
91
+ - 1a31e47: **A pod's new volume is seeded through its new engine, never the old one.** A restore or a lost volume moves the pod's host epoch, and the old engine's connection closes only when that reaches every process. A seed that started in between could write the version into the old pod, and the new volume came up without the restored files. A seed now waits for an engine that joined at the Environment's epoch or later, and writes through it.
92
+ - Pin core to the tested release.
93
+ - Pin harness to the tested release.
94
+ - Updated dependencies [3a32a57]
95
+ - Updated dependencies [af90d1e]
96
+ - Updated dependencies [104f24f]
97
+ - Updated dependencies [c96b9a8]
98
+ - Updated dependencies [94bf329]
99
+ - Updated dependencies [b5ea2b5]
100
+ - Updated dependencies [aff9bb8]
101
+ - Updated dependencies [f44c018]
102
+ - Updated dependencies [c8e640b]
103
+ - Updated dependencies
104
+ - @nylorun/core@0.21.0-beta
105
+ - @nylorun/harness@0.27.0-beta
106
+
3
107
  ## 0.24.0-beta
4
108
 
5
109
  ### Minor Changes
package/README.md CHANGED
@@ -160,6 +160,7 @@ The Host root is `NYLORUN_HOME` or `~/.nylorun` (for a local Tenant,
160
160
  | `/v1/*` Runtime API routes (all but `/v1/tenant/*`) | application key or trusted issuer's token | Require `Nylorun-Protocol`; nothing names the Tenant. A management key is `403 key_role_mismatch`, except on `/v1/me` and the public `GET /v1/access/jwks` |
161
161
  | `/v1/tenant/*` Management API routes | management key | Require `Nylorun-Protocol`. An application key, alone or acting for a subject, is `403 key_role_mismatch`; a management key with `Nylorun-Subject` or `Nylorun-Scopes` is `403 subject_invalid` |
162
162
  | `PUT /v1/tenant/keys/{keyId}` | management key | Creates application key `keyId` or rotates it, and returns it once; `GET /v1/tenant/keys` lists every key (id, role, when issued, never the keys) and `DELETE` deletes one. `studio`, `bootstrap` and management keys are refused |
163
+ | `PUT /v1/tenant/models/inputs` | management key | What a catalog model reads beyond its catalog's inputs (protocol 12): only inputs in its `addableInputs`; an empty list removes them. `GET /v1/tenant/models` gives each model `inputs` and `addableInputs`. A custom endpoint sets `settings.inputs` on `PUT /v1/tenant/model` (text only by default) |
163
164
  | `GET /v1/artifact-links/{token}` | the link itself | A capability link to one artifact version (protocol 6): no credential, no `Nylorun-Protocol`, Range supported; a folder's link opens its zip, or one of its files |
164
165
 
165
166
  Every route checks `Host` first (`421 host_rejected`) and rejects non-JSON bodies
@@ -171,7 +172,12 @@ are refused from browsers before they are looked up. The Runtime sends no CORS h
171
172
  `OPTIONS` with `204` and `Allow` only: the operator's reverse proxy answers preflights
172
173
  ([DEPLOYMENT.md](../../guides/DEPLOYMENT.md#calling-the-runtime-from-browsers-and-apps)). Missing or
173
174
  unsupported protocol → `426` before authentication, unless the request sends neither
174
- `Nylorun-Protocol` nor `Authorization` (the Host serves protocol 11 only; protocol 11 replaces
175
+ `Nylorun-Protocol` nor `Authorization` (the Host serves protocol 13 only; protocol 13 makes the
176
+ manifest flat, manifest 7: no `capabilities`, tools known by name, the Runtime's own tools in the
177
+ pinned manifest's `runtime` block and no `capabilityId` on events, tool call rows or the Harness
178
+ API, now 3; it closes every session opened before it; protocol 12 adds
179
+ model inputs, the `422 attachment_unsupported` accept check and `read_artifact` for every agent,
180
+ and changes nothing a protocol 11 client sends; protocol 11 replaces
175
181
  sandbox resources, the Tenant's default sandbox, inline sandboxes and user and installation
176
182
  vaults with Environments, each one sandbox and one vault, under `/v1/environments`, and
177
183
  removes the Management API's vault routes, so a protocol 10 client gets `426`; protocol 10 removes
@@ -283,7 +289,8 @@ an epoch; a Worker that takes over after a crash marks in-flight model effects
283
289
  `uncertain`, and sends an interrupted MCP or HTTP tool call again with the same key (R5). Vault ciphertext needs that Tenant's own KEK.
284
290
 
285
291
  A session that names an Environment of kind `virtual` or `pod` gets Runtime-executed
286
- sandbox tools; the agent declares nothing. The Tenant owns each Environment; backend
292
+ sandbox tools, pinned in its manifest's `runtime` block with `save_artifact`; the agent declares
293
+ nothing, and none of its own tools may take a Runtime tool's name. The Tenant owns each Environment; backend
287
294
  names are prefixed `nylorun-<tenant-id>-`.
288
295
 
289
296
  ## Local Project workflow
@@ -304,6 +311,7 @@ the Project link (outside a project it runs the Tenant `default`); the project's
304
311
  | `database-layout-old` | The database holds `tenant_<id>` schemas of an older Runtime, or the `schema_version` tables of a pre-release one: point the Runtime at a new database (locally, a new Tenant: `nylorun start --tenant <new name>`); the old one is left as it is |
305
312
  | `426 protocol_unsupported` | Upgrade clients or Host to a compatible set |
306
313
  | `421 host_rejected` / `403 origin_rejected` | In a container, list the `Host` in `NYLORUN_ALLOWED_HOSTS`. From a browser, send a trusted issuer's token, never a key |
314
+ | `422 attachment_unsupported` | A message names a file the Tenant's model does not read (`details.parts` says which and why): choose a model that reads it, add the input with `PUT /v1/tenant/models/inputs` (or a custom endpoint's `settings.inputs`), or send it as another type |
307
315
  | `403 key_role_mismatch` | The key belongs to the other API: use a management key on `/v1/tenant/*` (`@nylorun/admin`) and an application key everywhere else (`@nylorun/agents`) |
308
316
  | `503` for a Tenant | Postgres or Restate is unreachable; `GET /ready` names which |
309
317
  | Port in use | Change `NYLORUN_PORT` in `~/.nylorun/tenants/<name>/docker/.env` and run `nylorun start` |
@@ -12,4 +12,14 @@ export interface CardDefinition {
12
12
  };
13
13
  readonly implementationVersion: string;
14
14
  }
15
- export declare function agentCard(agentId: string, definition: CardDefinition): Record<string, unknown>;
15
+ import type { ModelInput } from "@nylorun/core/contracts";
16
+ /**
17
+ * The input modes for a model that reads `inputs`: text and JSON, then the media types of the
18
+ * files it reads. No inputs known (no model in the vault): text and JSON.
19
+ */
20
+ export declare function inputModesFor(inputs: readonly ModelInput[] | undefined): string[];
21
+ /**
22
+ * `inputModes`: the media types the agent takes, when its model reads files; default text and
23
+ * JSON. The output modes stay text and JSON.
24
+ */
25
+ export declare function agentCard(agentId: string, definition: CardDefinition, inputModes?: readonly string[]): Record<string, unknown>;
@@ -4,8 +4,32 @@
4
4
  * `supportedInterfaces` is empty: whoever publishes the card (the gateway in v1) adds its own
5
5
  * URL, provider and security schemes.
6
6
  */
7
+ import { MODEL_IMAGE_TYPES } from "../../artifacts/media-types.js";
7
8
  const MODES = ["text/plain", "application/json"];
8
- export function agentCard(agentId, definition) {
9
+ /** The media types a card advertises for each file input a model reads. */
10
+ const INPUT_MODES = {
11
+ image: MODEL_IMAGE_TYPES,
12
+ pdf: ["application/pdf"],
13
+ office: [
14
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
15
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
16
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
17
+ ],
18
+ audio: ["audio/*"],
19
+ video: ["video/*"],
20
+ };
21
+ /**
22
+ * The input modes for a model that reads `inputs`: text and JSON, then the media types of the
23
+ * files it reads. No inputs known (no model in the vault): text and JSON.
24
+ */
25
+ export function inputModesFor(inputs) {
26
+ return [...MODES, ...(inputs ?? []).flatMap((input) => (input === "text" ? [] : INPUT_MODES[input]))];
27
+ }
28
+ /**
29
+ * `inputModes`: the media types the agent takes, when its model reads files; default text and
30
+ * JSON. The output modes stay text and JSON.
31
+ */
32
+ export function agentCard(agentId, definition, inputModes = MODES) {
9
33
  const { manifest } = definition;
10
34
  const name = typeof manifest.name === "string" && manifest.name !== "" ? manifest.name : agentId;
11
35
  const description = typeof manifest.description === "string" && manifest.description !== ""
@@ -17,7 +41,7 @@ export function agentCard(agentId, definition) {
17
41
  supportedInterfaces: [],
18
42
  version: definition.implementationVersion,
19
43
  capabilities: { streaming: false, pushNotifications: false, extendedAgentCard: false },
20
- defaultInputModes: MODES,
44
+ defaultInputModes: [...inputModes],
21
45
  defaultOutputModes: MODES,
22
46
  skills: [{ id: agentId, name, description, tags: [] }],
23
47
  };
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { readText } from "../http/body.js";
3
+ import { INLINE_BODY_MAX_BYTES } from "../inbound-files.js";
3
4
  import { TraceHeaders, tenantRoute } from "../http/define.js";
4
5
  import { traceContextOf } from "../../traces/ids.js";
5
6
  import { jsonResponse } from "../http/respond.js";
@@ -56,7 +57,7 @@ export function a2aRoutes(api) {
56
57
  path: "/v1/a2a/agents/{agentId}",
57
58
  tags: TAGS,
58
59
  summary: "Call an agent over A2A",
59
- description: "A2A 1.0 JSON-RPC, for a subject: `SendMessage` (blocking up to 5 minutes, or `returnImmediately`), `GetTask` and `CancelTask`. A context is one session per subject, agent and `contextId`; a task is one turn. JSON-RPC errors are answered with 200; limits, scopes and unavailable streams stay HTTP errors.",
60
+ description: "A2A 1.0 JSON-RPC, for a subject: `SendMessage` (blocking up to 5 minutes, or `returnImmediately`), `GetTask` and `CancelTask`. A message is text, one data part, or text and `raw` files (at most 20 MiB each, stored as artifacts of the context's session); a file `url` is `ContentTypeNotSupportedError`. A context is one session per subject, agent and `contextId`; a task is one turn. JSON-RPC errors are answered with 200; limits, scopes and unavailable streams stay HTTP errors.",
60
61
  externalDocs: A2A_SPECIFICATION,
61
62
  request: {
62
63
  params: agentId,
@@ -77,7 +78,8 @@ export function a2aRoutes(api) {
77
78
  const version = c.env.incoming.headers["a2a-version"];
78
79
  const trace = traceContextOf(c.req.raw.headers);
79
80
  return jsonResponse(200, await a2aCall(c.env.tenant, scope, c.req.param("agentId"), {
80
- body: () => readText(c.req.raw),
81
+ // A message may carry files inline.
82
+ body: () => readText(c.req.raw, INLINE_BODY_MAX_BYTES),
81
83
  version: typeof version === "string" ? version : (c.req.query("A2A-Version") ?? null),
82
84
  ...(trace ? { trace } : {}),
83
85
  }, c.env.outgoing));
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * A2A 1.0 over JSON-RPC: the wire types the Runtime answers with, the envelope, the error
3
3
  * codes and the parameter checks (Host feature `a2a-endpoint`). Only what v1 serves is typed:
4
- * text and data parts, tasks, the card. Field names are the spec's ProtoJSON (camelCase,
4
+ * text, data and file parts, tasks, the card. Field names are the spec's ProtoJSON (camelCase,
5
5
  * enum values such as `TASK_STATE_WORKING`). No Tenant state is read here.
6
6
  */
7
7
  /** The A2A version this endpoint speaks (`A2A-Version`, Major.Minor). */
@@ -16,6 +16,16 @@ export type Part = {
16
16
  data: unknown;
17
17
  mediaType?: string;
18
18
  metadata?: Record<string, unknown>;
19
+ } | {
20
+ raw: string;
21
+ mediaType?: string;
22
+ filename?: string;
23
+ metadata?: Record<string, unknown>;
24
+ } | {
25
+ url: string;
26
+ mediaType?: string;
27
+ filename?: string;
28
+ metadata?: Record<string, unknown>;
19
29
  };
20
30
  export interface Message {
21
31
  messageId: string;
@@ -120,11 +130,24 @@ export declare function parseEnvelope(text: string): {
120
130
  };
121
131
  /** `A2A-Version` as Major.Minor; empty means 0.3 (spec §3.6.2). */
122
132
  export declare function checkVersion(value: string | null | undefined): void;
123
- /** A client's message reduced to what the Runtime runs: text, or one JSON value. */
133
+ /** A file sent inline (`raw`, base64): stored as an artifact of the context's session. */
134
+ export interface RawFile {
135
+ readonly raw: string;
136
+ readonly mediaType?: string;
137
+ readonly filename?: string;
138
+ }
139
+ /**
140
+ * A client's message reduced to what the Runtime runs: text, one JSON value, or text and files
141
+ * sent inline.
142
+ */
124
143
  export type MessageInput = {
125
144
  content: string;
126
145
  } | {
127
146
  data: unknown;
147
+ } | {
148
+ files: readonly ({
149
+ text: string;
150
+ } | RawFile)[];
128
151
  };
129
152
  export interface SendParams {
130
153
  messageId: string;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * A2A 1.0 over JSON-RPC: the wire types the Runtime answers with, the envelope, the error
3
3
  * codes and the parameter checks (Host feature `a2a-endpoint`). Only what v1 serves is typed:
4
- * text and data parts, tasks, the card. Field names are the spec's ProtoJSON (camelCase,
4
+ * text, data and file parts, tasks, the card. Field names are the spec's ProtoJSON (camelCase,
5
5
  * enum values such as `TASK_STATE_WORKING`). No Tenant state is read here.
6
6
  */
7
7
  /** The A2A version this endpoint speaks (`A2A-Version`, Major.Minor). */
@@ -136,7 +136,12 @@ function partKind(part, field) {
136
136
  return a2aFail("invalidParams", `${field} must contain exactly one of text, raw, url, data`, {}, field);
137
137
  if (kinds[0] === "text" && typeof part.text !== "string")
138
138
  return a2aFail("invalidParams", `${field}.text must be a string`, {}, `${field}.text`);
139
- return kinds[0] === "raw" || kinds[0] === "url" ? "file" : kinds[0];
139
+ if (kinds[0] === "raw" && typeof part.raw !== "string")
140
+ return a2aFail("invalidParams", `${field}.raw must be base64 bytes`, {}, `${field}.raw`);
141
+ for (const key of ["mediaType", "filename"])
142
+ if (part[key] !== undefined && typeof part[key] !== "string")
143
+ return a2aFail("invalidParams", `${field}.${key} must be a string`, {}, `${field}.${key}`);
144
+ return kinds[0] === "raw" ? "raw" : kinds[0] === "url" ? "url" : kinds[0];
140
145
  }
141
146
  /** `SendMessage` params (spec §3.2.1), checked in the order a client can fix them. */
142
147
  export function parseSendParams(params) {
@@ -166,8 +171,20 @@ export function parseSendParams(params) {
166
171
  input = { content: parts.map((part) => part.text).join("\n") };
167
172
  else if (kinds.length === 1 && kinds[0] === "data")
168
173
  input = { data: parts[0].data };
174
+ else if (kinds.includes("url"))
175
+ return a2aFail("contentTypeNotSupported", "File URLs are not fetched yet; send the bytes inline as raw", { accepted: "raw" });
176
+ else if (kinds.includes("data"))
177
+ return a2aFail("contentTypeNotSupported", "This agent accepts text and file parts, or a single data part", { accepted: "text/plain,application/json" });
169
178
  else
170
- return a2aFail("contentTypeNotSupported", "This agent accepts text parts, or a single data part; files are not supported", { accepted: "text/plain,application/json" });
179
+ input = {
180
+ files: parts.map((part, index) => kinds[index] === "text"
181
+ ? { text: part.text }
182
+ : {
183
+ raw: part.raw,
184
+ ...(part.mediaType !== undefined ? { mediaType: part.mediaType } : {}),
185
+ ...(part.filename !== undefined ? { filename: part.filename } : {}),
186
+ }),
187
+ };
171
188
  if ("content" in input && input.content.trim() === "")
172
189
  a2aFail("invalidParams", "The message has no text", {}, "message.parts");
173
190
  return {
@@ -6,7 +6,10 @@ import type { TraceContext } from "../../traces/ids.js";
6
6
  * task is returned `WORKING` and keeps running; the caller polls `GetTask`.
7
7
  */
8
8
  export declare const A2A_BLOCKING_WAIT_MS: number;
9
- /** `GET /v1/a2a/agents/:agent/card`: the agent's card, for a caller who may use the agent. */
9
+ /**
10
+ * `GET /v1/a2a/agents/:agent/card`: the agent's card, for a caller who may use the agent. Its
11
+ * input modes name the files the Tenant's model reads.
12
+ */
10
13
  export declare function a2aCard(ctx: TenantContext, scope: AuthScope, agentId: string): Promise<unknown>;
11
14
  /**
12
15
  * `POST /v1/a2a/agents/:agent`: one A2A JSON-RPC call, for a subject. JSON-RPC answers (errors
@@ -9,13 +9,20 @@
9
9
  * caller's scope (limits and owner checks apply), sessions derived from the caller's own
10
10
  * subject, history from Durable Streams. A task id from the client only selects a context and
11
11
  * a turn within the caller's own sessions, so another subject's task is `TaskNotFoundError`.
12
+ *
13
+ * A message's `raw` file parts are stored as artifacts of the context's session once it is open,
14
+ * and the message goes in as `parts` (`../inbound-files.ts`): a file the model does not read is
15
+ * `ContentTypeNotSupportedError`, and the files of a refused message are deleted.
12
16
  */
13
17
  import { randomUUID } from "node:crypto";
14
18
  import { SessionCommandSchema, } from "@nylorun/core/contracts";
15
19
  import { commandKey } from "../../core/flow-host.js";
16
- import { agentCard } from "./card.js";
20
+ import { contentTypeFor } from "../../artifacts/media-types.js";
21
+ import { discardFiles, inlineFileName, messageParts, } from "../inbound-files.js";
22
+ import { agentCard, inputModesFor } from "./card.js";
17
23
  import { A2aError, a2aFail, checkVersion, jsonRpcError, jsonRpcResult, parseEnvelope, parseSendParams, parseTaskParams, } from "./protocol.js";
18
24
  import { SETTLED_EVENTS, buildTask, contextSessionId, newContextId, parseTaskId, pendingInteraction, taskIdOf, } from "./tasks.js";
25
+ import { currentModelInputs } from "../../model/catalog.js";
19
26
  import { accessOf } from "../../tenant/auth.js";
20
27
  import { command } from "../../tenant/commands.js";
21
28
  import { loadSession, } from "../../tenant/context.js";
@@ -27,9 +34,14 @@ import { putSession } from "../../tenant/sessions.js";
27
34
  * task is returned `WORKING` and keeps running; the caller polls `GetTask`.
28
35
  */
29
36
  export const A2A_BLOCKING_WAIT_MS = 5 * 60_000;
30
- /** `GET /v1/a2a/agents/:agent/card`: the agent's card, for a caller who may use the agent. */
37
+ /**
38
+ * `GET /v1/a2a/agents/:agent/card`: the agent's card, for a caller who may use the agent. Its
39
+ * input modes name the files the Tenant's model reads.
40
+ */
31
41
  export async function a2aCard(ctx, scope, agentId) {
32
- return agentCard(agentId, await definitionFor(ctx, agentId, accessOf(scope)));
42
+ const definition = await definitionFor(ctx, agentId, accessOf(scope));
43
+ // The files a caller may send: those the Tenant's model reads now.
44
+ return agentCard(agentId, definition, inputModesFor(await currentModelInputs(ctx)));
33
45
  }
34
46
  /**
35
47
  * `POST /v1/a2a/agents/:agent`: one A2A JSON-RPC call, for a subject. JSON-RPC answers (errors
@@ -60,7 +72,7 @@ export async function a2aCall(ctx, scope, agentId, input, response) {
60
72
  if (error instanceof A2aError)
61
73
  return jsonRpcError(id, error);
62
74
  // Limits, scopes and unavailable streams stay HTTP answers, as on every Tenant route.
63
- if (error instanceof HttpError && [403, 429, 503].includes(error.status))
75
+ if (error instanceof HttpError && [403, 413, 429, 503].includes(error.status))
64
76
  throw error;
65
77
  ctx.config.logger.warn("a2a request failed", {
66
78
  agentId,
@@ -141,7 +153,14 @@ conflict) {
141
153
  return { turnId: accepted.turnId, cursor: accepted.cursor };
142
154
  }
143
155
  catch (error) {
144
- if (!(error instanceof HttpError) || error.status !== 409)
156
+ if (!(error instanceof HttpError))
157
+ throw error;
158
+ // A file the agent's model does not read (`attachment_unsupported`).
159
+ if (error.status === 422)
160
+ return a2aFail("contentTypeNotSupported", error.message, {
161
+ reason: error.rejection.code ?? "request_rejected",
162
+ });
163
+ if (error.status !== 409)
145
164
  throw error;
146
165
  if (error.message.startsWith("Idempotency key"))
147
166
  return a2aFail("invalidParams", "message.messageId was already used for a different message", {}, "message.messageId");
@@ -162,13 +181,17 @@ async function openContext(caller, sessionId) {
162
181
  async function sendMessage(caller, params, signal) {
163
182
  const send = parseSendParams(params);
164
183
  const idempotencyKey = `a2a:${send.messageId}`;
165
- const value = "content" in send.input ? send.input.content : send.input.data;
166
184
  let contextId;
167
185
  let sessionId;
168
186
  let accepted;
169
187
  if (send.taskId !== undefined) {
170
188
  // A reply on an existing task: only a question it is waiting on can take it.
171
189
  const ids = parseTaskId(send.taskId) ?? taskNotFound(send.taskId);
190
+ const value = "content" in send.input
191
+ ? send.input.content
192
+ : "data" in send.input
193
+ ? send.input.data
194
+ : a2aFail("contentTypeNotSupported", "A reply takes text or one data part; send files in a new message", { accepted: "text/plain,application/json" });
172
195
  if (send.contextId !== undefined && send.contextId !== ids.contextId)
173
196
  a2aFail("invalidParams", "message.contextId does not match the task's context", {}, "message.contextId");
174
197
  contextId = ids.contextId;
@@ -198,17 +221,21 @@ async function sendMessage(caller, params, signal) {
198
221
  sessionId = contextSessionId(caller.access.owner, caller.agentId, contextId);
199
222
  await openContext(caller, sessionId);
200
223
  const context = contextId;
224
+ const { body, stored } = await messageBody(caller, sessionId, idempotencyKey, send.input);
201
225
  accepted = await run(caller, sessionId, {
202
226
  type: "message",
203
227
  requestId: randomUUID(),
204
228
  idempotencyKey,
205
- ...send.input,
229
+ ...body,
206
230
  }, async () => {
207
231
  const active = (await contextSession(caller, sessionId))?.activeTurnId;
208
232
  const taskId = active ? taskIdOf(context, active) : undefined;
209
233
  return taskId
210
234
  ? new A2aError("unsupportedOperation", `The context has a task in progress (${taskId}); reply to it or cancel it`, { taskId })
211
235
  : new A2aError("unsupportedOperation", "The context has unresolved work; start a new context");
236
+ }).catch(async (error) => {
237
+ await discardFiles(caller.ctx, stored, caller.access);
238
+ throw error;
212
239
  });
213
240
  }
214
241
  if (!send.returnImmediately)
@@ -216,6 +243,37 @@ async function sendMessage(caller, params, signal) {
216
243
  const { task } = await ownedTask(caller, taskIdOf(contextId, accepted.turnId), send.historyLength);
217
244
  return { task };
218
245
  }
246
+ /**
247
+ * A new message's body: its text, its JSON value, or parts with its `raw` files stored as
248
+ * artifacts of the session (`stored`: the ones this call created). A file the Runtime cannot
249
+ * store is `InvalidParamsError`; one past a limit stays an HTTP `413`.
250
+ */
251
+ async function messageBody(caller, sessionId, idempotencyKey, input) {
252
+ if (!("files" in input))
253
+ return { body: input, stored: [] };
254
+ let files = 0;
255
+ const inbound = input.files.map((part) => {
256
+ if ("text" in part)
257
+ return { type: "text", text: part.text };
258
+ const index = files++;
259
+ const mediaType = part.mediaType ?? (part.filename !== undefined ? contentTypeFor(part.filename) : "application/octet-stream");
260
+ return {
261
+ type: "inline",
262
+ name: part.filename ?? inlineFileName(mediaType, index),
263
+ mediaType,
264
+ data: part.raw,
265
+ };
266
+ });
267
+ try {
268
+ const { parts, stored } = await messageParts(caller.ctx, sessionId, idempotencyKey, inbound, caller.access);
269
+ return { body: { parts }, stored };
270
+ }
271
+ catch (error) {
272
+ if (error instanceof HttpError && error.status === 400)
273
+ return a2aFail("invalidParams", error.message, {}, "message.parts");
274
+ throw error;
275
+ }
276
+ }
219
277
  /** The interaction a reply answers: the task must be active and paused on a question. */
220
278
  async function replyTarget(caller, session, sessionId, turnId, taskId) {
221
279
  if (session.activeTurnId !== turnId) {
@@ -2,6 +2,7 @@ import { RESPONSE_ALREADY_SENT } from "@hono/node-server/utils/response";
2
2
  import { z } from "zod";
3
3
  import { EventSchemas, MessageSchema, RunAgentInputSchema } from "@ag-ui/core/schemas";
4
4
  import { readJson } from "../http/body.js";
5
+ import { INLINE_BODY_MAX_BYTES } from "../inbound-files.js";
5
6
  import { TraceHeaders, tenantRoute } from "../http/define.js";
6
7
  import { traceContextOf } from "../../traces/ids.js";
7
8
  import { jsonResponse } from "../http/respond.js";
@@ -50,7 +51,7 @@ export function agUiRoutes(api) {
50
51
  path: "/v1/ag-ui/agents/{agentId}",
51
52
  tags: TAGS,
52
53
  summary: "Run an agent",
53
- description: "For a person, named by a trusted issuer's token or by `Nylorun-Subject`. The thread is one session per person, agent and thread, created on its first run with `forwardedProps.nylorun.session`. The trailing user message starts a turn (its id is the idempotency key); `resume` answers interrupts. A busy session is a `RUN_ERROR` in the stream.",
54
+ description: "For a person, named by a trusted issuer's token or by `Nylorun-Subject`. The thread is one session per person, agent and thread, created on its first run with `forwardedProps.nylorun.session`. The trailing user message starts a turn (its id is the idempotency key); its media parts are files: bytes sent inline (`data`, at most 20 MiB each) are stored as artifacts of the session, a `file` source with provider `nylorun` names an artifact, and a URL is refused. `resume` answers interrupts. A busy session, or a file the model does not read, is a `RUN_ERROR` in the stream.",
54
55
  request: {
55
56
  params: agentId,
56
57
  body: {
@@ -62,7 +63,9 @@ export function agUiRoutes(api) {
62
63
  responses: { 200: events("The run's events, from RUN_STARTED to its end") },
63
64
  }, async (c) => {
64
65
  const { scope, subject, access } = callerOf(c);
65
- await startRun(c.env.tenant, scope, c.req.param("agentId"), { subject, access }, await readJson(c.req.raw), c.env.outgoing, traceContextOf(c.req.raw.headers));
66
+ await startRun(c.env.tenant, scope, c.req.param("agentId"), { subject, access },
67
+ // A message may carry files inline.
68
+ await readJson(c.req.raw, INLINE_BODY_MAX_BYTES), c.env.outgoing, traceContextOf(c.req.raw.headers));
66
69
  return RESPONSE_ALREADY_SENT;
67
70
  });
68
71
  tenantRoute(api, FOR_A_PERSON, {
@@ -1,7 +1,12 @@
1
1
  /**
2
2
  * An AG-UI transcript rebuilt from a session's log, with the ids a live run gave each
3
- * message (see `translate.ts`), usable as `HttpAgent`'s `initialMessages`.
3
+ * message (see `translate.ts`), usable as `HttpAgent`'s `initialMessages`. A user message with
4
+ * files has array content: its text parts, and each file as a media part whose source is the
5
+ * artifact (`{ type: "file", provider: "nylorun", value: <artifact id> }`), typed by the media
6
+ * type its `artifact.created` event recorded.
4
7
  */
5
8
  import type { Message } from "@ag-ui/core";
6
9
  import { type LiveEvent } from "@nylorun/core/contracts";
10
+ /** The `provider` of a file source that names one of the Tenant's artifacts. */
11
+ export declare const NYLORUN_FILE_PROVIDER = "nylorun";
7
12
  export declare function messagesFromEvents(items: readonly LiveEvent[]): Message[];
@@ -1,10 +1,44 @@
1
1
  import { isEmptyAssistantMessage, parseTranscriptEvent, } from "@nylorun/core/contracts";
2
2
  import { outputText, toolResultOf } from "./translate.js";
3
+ /** The `provider` of a file source that names one of the Tenant's artifacts. */
4
+ export const NYLORUN_FILE_PROVIDER = "nylorun";
5
+ /** A file part as an AG-UI media part, its kind from its media type when the log has it. */
6
+ function filePart(artifactId, mediaType) {
7
+ const top = mediaType?.split("/")[0];
8
+ const type = top === "image" || top === "audio" || top === "video" ? top : "document";
9
+ return {
10
+ type,
11
+ source: {
12
+ type: "file",
13
+ value: artifactId,
14
+ provider: NYLORUN_FILE_PROVIDER,
15
+ ...(mediaType ? { mimeType: mediaType } : {}),
16
+ },
17
+ };
18
+ }
19
+ /** A message's parts (protocol 6): its text, or text and media parts when it has files. */
20
+ function partsContent(parts, mediaTypes) {
21
+ if (!parts.some((part) => part.type === "file"))
22
+ return parts.flatMap((part) => (typeof part.text === "string" ? [part.text] : [])).join("\n");
23
+ return parts.flatMap((part) => part.type === "file" && typeof part.artifactId === "string"
24
+ ? [filePart(part.artifactId, mediaTypes.get(part.artifactId))]
25
+ : typeof part.text === "string"
26
+ ? [{ type: "text", text: part.text }]
27
+ : []);
28
+ }
3
29
  export function messagesFromEvents(items) {
4
30
  const messages = [];
5
31
  const results = new Set();
32
+ // Each artifact's media type, from its events, for the files a message names.
33
+ const mediaTypes = new Map();
6
34
  let sawAssistant = false;
7
35
  for (const raw of items) {
36
+ if (raw.type === "artifact.created" || raw.type === "artifact.version.created") {
37
+ const p = (raw.payload ?? {});
38
+ if (typeof p.artifactId === "string" && typeof p.contentType === "string")
39
+ mediaTypes.set(p.artifactId, p.contentType);
40
+ continue;
41
+ }
8
42
  if (raw.type === "command.message") {
9
43
  const p = (raw.payload ?? {});
10
44
  messages.push({
@@ -14,8 +48,7 @@ export function messagesFromEvents(items) {
14
48
  content: typeof p.content === "string"
15
49
  ? p.content
16
50
  : Array.isArray(p.parts)
17
- ? // A message with parts (protocol 6): its text; files are artifacts, not text.
18
- p.parts.flatMap((part) => (typeof part.text === "string" ? [part.text] : [])).join("\n")
51
+ ? partsContent(p.parts, mediaTypes)
19
52
  : JSON.stringify(p.data),
20
53
  });
21
54
  sawAssistant = false;