@nylorun/runtime 0.36.1-beta → 0.37.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.
- package/CHANGELOG.md +49 -0
- package/README.md +10 -0
- package/dist/api/a2a/card.d.ts +10 -4
- package/dist/api/a2a/card.js +18 -6
- package/dist/api/a2a/endpoint.d.ts +5 -2
- package/dist/api/a2a/endpoint.js +31 -9
- package/dist/api/a2a/extensions.d.ts +41 -0
- package/dist/api/a2a/extensions.js +92 -0
- package/dist/api/a2a/list.d.ts +60 -0
- package/dist/api/a2a/list.js +169 -0
- package/dist/api/a2a/protocol.d.ts +105 -9
- package/dist/api/a2a/protocol.js +169 -18
- package/dist/api/a2a/push.d.ts +76 -0
- package/dist/api/a2a/push.js +305 -0
- package/dist/api/a2a/routes.d.ts +12 -6
- package/dist/api/a2a/routes.js +409 -112
- package/dist/api/a2a/tasks.d.ts +72 -4
- package/dist/api/a2a/tasks.js +305 -27
- package/dist/api/components.d.ts +532 -0
- package/dist/api/components.js +3 -1
- package/dist/api/http/app.js +2 -0
- package/dist/api/http/routes/artifacts.js +19 -0
- package/dist/api/http/routes/turns.d.ts +18 -0
- package/dist/api/http/routes/turns.js +67 -0
- package/dist/api/inbound-files.d.ts +12 -1
- package/dist/api/inbound-files.js +44 -0
- package/dist/api/openapi.js +7 -2
- package/dist/host/config-for.d.ts +2 -0
- package/dist/host/config-for.js +1 -0
- package/dist/host/main.js +1 -0
- package/dist/host/stack-config.d.ts +16 -0
- package/dist/host/stack-config.js +30 -0
- package/dist/management-openapi.json +5 -2
- package/dist/openapi.json +1485 -738
- package/dist/reads/types.d.ts +19 -0
- package/dist/store/postgres/drizzle/0039_a2a_push.sql +32 -0
- package/dist/store/postgres/drizzle/0040_turn_index.sql +23 -0
- package/dist/store/postgres/drizzle/0041_turn_index_backfill.sql +88 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +21 -0
- package/dist/store/postgres/reads.js +41 -1
- package/dist/store/postgres/schema.d.ts +1296 -534
- package/dist/store/postgres/schema.js +92 -0
- package/dist/store/postgres/store.js +133 -3
- package/dist/store/postgres/turns.d.ts +62 -0
- package/dist/store/postgres/turns.js +153 -0
- package/dist/store/types.d.ts +73 -3
- package/dist/tenant/runtime.js +7 -5
- package/dist/tenant/session-model.d.ts +11 -1
- package/dist/tenant/session-model.js +34 -5
- package/dist/tenant/sessions.d.ts +2 -1
- package/dist/tenant/sessions.js +8 -2
- package/dist/tenant/sweep.d.ts +3 -0
- package/dist/tenant/sweep.js +39 -11
- package/dist/tenant/types.d.ts +23 -0
- package/dist/tuning.d.ts +9 -0
- package/dist/tuning.js +11 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.37.0-beta
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d48b6ef: The A2A endpoint serves the Nylorun A2A profile v2 (Host feature `a2a-profile-v2`, Engine R1).
|
|
8
|
+
|
|
9
|
+
- **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).
|
|
10
|
+
- **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`.
|
|
11
|
+
- **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.
|
|
12
|
+
- **Progress, telemetry, effects.** `plan.updated` events become `progress/v1`; the terminal status carries `telemetry/v1` (totals and trace id); an uncertain effect `effects/v1`.
|
|
13
|
+
- **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`.
|
|
14
|
+
- **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`).
|
|
15
|
+
- ProtoJSON's original field names (`task_id`) are accepted in A2A params.
|
|
16
|
+
|
|
17
|
+
- d48b6ef: An agent manifest names the model its agent runs on (Host feature `agent-model`).
|
|
18
|
+
|
|
19
|
+
- **`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.
|
|
20
|
+
- **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.
|
|
21
|
+
- `agentModelChoice` and `AgentModelSchema` in `@nylorun/core/contracts`.
|
|
22
|
+
- 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.
|
|
23
|
+
|
|
24
|
+
- d48b6ef: An agent keeps a durable plan (A2A profile v2, `progress/v1`; A2A-E10).
|
|
25
|
+
|
|
26
|
+
- **`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.
|
|
27
|
+
- **`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.
|
|
28
|
+
- **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.
|
|
29
|
+
- The agent folder compiler takes `loop.plan` (true or false) and suggests it for a close typo.
|
|
30
|
+
|
|
31
|
+
- d48b6ef: The Runtime keeps a turn index and serves it as a change feed (Host feature `turns`, A2A-E1 to E3).
|
|
32
|
+
|
|
33
|
+
- **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.
|
|
34
|
+
- **`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.
|
|
35
|
+
- **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.
|
|
36
|
+
- **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.
|
|
37
|
+
- `@nylorun/core` adds `TurnSummarySchema`, `TurnChangesPageSchema` and `TURN_STATUSES`.
|
|
38
|
+
|
|
39
|
+
### Patch Changes
|
|
40
|
+
|
|
41
|
+
- Pin core to the tested release.
|
|
42
|
+
- Pin harness to the tested release.
|
|
43
|
+
- Updated dependencies [d48b6ef]
|
|
44
|
+
- Updated dependencies [d48b6ef]
|
|
45
|
+
- Updated dependencies [d48b6ef]
|
|
46
|
+
- Updated dependencies [d48b6ef]
|
|
47
|
+
- Updated dependencies [d48b6ef]
|
|
48
|
+
- Updated dependencies
|
|
49
|
+
- @nylorun/core@0.32.0-beta
|
|
50
|
+
- @nylorun/harness@0.30.0-beta
|
|
51
|
+
|
|
3
52
|
## 0.36.1-beta
|
|
4
53
|
|
|
5
54
|
### 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
|
package/dist/api/a2a/card.d.ts
CHANGED
|
@@ -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.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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[]
|
|
29
|
+
export declare function agentCard(agentId: string, definition: CardDefinition, inputModes?: readonly string[], serving?: {
|
|
30
|
+
url?: string;
|
|
31
|
+
pushNotifications?: boolean;
|
|
32
|
+
}): Record<string, unknown>;
|
package/dist/api/a2a/card.js
CHANGED
|
@@ -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.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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: {
|
|
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
|
|
4
|
-
*
|
|
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";
|
package/dist/api/a2a/endpoint.js
CHANGED
|
@@ -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({
|
|
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({
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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>;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { encodeCursor } from "../../record/index.js";
|
|
2
|
+
import { historyCursor, readCursor } from "../../reads/cursor.js";
|
|
3
|
+
import { ENDED_TURN_STATUSES } from "../../store/postgres/turns.js";
|
|
4
|
+
import { loadSession } from "../../tenant/context.js";
|
|
5
|
+
import { HttpError } from "../../tenant/http.js";
|
|
6
|
+
import { readHistory } from "../../tenant/session-streams.js";
|
|
7
|
+
import { a2aFail, parseHistoryLength } from "./protocol.js";
|
|
8
|
+
import { contextSessionId } from "./tasks.js";
|
|
9
|
+
/** `ListTasks`' page size when the request names none, and the most it may ask. */
|
|
10
|
+
export const LIST_PAGE_DEFAULT = 50;
|
|
11
|
+
export const LIST_PAGE_MAX = 100;
|
|
12
|
+
/**
|
|
13
|
+
* The turn index states each A2A state is (profile v2 §1): a paused turn by what its first open
|
|
14
|
+
* interaction asks for. `SUBMITTED` (a queued message), `REJECTED` and anything else: none.
|
|
15
|
+
*/
|
|
16
|
+
const TURN_STATES = {
|
|
17
|
+
TASK_STATE_WORKING: [{ status: "running" }],
|
|
18
|
+
TASK_STATE_INPUT_REQUIRED: [{ status: "paused", waitingFor: ["question"] }],
|
|
19
|
+
TASK_STATE_AUTH_REQUIRED: [{ status: "paused", waitingFor: ["approval", "connection"] }],
|
|
20
|
+
TASK_STATE_COMPLETED: [{ status: "completed" }],
|
|
21
|
+
TASK_STATE_FAILED: [{ status: "failed" }],
|
|
22
|
+
TASK_STATE_CANCELED: [{ status: "cancelled" }],
|
|
23
|
+
};
|
|
24
|
+
const TASK_STATES = [
|
|
25
|
+
"TASK_STATE_SUBMITTED",
|
|
26
|
+
"TASK_STATE_WORKING",
|
|
27
|
+
"TASK_STATE_INPUT_REQUIRED",
|
|
28
|
+
"TASK_STATE_AUTH_REQUIRED",
|
|
29
|
+
"TASK_STATE_COMPLETED",
|
|
30
|
+
"TASK_STATE_FAILED",
|
|
31
|
+
"TASK_STATE_CANCELED",
|
|
32
|
+
"TASK_STATE_REJECTED",
|
|
33
|
+
];
|
|
34
|
+
const VISIBLE = /^[\x21-\x7e]{1,200}$/;
|
|
35
|
+
const present = (value) => value !== undefined && value !== null && value !== "";
|
|
36
|
+
/** `ListTasks` params (A2A 1.0 §3.1.4), checked in the order a client can fix them. */
|
|
37
|
+
export function parseListParams(params) {
|
|
38
|
+
const { contextId, status, statusTimestampAfter, pageSize, pageToken, includeArtifacts } = params;
|
|
39
|
+
if (present(contextId) && (typeof contextId !== "string" || !VISIBLE.test(contextId)))
|
|
40
|
+
a2aFail("invalidParams", "contextId must be 1 to 200 visible ASCII characters", {}, "contextId");
|
|
41
|
+
if (present(status) &&
|
|
42
|
+
status !== "TASK_STATE_UNSPECIFIED" &&
|
|
43
|
+
!TASK_STATES.includes(status))
|
|
44
|
+
a2aFail("invalidParams", "status must be a TaskState", {}, "status");
|
|
45
|
+
let since;
|
|
46
|
+
if (present(statusTimestampAfter)) {
|
|
47
|
+
const time = typeof statusTimestampAfter === "string" &&
|
|
48
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/.test(statusTimestampAfter)
|
|
49
|
+
? new Date(statusTimestampAfter)
|
|
50
|
+
: undefined;
|
|
51
|
+
if (!time || Number.isNaN(time.getTime()))
|
|
52
|
+
a2aFail("invalidParams", "statusTimestampAfter must be an ISO 8601 timestamp", {}, "statusTimestampAfter");
|
|
53
|
+
since = time.toISOString();
|
|
54
|
+
}
|
|
55
|
+
if (present(pageSize) &&
|
|
56
|
+
(typeof pageSize !== "number" ||
|
|
57
|
+
!Number.isInteger(pageSize) ||
|
|
58
|
+
pageSize < 1 ||
|
|
59
|
+
pageSize > LIST_PAGE_MAX))
|
|
60
|
+
a2aFail("invalidParams", `pageSize must be an integer from 1 to ${LIST_PAGE_MAX}`, {}, "pageSize");
|
|
61
|
+
if (present(pageToken) && typeof pageToken !== "string")
|
|
62
|
+
a2aFail("invalidParams", "pageToken must be a string", {}, "pageToken");
|
|
63
|
+
if (includeArtifacts !== undefined && includeArtifacts !== null && typeof includeArtifacts !== "boolean")
|
|
64
|
+
a2aFail("invalidParams", "includeArtifacts must be a boolean", {}, "includeArtifacts");
|
|
65
|
+
const historyLength = parseHistoryLength(params.historyLength, "historyLength");
|
|
66
|
+
return {
|
|
67
|
+
...(present(contextId) ? { contextId: contextId } : {}),
|
|
68
|
+
...(present(status) && status !== "TASK_STATE_UNSPECIFIED" ? { status: status } : {}),
|
|
69
|
+
...(since !== undefined ? { statusTimestampAfter: since } : {}),
|
|
70
|
+
pageSize: present(pageSize) ? pageSize : LIST_PAGE_DEFAULT,
|
|
71
|
+
...(present(pageToken) ? { pageToken: pageToken } : {}),
|
|
72
|
+
...(historyLength !== undefined ? { historyLength } : {}),
|
|
73
|
+
includeArtifacts: includeArtifacts === true,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* One turn's events, in stream order, read from its first lifecycle event: up to its last one
|
|
78
|
+
* when it has ended, else to the stream's tail. Never the session's whole history.
|
|
79
|
+
*/
|
|
80
|
+
export async function indexedTurnEvents(ctx, row) {
|
|
81
|
+
const after = row.firstSeq > 0 ? encodeCursor(row.sessionId, row.firstSeq - 1) : undefined;
|
|
82
|
+
const { items } = ENDED_TURN_STATUSES.has(row.status)
|
|
83
|
+
? await readHistory(ctx, row.sessionId, after === undefined
|
|
84
|
+
? undefined
|
|
85
|
+
: historyCursor(ctx.config.tenantId, row.sessionId).encode(after), undefined, row.lastSeq - row.firstSeq + 1)
|
|
86
|
+
: await readHistory(ctx, row.sessionId, after, undefined);
|
|
87
|
+
return items.filter((event) => event.turnId === row.turnId);
|
|
88
|
+
}
|
|
89
|
+
/** The events of a session's turn, from the turn index; none when the session never had it. */
|
|
90
|
+
export async function turnEventsOf(ctx, sessionId, turnId) {
|
|
91
|
+
const row = await ctx.store.tx((t) => t.turn(sessionId, turnId));
|
|
92
|
+
return row ? indexedTurnEvents(ctx, row) : [];
|
|
93
|
+
}
|
|
94
|
+
/** `ListTasks`: the caller's tasks with this Agent, from the turn index. */
|
|
95
|
+
export async function listTasks(caller, params, build) {
|
|
96
|
+
const { ctx, access, agentId } = caller;
|
|
97
|
+
const owner = access.owner;
|
|
98
|
+
const token = readCursor(ctx.config.tenantId, "a2a-tasks", {
|
|
99
|
+
owner,
|
|
100
|
+
agentId,
|
|
101
|
+
contextId: params.contextId ?? null,
|
|
102
|
+
status: params.status ?? null,
|
|
103
|
+
statusTimestampAfter: params.statusTimestampAfter ?? null,
|
|
104
|
+
});
|
|
105
|
+
let key;
|
|
106
|
+
try {
|
|
107
|
+
key = token.decode(params.pageToken);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (!(error instanceof HttpError))
|
|
111
|
+
throw error;
|
|
112
|
+
return a2aFail("invalidParams", "pageToken is not one this request returned", {}, "pageToken");
|
|
113
|
+
}
|
|
114
|
+
if (key && (key.length !== 3 || key.some((part) => typeof part !== "string")))
|
|
115
|
+
a2aFail("invalidParams", "pageToken is not one this request returned", {}, "pageToken");
|
|
116
|
+
const states = params.status === undefined ? undefined : (TURN_STATES[params.status] ?? []);
|
|
117
|
+
const { turns, total } = await ctx.store.tx((t) => t.listTurns({
|
|
118
|
+
ownerUserId: owner,
|
|
119
|
+
agentId,
|
|
120
|
+
a2a: true,
|
|
121
|
+
...(params.contextId === undefined
|
|
122
|
+
? {}
|
|
123
|
+
: { sessionId: contextSessionId(owner, agentId, params.contextId) }),
|
|
124
|
+
...(states === undefined ? {} : { states }),
|
|
125
|
+
...(params.statusTimestampAfter === undefined ? {} : { changedSince: params.statusTimestampAfter }),
|
|
126
|
+
...(key ? { after: { changedAt: key[0], sessionId: key[1], turnId: key[2] } } : {}),
|
|
127
|
+
limit: params.pageSize + 1,
|
|
128
|
+
}));
|
|
129
|
+
const page = turns.slice(0, params.pageSize);
|
|
130
|
+
const last = page.at(-1);
|
|
131
|
+
const tasks = [];
|
|
132
|
+
const sessions = new Map();
|
|
133
|
+
for (const row of page) {
|
|
134
|
+
// Only a context the A2A server opened for this caller: its session id is the context's.
|
|
135
|
+
if (contextSessionId(owner, agentId, row.a2aContextId) !== row.sessionId)
|
|
136
|
+
continue;
|
|
137
|
+
if (!sessions.has(row.sessionId))
|
|
138
|
+
sessions.set(row.sessionId, await sessionOf(caller, row.sessionId));
|
|
139
|
+
const session = sessions.get(row.sessionId);
|
|
140
|
+
const built = session ? await build(row, session, params.historyLength ?? 0) : undefined;
|
|
141
|
+
if (!built)
|
|
142
|
+
continue;
|
|
143
|
+
const { artifacts, ...task } = built;
|
|
144
|
+
tasks.push({
|
|
145
|
+
...task,
|
|
146
|
+
status: { ...task.status, timestamp: row.changedAt },
|
|
147
|
+
...(params.includeArtifacts && artifacts ? { artifacts } : {}),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
tasks,
|
|
152
|
+
nextPageToken: turns.length > params.pageSize && last
|
|
153
|
+
? token.encode([last.changedAt, last.sessionId, last.turnId])
|
|
154
|
+
: "",
|
|
155
|
+
pageSize: params.pageSize,
|
|
156
|
+
totalSize: total,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/** The caller's session for a listed task, or undefined when it is gone. */
|
|
160
|
+
async function sessionOf(caller, sessionId) {
|
|
161
|
+
try {
|
|
162
|
+
return await loadSession(caller.ctx, sessionId, caller.access);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
if (error instanceof HttpError && error.status === 404)
|
|
166
|
+
return undefined;
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
}
|