@nylorun/runtime 0.36.1-beta → 0.38.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 (71) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +10 -0
  3. package/dist/api/a2a/card.d.ts +10 -4
  4. package/dist/api/a2a/card.js +18 -6
  5. package/dist/api/a2a/endpoint.d.ts +5 -2
  6. package/dist/api/a2a/endpoint.js +31 -9
  7. package/dist/api/a2a/extensions.d.ts +41 -0
  8. package/dist/api/a2a/extensions.js +92 -0
  9. package/dist/api/a2a/list.d.ts +60 -0
  10. package/dist/api/a2a/list.js +169 -0
  11. package/dist/api/a2a/protocol.d.ts +105 -9
  12. package/dist/api/a2a/protocol.js +169 -18
  13. package/dist/api/a2a/push.d.ts +76 -0
  14. package/dist/api/a2a/push.js +305 -0
  15. package/dist/api/a2a/routes.d.ts +12 -6
  16. package/dist/api/a2a/routes.js +409 -112
  17. package/dist/api/a2a/tasks.d.ts +72 -4
  18. package/dist/api/a2a/tasks.js +305 -27
  19. package/dist/api/components.d.ts +538 -0
  20. package/dist/api/components.js +3 -1
  21. package/dist/api/http/app.js +2 -0
  22. package/dist/api/http/routes/artifacts.js +19 -0
  23. package/dist/api/http/routes/turns.d.ts +18 -0
  24. package/dist/api/http/routes/turns.js +67 -0
  25. package/dist/api/inbound-files.d.ts +12 -1
  26. package/dist/api/inbound-files.js +44 -0
  27. package/dist/api/openapi.js +7 -2
  28. package/dist/harness/image-engine.d.ts +14 -0
  29. package/dist/harness/image-engine.js +16 -0
  30. package/dist/harness/pod.d.ts +2 -0
  31. package/dist/harness/pod.js +5 -0
  32. package/dist/harness-api/ws-server.js +7 -1
  33. package/dist/host/config-for.d.ts +2 -0
  34. package/dist/host/config-for.js +1 -0
  35. package/dist/host/main.js +1 -0
  36. package/dist/host/stack-config.d.ts +16 -0
  37. package/dist/host/stack-config.js +30 -0
  38. package/dist/management-openapi.json +37 -2
  39. package/dist/openapi.json +1485 -738
  40. package/dist/reads/types.d.ts +19 -0
  41. package/dist/sandbox/join.d.ts +7 -1
  42. package/dist/sandbox/join.js +5 -1
  43. package/dist/sandbox/pods/builtin.d.ts +18 -0
  44. package/dist/sandbox/pods/builtin.js +33 -0
  45. package/dist/sandbox/pods/client.d.ts +5 -4
  46. package/dist/sandbox/pods/spec.js +14 -1
  47. package/dist/store/postgres/drizzle/0039_a2a_push.sql +32 -0
  48. package/dist/store/postgres/drizzle/0040_turn_index.sql +23 -0
  49. package/dist/store/postgres/drizzle/0041_turn_index_backfill.sql +88 -0
  50. package/dist/store/postgres/drizzle/0042_worker_engine_image.sql +1 -0
  51. package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
  52. package/dist/store/postgres/reads.js +41 -1
  53. package/dist/store/postgres/schema.d.ts +1314 -533
  54. package/dist/store/postgres/schema.js +94 -0
  55. package/dist/store/postgres/store.js +135 -3
  56. package/dist/store/postgres/turns.d.ts +62 -0
  57. package/dist/store/postgres/turns.js +153 -0
  58. package/dist/store/types.d.ts +82 -3
  59. package/dist/tenant/runtime.js +7 -5
  60. package/dist/tenant/session-model.d.ts +11 -1
  61. package/dist/tenant/session-model.js +34 -5
  62. package/dist/tenant/sessions.d.ts +2 -1
  63. package/dist/tenant/sessions.js +8 -2
  64. package/dist/tenant/sweep.d.ts +3 -0
  65. package/dist/tenant/sweep.js +39 -11
  66. package/dist/tenant/types.d.ts +23 -0
  67. package/dist/tuning.d.ts +9 -0
  68. package/dist/tuning.js +11 -0
  69. package/dist/version.d.ts +1 -1
  70. package/dist/version.js +1 -1
  71. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,75 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.38.0-beta
4
+
5
+ ### Minor Changes
6
+
7
+ - 63a6217: An image can carry the Runtime's engine (ADR-99). A pod of an image the Tenant lists in
8
+ `engine.images` (`PUT /v1/tenant/sandbox`) runs the engine under `/opt/nylorun` with nothing copied
9
+ in: the sandboxes service's new `entrypoint: "builtin"`, without the `nylo-copy` init container and
10
+ the `/nylo` volume. Such an engine reports its Runtime version at join (header
11
+ `nylorun-image-engine`), the Agent's record keeps it beside its pinned image digest, and a pod runs
12
+ it only while it is the Runtime's own version; otherwise the engine is copied in as for any image.
13
+ New column `agents.engine_image` (migration 0042).
14
+
15
+ ### Patch Changes
16
+
17
+ - Pin core to the tested release.
18
+ - Pin harness to the tested release.
19
+ - Updated dependencies [63a6217]
20
+ - Updated dependencies
21
+ - @nylorun/core@0.33.0-beta
22
+ - @nylorun/harness@0.30.1-beta
23
+
24
+ ## 0.37.0-beta
25
+
26
+ ### Minor Changes
27
+
28
+ - d48b6ef: The A2A endpoint serves the Nylorun A2A profile v2 (Host feature `a2a-profile-v2`, Engine R1).
29
+
30
+ - **Push notifications.** The four push config methods and `configuration.taskPushNotificationConfig` in `SendMessage`. Each task update is POSTed as one A2A `StreamResponse`, at least once, with `X-A2A-Notification-Token` and Standard Webhooks signatures (`webhook-id`, `webhook-timestamp`, `webhook-signature`) made with the new Runtime config `a2a.push.secret` (`NYLORUN_A2A_PUSH_SECRET`, `whsec_…`). Private hosts must be listed in `a2a.push.allowHosts` (`NYLORUN_A2A_PUSH_ALLOW_HOSTS`, `host:port` or `host:*`). Failed pushes are retried with backoff for 24 hours (Host tuning `a2a.pushRetry`, `a2a.pushMaxAgeMs`), then dropped. Configs and pushes live in two new tables (migration 0039).
31
+ - **Queued follow-ups.** A message to a busy context is queued and answered with the task that takes it (`TASK_STATE_SUBMITTED`); `metadata.delivery: "reject"` keeps `UnsupportedOperationError` with reason `CONTEXT_BUSY`.
32
+ - **Interactions.** Approvals and connection asks are `AUTH_REQUIRED`, questions `INPUT_REQUIRED`, shown under `interactions/v1`; an answer on the same task (`{ interactionId, approved }` or `{ interactionId, value }`) maps to `approve` or `respond`, with the caller as `approvedBy`. Text never approves.
33
+ - **Progress, telemetry, effects.** `plan.updated` events become `progress/v1`; the terminal status carries `telemetry/v1` (totals and trace id); an uncertain effect `effects/v1`.
34
+ - **Files.** `url` parts are fetched and stored like `raw` ones; the files a task writes are artifacts whose `url` points at the new `GET /v1/a2a/files/{artifactId}/versions/{version}`, served only to the same subject, with `files/v1`.
35
+ - **Extensions and card.** `A2A-Extensions` activates the five extensions and is echoed; data of the others is left out. The card lists the Runtime's JSON-RPC interface (A2A 1.0), `streaming: false`, `pushNotifications`, and the five extensions (`required: false`).
36
+ - ProtoJSON's original field names (`task_id`) are accepted in A2A params.
37
+
38
+ - d48b6ef: An agent manifest names the model its agent runs on (Host feature `agent-model`).
39
+
40
+ - **`harness.model`**: `<provider>/<model>`, such as `anthropic/claude-sonnet-4-5` (the provider's id, a `/`, then the model's id, which may hold a `/`; at most 200 characters). In an agent folder it is `model:` in `AGENT.md`'s frontmatter; the compiler refuses any other shape (`value_invalid`) and a `model` in an agent under `agents/`, which runs on the root agent's. `harness.manifest.json` takes it too, and the composer keeps it.
41
+ - **A new session starts on it.** A session opened on a version that names a model gets it as its own model (`GET /v1/sessions/{id}/model` answers `source: "session"`), as `PUT /v1/sessions/{id}/model` would choose it, when the provider is connected and its catalog lists the model. Otherwise the session has none and the Tenant's model applies; nothing fails. Sessions opened by the A2A server and by triggers get it too. An explicit choice still replaces it, and an open session keeps its model across versions.
42
+ - `agentModelChoice` and `AgentModelSchema` in `@nylorun/core/contracts`.
43
+ - The JSON Schema documents that gained the field are published at new ids: `agent-manifest/v8.1.json`, `harness-manifest/v1.1.json` and `agent-folder/v1/agent-md.1.json`; the old ids are retired. `manifestSchemaVersion` stays 8.
44
+
45
+ - d48b6ef: An agent keeps a durable plan (A2A profile v2, `progress/v1`; A2A-E10).
46
+
47
+ - **`plan.updated`**, a new durable session event: `{ steps: [{ label, state, note? }] }`, the agent's whole plan each time, never a diff. `state` is `pending`, `active`, `done`, `blocked` or `skipped`; at most 20 steps, labels of at most 120 and notes of at most 280 characters (`PLAN_LIMITS`, `PLAN_STEP_STATES`, `PlanUpdatedPayloadSchema` in `@nylorun/core/contracts`). The Runtime records and serves it like any public event, with the harness as its writer (evidence `claimed`), and the OpenAPI document lists it.
48
+ - **`update_plan`**, a built-in tool of our engine, off by default. A version opts in with `harness.loop.plan: true` (in an agent folder, `loop: { plan: true }` in `AGENT.md`'s frontmatter); the session's root agent then gets the tool, unless it declares its own tool of that name. A call takes `{ steps: [...] }`, cuts long labels and notes (ending in `…`) and steps past 20 rather than refusing them, claims one `plan.updated` and answers `Plan updated`. A malformed plan is a failed result the model sees.
49
+ - **Harness API**: `plan.updated` is a claim a harness may make (`HARNESS_CLAIMS`), additive within v5. The JSON Schema document is now published as `https://docs.nylorun.com/schemas/harness-api/v5.1.json`; `v5.json` is retired.
50
+ - The agent folder compiler takes `loop.plan` (true or false) and suggests it for a close typo.
51
+
52
+ - d48b6ef: The Runtime keeps a turn index and serves it as a change feed (Host feature `turns`, A2A-E1 to E3).
53
+
54
+ - **The turn index.** Every turn has a row: its session, Agent, status (`running`, `paused`, `completed`, `failed`, `cancelled`), start, end, last change, totals and, for an A2A context, its context and task id. It is written in the same transaction as the event that changes the turn, so it always agrees with the session's log. Migration `0041_turn_index_backfill` indexes the turns already recorded when the Runtime first starts on this version.
55
+ - **`GET /v1/turns`** pages the turns in the order they changed: pass the page's `cursor` as `changedAfter` to read on (a turn comes again each time it changes, and the cursor comes back even when nothing changed), `limit` up to 500 (default 100). `sessionId` reads one session's turns, oldest first. An application key reads the whole Tenant; acting for a person, only their sessions' turns. A client that missed pushes catches up with one call per interval.
56
+ - **A2A `ListTasks`** is served: the caller's tasks with the Agent, latest status change first, filtered by `contextId`, `status` and `statusTimestampAfter`, paged with `pageSize` (up to 100, default 50) and `pageToken`, with `totalSize`; each task as `GetTask` answers it (interaction, `progress/v1` and `telemetry/v1` metadata), with history only for `historyLength` and artifacts only for `includeArtifacts`. A paused task is `AUTH_REQUIRED` for an approval or a connection and `INPUT_REQUIRED` for a question, and the `status` filter agrees. `GetTask`, `CancelTask`, replies and push configs find a task (a queued message's id too, once a turn took it) through the index and read only that turn's events.
57
+ - **A2A contexts are listable.** The session of a context now carries `info.a2a` (`contextId` and the subject); a context opened by an earlier version gets it with its next message. Its session id is still the one-way hash of the subject, Agent and context.
58
+ - `@nylorun/core` adds `TurnSummarySchema`, `TurnChangesPageSchema` and `TURN_STATUSES`.
59
+
60
+ ### Patch Changes
61
+
62
+ - Pin core to the tested release.
63
+ - Pin harness to the tested release.
64
+ - Updated dependencies [d48b6ef]
65
+ - Updated dependencies [d48b6ef]
66
+ - Updated dependencies [d48b6ef]
67
+ - Updated dependencies [d48b6ef]
68
+ - Updated dependencies [d48b6ef]
69
+ - Updated dependencies
70
+ - @nylorun/core@0.32.0-beta
71
+ - @nylorun/harness@0.30.0-beta
72
+
3
73
  ## 0.36.1-beta
4
74
 
5
75
  ### Patch Changes
package/README.md CHANGED
@@ -340,6 +340,16 @@ requests without `limit` retain their response shapes. These are public APIs for
340
340
  CLI and custom clients; clients never need database access. See the
341
341
  [API contracts, authorization, SDK examples and Studio handoff](https://github.com/nylorun/agents/blob/main/runtime/docs/session-reads.md).
342
342
 
343
+ ## Turns
344
+
345
+ Hosts advertising `turns` keep a turn index, written with the events that change each turn, and
346
+ serve it as a change feed: `GET /v1/turns?changedAfter=<cursor>&limit=<1..500>` pages the turns a
347
+ caller may see in the order they changed (a turn again each time it changes), each with its
348
+ session, Agent, status, start and end, last change, totals and A2A context and task. Pass the
349
+ page's `cursor` as `changedAfter` to read on; `sessionId` reads one session's turns. An application
350
+ key reads the whole Tenant, a subject only its own sessions'. The A2A endpoint answers `ListTasks`
351
+ from the same index.
352
+
343
353
  ## Artifact pages
344
354
 
345
355
  Hosts advertising `artifact-reads` accept `GET /v1/artifacts?limit=50`, returning
@@ -1,8 +1,11 @@
1
1
  /**
2
2
  * The Agent Card of one Agent, from its manifest version: its id as the name, the manifest's
3
- * description and one skill, never its instructions, tools or MCP servers. The Runtime does not know where callers reach it, so
4
- * `supportedInterfaces` is empty: whoever publishes the card (the gateway in v1) adds its own
5
- * URL, provider and security schemes.
3
+ * description and one skill, never its instructions, tools or MCP servers. Its one interface is
4
+ * the Runtime's own JSON-RPC endpoint (A2A 1.0) at the URL the caller reached, or
5
+ * `NYLORUN_PUBLIC_URL`; a gateway that publishes the card (the SDK's) lists its own URL,
6
+ * provider and security schemes instead. Capabilities (profile v2 §3): no streaming, push
7
+ * notifications when the Runtime has a push secret, no extended card, and the profile's five
8
+ * extensions, all optional.
6
9
  */
7
10
  export interface CardDefinition {
8
11
  readonly manifest: {
@@ -23,4 +26,7 @@ export declare function inputModesFor(inputs: readonly ModelInput[] | undefined)
23
26
  * `inputModes`: the media types the agent takes, when its model reads files; default text and
24
27
  * JSON. The output modes stay text and JSON.
25
28
  */
26
- export declare function agentCard(agentId: string, definition: CardDefinition, inputModes?: readonly string[]): Record<string, unknown>;
29
+ export declare function agentCard(agentId: string, definition: CardDefinition, inputModes?: readonly string[], serving?: {
30
+ url?: string;
31
+ pushNotifications?: boolean;
32
+ }): Record<string, unknown>;
@@ -1,10 +1,15 @@
1
1
  /**
2
2
  * The Agent Card of one Agent, from its manifest version: its id as the name, the manifest's
3
- * description and one skill, never its instructions, tools or MCP servers. The Runtime does not know where callers reach it, so
4
- * `supportedInterfaces` is empty: whoever publishes the card (the gateway in v1) adds its own
5
- * URL, provider and security schemes.
3
+ * description and one skill, never its instructions, tools or MCP servers. Its one interface is
4
+ * the Runtime's own JSON-RPC endpoint (A2A 1.0) at the URL the caller reached, or
5
+ * `NYLORUN_PUBLIC_URL`; a gateway that publishes the card (the SDK's) lists its own URL,
6
+ * provider and security schemes instead. Capabilities (profile v2 §3): no streaming, push
7
+ * notifications when the Runtime has a push secret, no extended card, and the profile's five
8
+ * extensions, all optional.
6
9
  */
7
10
  import { MODEL_IMAGE_TYPES } from "@nylorun/core/contracts";
11
+ import { cardExtensions } from "./extensions.js";
12
+ import { A2A_VERSION } from "./protocol.js";
8
13
  const MODES = ["text/plain", "application/json"];
9
14
  /** The media types a card advertises for each file input a model reads. */
10
15
  const INPUT_MODES = {
@@ -29,7 +34,7 @@ export function inputModesFor(inputs) {
29
34
  * `inputModes`: the media types the agent takes, when its model reads files; default text and
30
35
  * JSON. The output modes stay text and JSON.
31
36
  */
32
- export function agentCard(agentId, definition, inputModes = MODES) {
37
+ export function agentCard(agentId, definition, inputModes = MODES, serving = {}) {
33
38
  const { manifest } = definition;
34
39
  const name = typeof manifest.name === "string" && manifest.name !== "" ? manifest.name : agentId;
35
40
  const description = typeof manifest.description === "string" && manifest.description !== ""
@@ -38,9 +43,16 @@ export function agentCard(agentId, definition, inputModes = MODES) {
38
43
  return {
39
44
  name,
40
45
  description,
41
- supportedInterfaces: [],
46
+ supportedInterfaces: serving.url
47
+ ? [{ url: serving.url, protocolBinding: "JSONRPC", protocolVersion: A2A_VERSION }]
48
+ : [],
42
49
  version: definition.version,
43
- capabilities: { streaming: false, pushNotifications: false, extendedAgentCard: false },
50
+ capabilities: {
51
+ streaming: false,
52
+ pushNotifications: serving.pushNotifications === true,
53
+ extendedAgentCard: false,
54
+ extensions: cardExtensions(),
55
+ },
44
56
  defaultInputModes: [...inputModes],
45
57
  defaultOutputModes: MODES,
46
58
  skills: [{ id: agentId, name, description, tags: [] }],
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * The A2A endpoint's routes (`/v1/a2a/agents/{agentId}[/card]`), declared for serving and for
3
- * the Runtime's OpenAPI document. What each does is in `routes.ts`. The JSON-RPC operations are
4
- * A2A's (specification 1.0); the document describes the envelope and links the specification.
3
+ * the Runtime's OpenAPI document. What each does is in `routes.ts`; the files tasks write are
4
+ * served at `/v1/a2a/files/…` (`../http/routes/artifacts.ts`). The JSON-RPC operations are A2A's
5
+ * (specification 1.0); the document describes the envelope and links the specification and the
6
+ * Nylorun profile v2. `A2A-Extensions` activates the profile's extensions, and the answer echoes
7
+ * the ones activated.
5
8
  */
6
9
  import type { OpenAPIHono } from "@hono/zod-openapi";
7
10
  import type { TenantEnv } from "../http/app.js";
@@ -4,6 +4,11 @@ import { TraceHeaders, tenantRoute } from "../http/define.js";
4
4
  import { traceContextOf } from "../../traces/ids.js";
5
5
  import { jsonResponse } from "../http/respond.js";
6
6
  import { a2aCall, a2aCard } from "./routes.js";
7
+ import { A2A_PROFILE_URL, extensionsHeader, parseExtensions } from "./extensions.js";
8
+ /** The Runtime's URL as a caller reaches it: `NYLORUN_PUBLIC_URL`, else the request's origin. */
9
+ function baseUrlOf(publicUrl, url) {
10
+ return publicUrl ?? new URL(url).origin;
11
+ }
7
12
  const A2A_SPECIFICATION = {
8
13
  description: "The A2A specification",
9
14
  url: "https://a2a-protocol.org/latest/specification/",
@@ -16,7 +21,9 @@ const JsonRpcRequest = z
16
21
  id: z.union([z.string(), z.number(), z.null()]).optional(),
17
22
  method: z
18
23
  .string()
19
- .meta({ description: "`SendMessage`, `GetTask` or `CancelTask`; others answer their A2A error" }),
24
+ .meta({
25
+ description: "`SendMessage`, `GetTask`, `CancelTask`, `CreateTaskPushNotificationConfig`, `GetTaskPushNotificationConfig`, `ListTaskPushNotificationConfigs`, `DeleteTaskPushNotificationConfig` or `ListTasks`; others answer their A2A error",
26
+ }),
20
27
  params: z.unknown().optional(),
21
28
  })
22
29
  .meta({ id: "A2aJsonRpcRequest" });
@@ -32,7 +39,15 @@ const JsonRpcResponse = z
32
39
  .meta({ id: "A2aJsonRpcResponse" });
33
40
  const AgentCard = z
34
41
  .looseObject({ name: z.string(), description: z.string().optional(), skills: z.array(z.unknown()).optional() })
35
- .meta({ id: "A2aAgentCard", description: "The agent's A2A card, without interfaces" });
42
+ .meta({
43
+ id: "A2aAgentCard",
44
+ description: "The agent's A2A card: this Runtime's JSON-RPC interface (A2A 1.0), `streaming: false`, `pushNotifications` when the Runtime has a push secret, and the Nylorun profile v2 extensions, all `required: false`",
45
+ });
46
+ const PROFILE = { description: "The Nylorun A2A profile v2", url: A2A_PROFILE_URL };
47
+ const ExtensionsHeader = z
48
+ .string()
49
+ .optional()
50
+ .meta({ description: "Comma-separated profile v2 extension URIs to activate; the answer echoes the activated ones" });
36
51
  export function a2aRoutes(api) {
37
52
  tenantRoute(api, {
38
53
  credentials: ["application", "subject", "token"],
@@ -42,21 +57,22 @@ export function a2aRoutes(api) {
42
57
  path: "/v1/a2a/agents/{agentId}/card",
43
58
  tags: TAGS,
44
59
  summary: "Get an agent's A2A card",
45
- externalDocs: A2A_SPECIFICATION,
60
+ externalDocs: PROFILE,
46
61
  request: { params: agentId },
47
62
  responses: {
48
63
  200: { description: "The card", content: { "application/json": { schema: AgentCard } } },
49
64
  },
50
65
  }, async (c) => {
51
66
  const scope = c.get("scope");
52
- return jsonResponse(200, await a2aCard(c.env.tenant, scope, c.req.param("agentId")));
67
+ const ctx = c.env.tenant;
68
+ return jsonResponse(200, await a2aCard(ctx, scope, c.req.param("agentId"), baseUrlOf(ctx.config.publicUrl, c.req.url)));
53
69
  });
54
70
  tenantRoute(api, { credentials: ["subject", "token"], scopes: ["sessions:own"] }, {
55
71
  method: "post",
56
72
  path: "/v1/a2a/agents/{agentId}",
57
73
  tags: TAGS,
58
74
  summary: "Call an agent over A2A",
59
- description: "A2A 1.0 JSON-RPC, for a subject: `SendMessage` (blocking up to the Runtime limit `a2aWaitMs`, 5 minutes by default, or `returnImmediately`), `GetTask` and `CancelTask`. A message is text, one data part, or text and `raw` files (at most the Runtime limit `inlineFileBytes` each, 20 MiB by default, 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.",
75
+ description: "A2A 1.0 JSON-RPC, for a subject, under the Nylorun profile v2 (Host feature `a2a-profile-v2`). `SendMessage` blocks up to the Runtime limit `a2aWaitMs` (5 minutes by default; then the task is answered still `WORKING`) unless `returnImmediately`; `GetTask` (with `historyLength`), `CancelTask` and `ListTasks`; and the four push notification config methods, plus `configuration.taskPushNotificationConfig` in `SendMessage`: each update is POSTed as one `StreamResponse`, signed with Standard Webhooks (`webhook-id`, `webhook-timestamp`, `webhook-signature` with the Runtime's `a2a.push.secret`) and `X-A2A-Notification-Token`, at least once, retried for up to 24 hours, to hosts the Runtime allows (`a2a.push.allowHosts` for private ones). A message to a busy context is queued and answered with the task that will take it (`TASK_STATE_SUBMITTED`); `message.metadata.delivery: \"reject\"` asks for `UnsupportedOperationError` (reason `CONTEXT_BUSY`) instead. A message is text, one data part, or text with `raw` files (stored as artifacts of the context's session) and `url` files (fetched by the Runtime), each at most the Runtime limit `inlineFileBytes` (20 MiB by default). An approval, question or connection the task waits for (`AUTH_REQUIRED` or `INPUT_REQUIRED`) is shown under `interactions/v1` and answered on the same task with `{ interactionId, approved }` or `{ interactionId, value }` under that URI in the message's `metadata`; text never approves. Artifacts carry the turn's output and the files it wrote, as `url` parts at `/v1/a2a/files/…` with `files/v1`; the task's metadata carries `progress/v1`, `telemetry/v1` and `effects/v1`. Extension data is answered only for the extensions activated with `A2A-Extensions`. 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. Not served: streaming, `GetExtendedAgentCard`.",
60
76
  externalDocs: A2A_SPECIFICATION,
61
77
  request: {
62
78
  params: agentId,
@@ -64,23 +80,29 @@ export function a2aRoutes(api) {
64
80
  "A2A-Version": z.string().optional().meta({ description: "Or the `A2A-Version` header: `1.0`" }),
65
81
  }),
66
82
  body: { required: true, content: { "application/json": { schema: JsonRpcRequest } } },
67
- headers: TraceHeaders,
83
+ headers: TraceHeaders.extend({ "A2A-Extensions": ExtensionsHeader }),
68
84
  },
69
85
  responses: {
70
86
  200: {
71
87
  description: "The JSON-RPC answer: a result, or an A2A error",
88
+ headers: z.object({ "A2A-Extensions": ExtensionsHeader }),
72
89
  content: { "application/json": { schema: JsonRpcResponse } },
73
90
  },
74
91
  },
75
92
  }, async (c) => {
76
93
  const scope = c.get("scope");
94
+ const ctx = c.env.tenant;
77
95
  const version = c.env.incoming.headers["a2a-version"];
96
+ const extensions = parseExtensions(c.req.header("a2a-extensions"));
78
97
  const trace = traceContextOf(c.req.raw.headers);
79
- return jsonResponse(200, await a2aCall(c.env.tenant, scope, c.req.param("agentId"), {
98
+ const echo = extensionsHeader(extensions);
99
+ return jsonResponse(200, await a2aCall(ctx, scope, c.req.param("agentId"), {
80
100
  // A message may carry files inline.
81
- body: async () => readText(c.req.raw, await bodyLimit(c.env.tenant, "inline")),
101
+ body: async () => readText(c.req.raw, await bodyLimit(ctx, "inline")),
82
102
  version: typeof version === "string" ? version : (c.req.query("A2A-Version") ?? null),
83
103
  ...(trace ? { trace } : {}),
84
- }, c.env.outgoing));
104
+ extensions,
105
+ baseUrl: baseUrlOf(ctx.config.publicUrl, c.req.url),
106
+ }, c.env.outgoing), echo ? { "A2A-Extensions": echo } : {});
85
107
  });
86
108
  }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The Nylorun A2A profile v2 (`docs.nylorun.com/a2a/profile/v2`, Host feature `a2a-profile-v2`):
3
+ * five extensions, each declared on the Agent Card with `required: false`. A client activates
4
+ * them with the `A2A-Extensions` request header (A2A §4.6); the Runtime echoes the ones it
5
+ * activated in the same header, and keeps the data of the others out of what it answers, so a
6
+ * client that activates none gets plain A2A 1.0. Data travels in `metadata` keyed by the URI.
7
+ */
8
+ /** The profile's page: every extension's card entry links it. */
9
+ export declare const A2A_PROFILE_URL = "https://docs.nylorun.com/a2a/profile/v2";
10
+ /** The extension URIs (exact strings; profile v2 §2). */
11
+ export declare const A2A_EXTENSIONS: {
12
+ /** An open interaction on a status message, and the caller's answer on a reply message. */
13
+ readonly interactions: "https://docs.nylorun.com/a2a/interactions/v1";
14
+ /** `uncertain`: the effect whose outcome was lost, on a status update. */
15
+ readonly effects: "https://docs.nylorun.com/a2a/effects/v1";
16
+ /** The agent's plan (`plan.updated`), on working status updates and the task. */
17
+ readonly progress: "https://docs.nylorun.com/a2a/progress/v1";
18
+ /** The turn's totals and trace id, on the terminal status update and the task. */
19
+ readonly telemetry: "https://docs.nylorun.com/a2a/telemetry/v1";
20
+ /** A file artifact's kind, path, version, size and `sha256`. */
21
+ readonly files: "https://docs.nylorun.com/a2a/files/v1";
22
+ };
23
+ export type A2aExtension = (typeof A2A_EXTENSIONS)[keyof typeof A2A_EXTENSIONS];
24
+ /** Every extension: what a push config registered without the header keeps. */
25
+ export declare const ALL_EXTENSIONS: ReadonlySet<string>;
26
+ /** The card's `capabilities.extensions`: all five, optional, linking the profile. */
27
+ export declare function cardExtensions(): Record<string, unknown>[];
28
+ /**
29
+ * The extensions an `A2A-Extensions` header activates: the comma-separated URIs this Runtime
30
+ * serves, in the profile's order. Unknown URIs are ignored (A2A §4.6: an agent activates the
31
+ * ones it supports).
32
+ */
33
+ export declare function parseExtensions(header: string | null | undefined): Set<string>;
34
+ /** The `A2A-Extensions` answer header for `active`, or undefined when none is. */
35
+ export declare function extensionsHeader(active: ReadonlySet<string>): string | undefined;
36
+ /**
37
+ * `value` without the data of the extensions `active` leaves out: their keys in every `metadata`
38
+ * object (an emptied one goes), and their URIs in every `extensions` list. Parts' `data` is the
39
+ * agent's or caller's own and is never read. Returns a copy.
40
+ */
41
+ export declare function keepExtensions<T>(value: T, active: ReadonlySet<string>): T;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * The Nylorun A2A profile v2 (`docs.nylorun.com/a2a/profile/v2`, Host feature `a2a-profile-v2`):
3
+ * five extensions, each declared on the Agent Card with `required: false`. A client activates
4
+ * them with the `A2A-Extensions` request header (A2A §4.6); the Runtime echoes the ones it
5
+ * activated in the same header, and keeps the data of the others out of what it answers, so a
6
+ * client that activates none gets plain A2A 1.0. Data travels in `metadata` keyed by the URI.
7
+ */
8
+ /** The profile's page: every extension's card entry links it. */
9
+ export const A2A_PROFILE_URL = "https://docs.nylorun.com/a2a/profile/v2";
10
+ /** The extension URIs (exact strings; profile v2 §2). */
11
+ export const A2A_EXTENSIONS = {
12
+ /** An open interaction on a status message, and the caller's answer on a reply message. */
13
+ interactions: "https://docs.nylorun.com/a2a/interactions/v1",
14
+ /** `uncertain`: the effect whose outcome was lost, on a status update. */
15
+ effects: "https://docs.nylorun.com/a2a/effects/v1",
16
+ /** The agent's plan (`plan.updated`), on working status updates and the task. */
17
+ progress: "https://docs.nylorun.com/a2a/progress/v1",
18
+ /** The turn's totals and trace id, on the terminal status update and the task. */
19
+ telemetry: "https://docs.nylorun.com/a2a/telemetry/v1",
20
+ /** A file artifact's kind, path, version, size and `sha256`. */
21
+ files: "https://docs.nylorun.com/a2a/files/v1",
22
+ };
23
+ const ALL = new Set(Object.values(A2A_EXTENSIONS));
24
+ /** Every extension: what a push config registered without the header keeps. */
25
+ export const ALL_EXTENSIONS = ALL;
26
+ const DESCRIPTIONS = {
27
+ [A2A_EXTENSIONS.interactions]: "Typed approvals, questions and connection requests on the status message, answered on the same task",
28
+ [A2A_EXTENSIONS.effects]: "The effect whose outcome was lost (`uncertain`) while the task stays working",
29
+ [A2A_EXTENSIONS.progress]: "The agent's plan: up to 20 steps, each with a label, a state and a note",
30
+ [A2A_EXTENSIONS.telemetry]: "The task's totals (steps, tokens, costUsd) and trace id, when it ends",
31
+ [A2A_EXTENSIONS.files]: "Each file artifact's kind, path, version, size and sha256",
32
+ };
33
+ /** The card's `capabilities.extensions`: all five, optional, linking the profile. */
34
+ export function cardExtensions() {
35
+ return Object.values(A2A_EXTENSIONS).map((uri) => ({
36
+ uri,
37
+ description: DESCRIPTIONS[uri],
38
+ required: false,
39
+ params: { profile: A2A_PROFILE_URL },
40
+ }));
41
+ }
42
+ /**
43
+ * The extensions an `A2A-Extensions` header activates: the comma-separated URIs this Runtime
44
+ * serves, in the profile's order. Unknown URIs are ignored (A2A §4.6: an agent activates the
45
+ * ones it supports).
46
+ */
47
+ export function parseExtensions(header) {
48
+ const asked = new Set((header ?? "")
49
+ .split(",")
50
+ .map((uri) => uri.trim())
51
+ .filter((uri) => uri !== ""));
52
+ return new Set(Object.values(A2A_EXTENSIONS).filter((uri) => asked.has(uri)));
53
+ }
54
+ /** The `A2A-Extensions` answer header for `active`, or undefined when none is. */
55
+ export function extensionsHeader(active) {
56
+ return active.size === 0 ? undefined : [...active].join(", ");
57
+ }
58
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
59
+ /**
60
+ * `value` without the data of the extensions `active` leaves out: their keys in every `metadata`
61
+ * object (an emptied one goes), and their URIs in every `extensions` list. Parts' `data` is the
62
+ * agent's or caller's own and is never read. Returns a copy.
63
+ */
64
+ export function keepExtensions(value, active) {
65
+ if (active.size === ALL.size)
66
+ return value;
67
+ const walk = (node) => {
68
+ if (Array.isArray(node))
69
+ return node.map(walk);
70
+ if (!isObject(node))
71
+ return node;
72
+ const out = {};
73
+ for (const [key, child] of Object.entries(node)) {
74
+ if (key === "data")
75
+ out[key] = child;
76
+ else if (key === "metadata" && isObject(child)) {
77
+ const kept = Object.fromEntries(Object.entries(child).filter(([name]) => !ALL.has(name) || active.has(name)));
78
+ if (Object.keys(kept).length > 0)
79
+ out[key] = kept;
80
+ }
81
+ else if (key === "extensions" && Array.isArray(child)) {
82
+ const kept = child.filter((uri) => typeof uri !== "string" || !ALL.has(uri) || active.has(uri));
83
+ if (kept.length > 0)
84
+ out[key] = kept;
85
+ }
86
+ else
87
+ out[key] = walk(child);
88
+ }
89
+ return out;
90
+ };
91
+ return walk(value);
92
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * A2A tasks read from the turn index (Host feature `turns`): `ListTasks` (A2A 1.0 §3.1.4) and
3
+ * the bounded reads of one turn's events that `GetTask`, `CancelTask` and replies use. A context
4
+ * is listable because `openContext` writes `info.a2a` on its session, which the index copies into
5
+ * each turn's row; nothing here scans a session's whole history.
6
+ *
7
+ * `ListTasks` is scoped before any lookup: the caller's own subject and this endpoint's Agent,
8
+ * A2A contexts only. Tasks come latest status change first; a page's token is the last task's
9
+ * position, bound to the caller and the filters. Each task is built as `GetTask` builds it, from
10
+ * its turn's own events (state, interaction, `progress/v1` and `telemetry/v1` metadata), with
11
+ * `history` only for `historyLength` and artifacts only for `includeArtifacts`; its
12
+ * `status.timestamp` is when its status last changed, which `statusTimestampAfter` compares with.
13
+ * A paused turn is `AUTH_REQUIRED` while it waits for an approval or a connection, and
14
+ * `INPUT_REQUIRED` for a question (the index's `waitingFor`). A queued message no turn took yet
15
+ * (`SUBMITTED`) is not a turn, so it is not listed.
16
+ */
17
+ import type { LiveEvent } from "@nylorun/core/contracts";
18
+ import type { TurnRow } from "../../store/types.js";
19
+ import { type Session, type SessionAccess, type TenantContext } from "../../tenant/context.js";
20
+ import { type Task, type TaskState } from "./protocol.js";
21
+ /** `ListTasks`' page size when the request names none, and the most it may ask. */
22
+ export declare const LIST_PAGE_DEFAULT = 50;
23
+ export declare const LIST_PAGE_MAX = 100;
24
+ /** `ListTasks` params, checked. */
25
+ export interface ListParams {
26
+ readonly contextId?: string;
27
+ readonly status?: TaskState;
28
+ /** ISO, normalized to the index's form (`toISOString`). */
29
+ readonly statusTimestampAfter?: string;
30
+ readonly pageSize: number;
31
+ readonly pageToken?: string;
32
+ readonly historyLength?: number;
33
+ readonly includeArtifacts: boolean;
34
+ }
35
+ /** `ListTasks` result (A2A 1.0): `nextPageToken` is empty on the last page. */
36
+ export interface ListTasksResult {
37
+ tasks: Task[];
38
+ nextPageToken: string;
39
+ pageSize: number;
40
+ totalSize: number;
41
+ }
42
+ /** `ListTasks` params (A2A 1.0 §3.1.4), checked in the order a client can fix them. */
43
+ export declare function parseListParams(params: Record<string, unknown>): ListParams;
44
+ /**
45
+ * One turn's events, in stream order, read from its first lifecycle event: up to its last one
46
+ * when it has ended, else to the stream's tail. Never the session's whole history.
47
+ */
48
+ export declare function indexedTurnEvents(ctx: TenantContext, row: TurnRow): Promise<LiveEvent[]>;
49
+ /** The events of a session's turn, from the turn index; none when the session never had it. */
50
+ export declare function turnEventsOf(ctx: TenantContext, sessionId: string, turnId: string): Promise<LiveEvent[]>;
51
+ /** The caller of `ListTasks`: one subject, with one Agent. */
52
+ export interface ListCaller {
53
+ readonly ctx: TenantContext;
54
+ readonly access: SessionAccess;
55
+ readonly agentId: string;
56
+ }
57
+ /** Builds one listed turn's task as `GetTask` does (`routes.ts`), or undefined when it has none. */
58
+ export type BuildListedTask = (row: TurnRow, session: Session, historyLength: number) => Promise<Task | undefined>;
59
+ /** `ListTasks`: the caller's tasks with this Agent, from the turn index. */
60
+ export declare function listTasks(caller: ListCaller, params: ListParams, build: BuildListedTask): Promise<ListTasksResult>;