@nylorun/agents 0.30.0-beta → 0.31.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 CHANGED
@@ -1,5 +1,34 @@
1
1
  # @nylorun/agents
2
2
 
3
+ ## 0.31.0-beta
4
+
5
+ ### Minor Changes
6
+
7
+ - d48b6ef: An A2A client for the Nylorun profile v2 (A2A-E12, E13, E16), on `@nylorun/agents/a2a` and the package root.
8
+
9
+ - **`createA2aClient({ url, key?, subject?, scopes?, headers?, fetch?, extensions?, cardUrl? })`**: `send`, `get`, `list`, `listAll`, `cancel`, `answer` (an interaction under `interactions/v1`), `card` and `call`, as A2A 1.0 JSON-RPC with `A2A-Version: 1.0` and the five profile extensions activated by `A2A-Extensions`. A subject is sent as `Nylorun-Subject` with `Nylorun-Scopes: sessions:own`. A JSON-RPC error is thrown as `A2aError` (`code`, `message`, `reason` from `google.rpc.ErrorInfo`); an HTTP refusal stays a `RuntimeError`.
10
+ - **`verifyA2aPush({ secret, headers, body, toleranceSeconds?, token? })`**: checks a push delivery's Standard Webhooks signature (`whsec_` secret, several signatures allowed, 300 s tolerance) with Web Crypto, and returns its `StreamResponse`; `A2aPushVerificationError` otherwise.
11
+ - **`A2A_EXTENSIONS`** and the readers `readInteraction`, `readProgress`, `readTelemetry`, `readEffects`, `readFiles` and `readExtension`, with the profile's types and the A2A 1.0 types (`Task`, `StreamResponse`, `AgentCard`, …; the root's namespace `A2a`).
12
+ - **`listTurnChanges({ baseUrl, key, changedAfter?, sessionId?, limit? })`**: the Runtime's change feed, `GET /v1/turns` (Host feature `turns`).
13
+ - **SSE idle timeout:** `session.observe` drops a connection that sends nothing, keepalives included, for `idleTimeoutMs` (default 45 s; `0` never) and reopens it from `Last-Event-ID`.
14
+
15
+ ### Patch Changes
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
+ - Pin core to the tested release.
25
+ - Updated dependencies [d48b6ef]
26
+ - Updated dependencies [d48b6ef]
27
+ - Updated dependencies [d48b6ef]
28
+ - Updated dependencies [d48b6ef]
29
+ - Updated dependencies [d48b6ef]
30
+ - @nylorun/core@0.32.0-beta
31
+
3
32
  ## 0.30.0-beta
4
33
 
5
34
  ### Minor Changes
package/README.md CHANGED
@@ -556,7 +556,7 @@ default release registry). Host contract:
556
556
  [HOST_CONTRACT.md](../../runtime/harness/HOST_CONTRACT.md). Vocabulary (turn loop vs
557
557
  workflow Loop): [runtime/harness/src/CONTEXT.md](../../runtime/harness/src/CONTEXT.md).
558
558
 
559
- Use `session.observe({ cursor, signal })` for resumable canonical events, `session.inspect()` for waiting/uncertain state, and `approve`, `respond` or `cancel` to answer or stop a turn. `cancel` takes an idempotency key; `approve(interactionId, approved)` and `respond(interactionId, value)` make one of their own when given none, so give them a stable one (`{ idempotencyKey }`) when you may retry. Retry the same semantic command with the same key. `ownerUserId` must come from trusted server authentication. Application credentials are not browser credentials; browser applications need an authorized backend. Definition authoring is browser-bundleable.
559
+ Use `session.observe({ cursor, signal })` for resumable canonical events (a connection that sends nothing, not even its 15-second keepalive, for `idleTimeoutMs`, default 45 s, is dropped and reopened from the last event's `Last-Event-ID`; `0` never times out), `session.inspect()` for waiting/uncertain state, and `approve`, `respond` or `cancel` to answer or stop a turn. `cancel` takes an idempotency key; `approve(interactionId, approved)` and `respond(interactionId, value)` make one of their own when given none, so give them a stable one (`{ idempotencyKey }`) when you may retry. Retry the same semantic command with the same key. `ownerUserId` must come from trusted server authentication. Application credentials are not browser credentials; browser applications need an authorized backend. Definition authoring is browser-bundleable.
560
560
 
561
561
  Connection defaults follow `resolveConnection` (options → environment → Project
562
562
  link). Agents do not hash the manifest; a session pins the manifest version its Agent runs
@@ -701,16 +701,144 @@ What the Runtime answers (Host feature `a2a-endpoint`):
701
701
  `url` is `ContentTypeNotSupportedError`; a file of any type is accepted, and
702
702
  the agent's harness decides how its model sees it.
703
703
  - Clients must send `A2A-Version: 1.0` (header or `?A2A-Version=`).
704
- - Not yet: `ListTasks`, streaming and `SubscribeToTask`
705
- (`UnsupportedOperationError`; the card says `streaming: false`), push
706
- notifications, the extended card, and approvals. An agent that pauses for an
707
- approval shows `INPUT_REQUIRED`, refuses a reply, and can be canceled, so do
708
- not publish agents whose tools the Tenant's tool policy asks about yet.
704
+ - Before Host feature `a2a-profile-v2`: no `ListTasks`, push notifications or
705
+ approvals. An agent that pauses for an approval shows `INPUT_REQUIRED`,
706
+ refuses a reply, and can be canceled, so on such a Runtime do not publish
707
+ agents whose tools the Tenant's tool policy asks about. Never: streaming and
708
+ `SubscribeToTask` (`UnsupportedOperationError`; the card says
709
+ `streaming: false`) and the extended card.
709
710
  - The handler calls the Runtime as the partner's subject with the scope
710
711
  `sessions:own` only, so one partner never reaches another's tasks. It sends
711
712
  no header the partner chose except `A2A-Version` and `A2A-Extensions`. Without
712
713
  `a2a-endpoint` on the Runtime it answers `502` (`runtime_feature_missing`).
713
714
 
715
+ ### Calling an agent: the A2A client
716
+
717
+ `createA2aClient` calls one A2A agent's JSON-RPC endpoint: a Runtime's
718
+ `/v1/a2a/agents/{agentId}`, a gateway's, or any A2A 1.0 agent. It sends
719
+ `A2A-Version: 1.0` and activates the
720
+ [Nylorun profile v2](https://docs.nylorun.com/a2a/profile/v2) extensions with
721
+ `A2A-Extensions` (all five by default). It is plain `fetch` and loads no A2A
722
+ package; it is also exported from the package root.
723
+
724
+ <!-- doc-test: typescript -->
725
+ ```ts
726
+ import { createA2aClient, readInteraction, readProgress } from "@nylorun/agents/a2a";
727
+
728
+ const a2a = createA2aClient({
729
+ url: "https://agents.example.com/v1/a2a/agents/coding",
730
+ key: process.env.NYLORUN_SERVER_KEY, // Authorization: Bearer
731
+ subject: "u:org_1:user_1", // Nylorun-Subject, with Nylorun-Scopes: sessions:own
732
+ });
733
+
734
+ const task = await a2a.send({
735
+ messageId: crypto.randomUUID(), // the idempotency key: a retry answers the same task
736
+ parts: [{ text: "Build the portal from the wikis" }],
737
+ returnImmediately: true,
738
+ push: { url: "https://bot.example.com/a2a/push", token: "per-task-token" },
739
+ });
740
+
741
+ const latest = await a2a.get(task.id, { historyLength: 0 });
742
+ console.log(readProgress(latest)?.steps);
743
+ const open = readInteraction(latest); // an approval pauses the task as AUTH_REQUIRED
744
+ if (open?.kind === "approval")
745
+ await a2a.answer({
746
+ taskId: latest.id,
747
+ contextId: latest.contextId,
748
+ interactionId: open.interactionId,
749
+ approved: true,
750
+ messageId: crypto.randomUUID(),
751
+ });
752
+ ```
753
+
754
+ | Method | A2A operation |
755
+ | --- | --- |
756
+ | `send({ messageId, contextId?, taskId?, parts, metadata?, extensions?, delivery?, push?, returnImmediately?, historyLength?, traceparent? })` | `SendMessage`, answered with the task. A message to a busy context is queued; `delivery: "reject"` asks for `CONTEXT_BUSY` instead. `traceparent` joins the task to your trace |
757
+ | `get(taskId, { historyLength? })` | `GetTask` |
758
+ | `list({ contextId?, status?, statusTimestampAfter?, pageSize?, pageToken? })`, `listAll(...)` | `ListTasks`: one page, or every task page after page |
759
+ | `cancel(taskId)` | `CancelTask` |
760
+ | `answer({ taskId, contextId, interactionId, approved? \| value?, messageId })` | Answers an open interaction under `interactions/v1`: `approved` for an approval or a connection, `value` for a question |
761
+ | `card()` | The Agent Card: `{url}/card` on a Runtime, else `{url}/.well-known/agent-card.json` (`cardUrl` overrides) |
762
+ | `call(method, params)` | Any other JSON-RPC method |
763
+
764
+ An error the agent answers in JSON-RPC is thrown as `A2aError` with its
765
+ `code` (e.g. `-32001`), `message` and `reason` from `google.rpc.ErrorInfo`
766
+ (e.g. `TASK_NOT_FOUND`, `CONTEXT_BUSY`). An HTTP refusal (`401`, an unknown
767
+ agent's `404`, `429`) is a `RuntimeError` with its `status`.
768
+
769
+ **Pushes.** Each delivery is a `POST` of one A2A `StreamResponse`, signed with
770
+ [Standard Webhooks](https://www.standardwebhooks.com) and the installation's
771
+ `whsec_` webhook secret. Verify the raw body before you parse it:
772
+
773
+ <!-- doc-test: typescript -->
774
+ ```ts
775
+ import { createServer } from "node:http";
776
+ import { A2aPushVerificationError, readFiles, readTelemetry, verifyA2aPush } from "@nylorun/agents/a2a";
777
+
778
+ createServer(async (req, res) => {
779
+ const chunks: Buffer[] = [];
780
+ for await (const chunk of req) chunks.push(chunk as Buffer);
781
+ try {
782
+ const update = await verifyA2aPush({
783
+ secret: process.env.A2A_PUSH_SECRET!, // whsec_…
784
+ headers: req.headers,
785
+ body: Buffer.concat(chunks).toString("utf8"),
786
+ token: "per-task-token", // optional: X-A2A-Notification-Token must match
787
+ });
788
+ // Order status updates by status.timestamp: a delivery may come twice or late.
789
+ if ("statusUpdate" in update) console.log(update.statusUpdate.status.state, readTelemetry(update));
790
+ if ("artifactUpdate" in update) console.log(readFiles(update));
791
+ res.writeHead(204).end();
792
+ } catch (error) {
793
+ res.writeHead(error instanceof A2aPushVerificationError ? 401 : 500).end();
794
+ }
795
+ }).listen(3000);
796
+ ```
797
+
798
+ `verifyA2aPush` checks `webhook-id`, `webhook-timestamp` (at most
799
+ `toleranceSeconds`, default 300, from now, either way) and every `v1,`
800
+ signature in `webhook-signature` (several are allowed, as during a rotation),
801
+ in constant time with Web Crypto, and returns the parsed `StreamResponse`. A
802
+ failure is `A2aPushVerificationError` with its `reason`.
803
+
804
+ **Profile v2 readers.** Each takes a task, a `StreamResponse`, a status
805
+ update, a status, a message or an artifact, and returns `undefined` (`[]` for
806
+ files) when the data is missing, as from an agent outside Nylorun:
807
+
808
+ | Reader | Extension (`A2A_EXTENSIONS`) | Returns |
809
+ | --- | --- | --- |
810
+ | `readInteraction` | `interactions` | `{ interactionId, kind, prompt?, detail? }`: `approval`, `question` or `connection` |
811
+ | `readProgress` | `progress` | `{ steps: [{ label, state, note? }] }`, the whole plan |
812
+ | `readTelemetry` | `telemetry` | `{ totals?: { steps, tokens, costUsd }, traceId? }`, on the terminal update |
813
+ | `readEffects` | `effects` | `{ uncertain? }`: the effect whose outcome was lost |
814
+ | `readFiles` | `files` | `[{ artifact, file: { kind, path?, version?, size?, sha256? } }]` |
815
+ | `readExtension(source, uri)` | any | The raw data under `uri` |
816
+
817
+ **The change feed.** `listTurnChanges` reads a Runtime's `GET /v1/turns`
818
+ (Host feature `turns`): the turns that changed after a cursor, in the order
819
+ they changed. Poll it with the last `cursor` to find what a lost push would
820
+ have said:
821
+
822
+ <!-- doc-test: typescript -->
823
+ ```ts
824
+ import { listTurnChanges } from "@nylorun/agents";
825
+
826
+ let cursor: string | undefined;
827
+ const page = await listTurnChanges({
828
+ baseUrl: "https://agents.example.com",
829
+ key: process.env.NYLORUN_SERVER_KEY!, // the whole Tenant; add `subject` for one subject's turns
830
+ ...(cursor ? { changedAfter: cursor } : {}),
831
+ limit: 200,
832
+ });
833
+ for (const turn of page.turns) console.log(turn.status, turn.a2a?.taskId, turn.totals?.costUsd);
834
+ cursor = page.cursor; // returned even when nothing changed
835
+ ```
836
+
837
+ The A2A 1.0 types (`Task`, `Message`, `Part`, `Artifact`,
838
+ `TaskStatusUpdateEvent`, `TaskArtifactUpdateEvent`, `StreamResponse`,
839
+ `AgentCard`, `ListTasksResult`, `TaskState`) are exported from
840
+ `@nylorun/agents/a2a`, and from the root as the namespace `A2a` (`A2a.Task`).
841
+
714
842
  ## Files: artifacts and message parts
715
843
 
716
844
  Files users upload or agents make are **artifacts** (protocol 6): an id, a name and
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The A2A client side of `@nylorun/agents/a2a`, also exported from the package root: it loads
3
+ * no Node-only module (the gateway's `toNodeListener` does).
4
+ */
5
+ export { A2A_VERSION, A2aError, createA2aClient, listTurnChanges, } from "./client.js";
6
+ export type { A2aAnswerOptions, A2aClient, A2aClientOptions, A2aListOptions, A2aSendOptions, ListTurnChangesOptions, TurnChange, TurnChangesPage, } from "./client.js";
7
+ export { A2aPushVerificationError, verifyA2aPush } from "./push.js";
8
+ export type { A2aPushFailure, PushHeaders, VerifyA2aPushOptions } from "./push.js";
9
+ export { A2A_EXTENSIONS, A2A_PROFILE_EXTENSIONS, readEffects, readExtension, readFiles, readInteraction, readProgress, readTelemetry, } from "./profile.js";
10
+ export type { A2aEffects, A2aExtensionUri, A2aFile, A2aFileArtifact, A2aInteraction, A2aPlanStep, A2aPlanStepState, A2aProgress, A2aTelemetry, } from "./profile.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The A2A client side of `@nylorun/agents/a2a`, also exported from the package root: it loads
3
+ * no Node-only module (the gateway's `toNodeListener` does).
4
+ */
5
+ export { A2A_VERSION, A2aError, createA2aClient, listTurnChanges, } from "./client.js";
6
+ export { A2aPushVerificationError, verifyA2aPush } from "./push.js";
7
+ export { A2A_EXTENSIONS, A2A_PROFILE_EXTENSIONS, readEffects, readExtension, readFiles, readInteraction, readProgress, readTelemetry, } from "./profile.js";
@@ -0,0 +1,178 @@
1
+ import { type SubjectScope } from "@nylorun/core/contracts";
2
+ import type { AgentCard, ListTasksResult, Metadata, Part, PushNotificationConfig, Task, TaskState } from "./types.js";
3
+ /** The A2A version the client speaks (`A2A-Version`). */
4
+ export declare const A2A_VERSION = "1.0";
5
+ /**
6
+ * An error the agent answered in JSON-RPC: `code` is the JSON-RPC or A2A code (e.g. `-32001`
7
+ * for a task that is not found), `reason` the `google.rpc.ErrorInfo` reason (e.g.
8
+ * `TASK_NOT_FOUND`, `CONTEXT_BUSY`). HTTP-level refusals (`401`, `404` for an unknown agent,
9
+ * `429`) are `RuntimeError`s, as everywhere in the SDK.
10
+ */
11
+ export declare class A2aError extends Error {
12
+ readonly code: number;
13
+ readonly reason: string | undefined;
14
+ /** `google.rpc.ErrorInfo` metadata. */
15
+ readonly metadata: Readonly<Record<string, string>>;
16
+ /** The error's `data` as sent. */
17
+ readonly data?: unknown;
18
+ constructor(code: number, message: string, reason: string | undefined,
19
+ /** `google.rpc.ErrorInfo` metadata. */
20
+ metadata?: Readonly<Record<string, string>>,
21
+ /** The error's `data` as sent. */
22
+ data?: unknown);
23
+ }
24
+ export interface A2aClientOptions {
25
+ /** The agent's A2A endpoint, e.g. `${runtimeUrl}/v1/a2a/agents/${agentId}`. */
26
+ url: string;
27
+ /** A Nylorun application key, sent as `Authorization: Bearer`. */
28
+ key?: string;
29
+ /**
30
+ * The subject the calls act for (`Nylorun-Subject`, with `Nylorun-Scopes`): its own
31
+ * contexts and tasks only. Required by the Runtime with an application key, e.g.
32
+ * `u:<org>:<user>`.
33
+ */
34
+ subject?: string;
35
+ /** The subject's scopes. Default `["sessions:own"]`. */
36
+ scopes?: readonly SubjectScope[];
37
+ /** Sent on every request, e.g. a gateway's partner key. */
38
+ headers?: Readonly<Record<string, string>>;
39
+ fetch?: typeof fetch;
40
+ /** The extensions to activate (`A2A-Extensions`). Default: all five of profile v2. */
41
+ extensions?: readonly string[];
42
+ /**
43
+ * Where the Agent Card is. Default: `${url}/card` for a Runtime's `/v1/a2a/agents/{id}`,
44
+ * else `${url}/.well-known/agent-card.json` (the gateway's).
45
+ */
46
+ cardUrl?: string;
47
+ }
48
+ export interface A2aSendOptions {
49
+ /** The idempotency key: a retried message answers with the same task. */
50
+ messageId: string;
51
+ /** Omit to start a new context. */
52
+ contextId?: string;
53
+ /** To continue an interrupted task. */
54
+ taskId?: string;
55
+ parts: Part[];
56
+ metadata?: Metadata;
57
+ /** The extension URIs whose data `metadata` carries (the message's `extensions`). */
58
+ extensions?: string[];
59
+ /**
60
+ * What a message to a busy context does: `queue` (profile v2's default) is answered with the
61
+ * task that will take it; `reject` is `A2aError` with reason `CONTEXT_BUSY`. Sent as
62
+ * `metadata.delivery`.
63
+ */
64
+ delivery?: "queue" | "reject";
65
+ /** Where the agent pushes this task's updates; verify each with `verifyA2aPush`. */
66
+ push?: PushNotificationConfig;
67
+ /** Answer at once with the task (`SUBMITTED` or `WORKING`) instead of waiting. */
68
+ returnImmediately?: boolean;
69
+ historyLength?: number;
70
+ acceptedOutputModes?: string[];
71
+ /** W3C trace context: the task joins the caller's trace. */
72
+ traceparent?: string;
73
+ signal?: AbortSignal;
74
+ }
75
+ export interface A2aListOptions {
76
+ contextId?: string;
77
+ status?: TaskState;
78
+ /** ISO 8601: tasks whose status changed after this time. */
79
+ statusTimestampAfter?: string;
80
+ /** At most 100; the agent's default is 50. */
81
+ pageSize?: number;
82
+ pageToken?: string;
83
+ historyLength?: number;
84
+ signal?: AbortSignal;
85
+ }
86
+ export interface A2aAnswerOptions {
87
+ taskId: string;
88
+ contextId: string;
89
+ /** The `interactionId` from `readInteraction`. */
90
+ interactionId: string;
91
+ /** For an approval. */
92
+ approved?: boolean;
93
+ /** For a question. */
94
+ value?: unknown;
95
+ messageId: string;
96
+ signal?: AbortSignal;
97
+ }
98
+ export interface A2aClient {
99
+ readonly url: string;
100
+ /** `SendMessage`: the task that runs (or will run) the message. */
101
+ send(options: A2aSendOptions): Promise<Task>;
102
+ /** `GetTask`. */
103
+ get(taskId: string, options?: {
104
+ historyLength?: number;
105
+ signal?: AbortSignal;
106
+ }): Promise<Task>;
107
+ /** `ListTasks`: one page of the caller's tasks, newest change first. */
108
+ list(options?: A2aListOptions): Promise<ListTasksResult>;
109
+ /** Every task `list` matches, page after page. */
110
+ listAll(options?: Omit<A2aListOptions, "pageToken">): AsyncGenerator<Task>;
111
+ /** `CancelTask`. */
112
+ cancel(taskId: string, options?: {
113
+ signal?: AbortSignal;
114
+ }): Promise<Task>;
115
+ /**
116
+ * Answers an open interaction (`interactions/v1`) on its task: `approved` for an approval or
117
+ * a connection, `value` for a question.
118
+ */
119
+ answer(options: A2aAnswerOptions): Promise<Task>;
120
+ /** The agent's card. */
121
+ card(options?: {
122
+ signal?: AbortSignal;
123
+ }): Promise<AgentCard>;
124
+ /** One JSON-RPC call, for operations the client has no method for. */
125
+ call<T = unknown>(method: string, params: Record<string, unknown>, options?: {
126
+ headers?: Readonly<Record<string, string>>;
127
+ signal?: AbortSignal;
128
+ }): Promise<T>;
129
+ }
130
+ export declare function createA2aClient(options: A2aClientOptions): A2aClient;
131
+ /** One turn as the change feed lists it (Host feature `turns`). */
132
+ export interface TurnChange {
133
+ turnId: string;
134
+ sessionId: string;
135
+ agentId: string;
136
+ status: "running" | "paused" | "completed" | "failed" | "cancelled";
137
+ startedAt: string;
138
+ endedAt: string | null;
139
+ /** When it last changed: a turn reappears each time it changes. */
140
+ changedAt: string;
141
+ totals: {
142
+ steps: number;
143
+ tokens: number;
144
+ costUsd: number;
145
+ } | null;
146
+ /** The A2A context and task it is, when it came in over A2A. */
147
+ a2a: {
148
+ contextId: string;
149
+ taskId: string;
150
+ } | null;
151
+ }
152
+ export interface TurnChangesPage {
153
+ turns: TurnChange[];
154
+ /** Pass as `changedAfter` next time; returned even when `turns` is empty. */
155
+ cursor: string;
156
+ hasMore: boolean;
157
+ }
158
+ export interface ListTurnChangesOptions {
159
+ /** The Runtime's URL, e.g. `https://agents.example`. */
160
+ baseUrl: string;
161
+ /** An application key sees the whole Tenant; with `subject`, only that subject's sessions. */
162
+ key: string;
163
+ subject?: string;
164
+ scopes?: readonly SubjectScope[];
165
+ /** The `cursor` of the previous page. Omit to start from the beginning. */
166
+ changedAfter?: string;
167
+ /** One session's turns instead of the feed. */
168
+ sessionId?: string;
169
+ /** 1 to 500; the Runtime's default is 100. */
170
+ limit?: number;
171
+ fetch?: typeof fetch;
172
+ signal?: AbortSignal;
173
+ }
174
+ /**
175
+ * `GET /v1/turns`: the turns that changed after `changedAfter`, in the order they changed. Call
176
+ * it on an interval with the last `cursor` to find what a lost push would have said.
177
+ */
178
+ export declare function listTurnChanges(options: ListTurnChangesOptions): Promise<TurnChangesPage>;
@@ -0,0 +1,227 @@
1
+ /**
2
+ * An A2A 1.0 client for one agent's JSON-RPC endpoint (a Nylorun Runtime's
3
+ * `/v1/a2a/agents/{agentId}`, the SDK gateway, or any A2A agent), with the Nylorun profile v2's
4
+ * extensions activated, and the Runtime's change feed (`GET /v1/turns`). Plain `fetch`: it loads
5
+ * no A2A package and needs no Runtime client.
6
+ */
7
+ import { SCOPES_HEADER, SUBJECT_HEADER } from "@nylorun/core/compatibility";
8
+ import { parseSubjectHeaders } from "@nylorun/core/contracts";
9
+ import { readBody, requestHeaders } from "@nylorun/core/transport";
10
+ import { RuntimeError } from "../http.js";
11
+ import { A2A_EXTENSIONS, A2A_PROFILE_EXTENSIONS } from "./profile.js";
12
+ /** The A2A version the client speaks (`A2A-Version`). */
13
+ export const A2A_VERSION = "1.0";
14
+ /**
15
+ * An error the agent answered in JSON-RPC: `code` is the JSON-RPC or A2A code (e.g. `-32001`
16
+ * for a task that is not found), `reason` the `google.rpc.ErrorInfo` reason (e.g.
17
+ * `TASK_NOT_FOUND`, `CONTEXT_BUSY`). HTTP-level refusals (`401`, `404` for an unknown agent,
18
+ * `429`) are `RuntimeError`s, as everywhere in the SDK.
19
+ */
20
+ export class A2aError extends Error {
21
+ code;
22
+ reason;
23
+ metadata;
24
+ data;
25
+ constructor(code, message, reason,
26
+ /** `google.rpc.ErrorInfo` metadata. */
27
+ metadata = {},
28
+ /** The error's `data` as sent. */
29
+ data) {
30
+ super(message);
31
+ this.code = code;
32
+ this.reason = reason;
33
+ this.metadata = metadata;
34
+ this.data = data;
35
+ this.name = "A2aError";
36
+ }
37
+ }
38
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
39
+ /** The `A2aError` a JSON-RPC error object stands for, or `undefined` when it isn't one. */
40
+ function errorOf(body) {
41
+ if (!isObject(body) || !isObject(body.error))
42
+ return undefined;
43
+ const { code, message, data } = body.error;
44
+ if (typeof code !== "number")
45
+ return undefined;
46
+ const info = Array.isArray(data)
47
+ ? data.find((entry) => isObject(entry) && entry["@type"] === "type.googleapis.com/google.rpc.ErrorInfo")
48
+ : undefined;
49
+ const metadata = {};
50
+ if (info && isObject(info.metadata))
51
+ for (const [name, value] of Object.entries(info.metadata))
52
+ if (typeof value === "string")
53
+ metadata[name] = value;
54
+ return new A2aError(code, typeof message === "string" ? message : `A2A error ${code}`, info && typeof info.reason === "string" ? info.reason : undefined, metadata, data);
55
+ }
56
+ function fetcherOf(fetcher) {
57
+ // Called as a method on a page's window would otherwise lose `this`.
58
+ return fetcher ?? ((input, init) => globalThis.fetch(input, init));
59
+ }
60
+ const RUNTIME_ENDPOINT = /\/v1\/a2a\/agents\/[^/]+$/;
61
+ export function createA2aClient(options) {
62
+ const url = options.url.replace(/\/+$/, "");
63
+ if (!/^https?:\/\//.test(url))
64
+ throw new TypeError("createA2aClient: url must be an http(s) URL");
65
+ const fetcher = fetcherOf(options.fetch);
66
+ const base = {
67
+ ...options.headers,
68
+ ...requestHeaders(options.key),
69
+ "A2A-Version": A2A_VERSION,
70
+ };
71
+ if (options.subject !== undefined) {
72
+ const parsed = parseSubjectHeaders(options.subject, [...(options.scopes ?? ["sessions:own"])].join(" "));
73
+ if (!parsed.ok)
74
+ throw new TypeError(parsed.message);
75
+ base[SUBJECT_HEADER] = options.subject;
76
+ base[SCOPES_HEADER] = [...parsed.scopes].join(" ");
77
+ }
78
+ const extensions = [...(options.extensions ?? A2A_PROFILE_EXTENSIONS)];
79
+ if (extensions.length > 0)
80
+ base["A2A-Extensions"] = extensions.join(", ");
81
+ const cardUrl = options.cardUrl ??
82
+ (RUNTIME_ENDPOINT.test(new URL(url).pathname) ? `${url}/card` : `${url}/.well-known/agent-card.json`);
83
+ async function call(method, params, init = {}) {
84
+ const response = await fetcher(url, {
85
+ method: "POST",
86
+ headers: { ...base, ...init.headers, "Content-Type": "application/json" },
87
+ body: JSON.stringify({ jsonrpc: "2.0", id: crypto.randomUUID(), method, params }),
88
+ redirect: "error",
89
+ ...(init.signal ? { signal: init.signal } : {}),
90
+ });
91
+ const body = await readBody(response);
92
+ // Some agents answer a JSON-RPC error with an HTTP error status: it is still an A2A error.
93
+ const error = errorOf(body);
94
+ if (error)
95
+ throw error;
96
+ if (!response.ok)
97
+ throw new RuntimeError(response.status, body);
98
+ if (!isObject(body) || !("result" in body))
99
+ throw new A2aError(-32603, "The agent's answer is not a JSON-RPC response", undefined, {}, body);
100
+ return body.result;
101
+ }
102
+ async function sendMessage(message, options) {
103
+ const configuration = {};
104
+ if (options.returnImmediately !== undefined)
105
+ configuration.returnImmediately = options.returnImmediately;
106
+ if (options.push)
107
+ configuration.taskPushNotificationConfig = { ...options.push };
108
+ if (options.historyLength !== undefined)
109
+ configuration.historyLength = options.historyLength;
110
+ if (options.acceptedOutputModes)
111
+ configuration.acceptedOutputModes = [...options.acceptedOutputModes];
112
+ const result = await call("SendMessage", { message, ...(Object.keys(configuration).length ? { configuration } : {}) }, {
113
+ ...(options.traceparent ? { headers: { traceparent: options.traceparent } } : {}),
114
+ ...(options.signal ? { signal: options.signal } : {}),
115
+ });
116
+ if (isObject(result) && isObject(result.task))
117
+ return result.task;
118
+ throw new A2aError(-32603, "The agent answered with a message, not a task", undefined, {}, result);
119
+ }
120
+ async function list(listOptions = {}) {
121
+ const { signal, ...params } = listOptions;
122
+ return call("ListTasks", { ...params }, signal ? { signal } : {});
123
+ }
124
+ return {
125
+ url,
126
+ call,
127
+ send(send) {
128
+ const metadata = {
129
+ ...send.metadata,
130
+ ...(send.delivery ? { delivery: send.delivery } : {}),
131
+ };
132
+ return sendMessage({
133
+ messageId: send.messageId,
134
+ role: "ROLE_USER",
135
+ parts: send.parts,
136
+ ...(send.contextId !== undefined ? { contextId: send.contextId } : {}),
137
+ ...(send.taskId !== undefined ? { taskId: send.taskId } : {}),
138
+ ...(send.extensions?.length ? { extensions: [...send.extensions] } : {}),
139
+ ...(Object.keys(metadata).length ? { metadata } : {}),
140
+ }, send);
141
+ },
142
+ get(taskId, getOptions = {}) {
143
+ return call("GetTask", {
144
+ id: taskId,
145
+ ...(getOptions.historyLength !== undefined
146
+ ? { historyLength: getOptions.historyLength }
147
+ : {}),
148
+ }, getOptions.signal ? { signal: getOptions.signal } : {});
149
+ },
150
+ list,
151
+ async *listAll(listOptions = {}) {
152
+ let pageToken;
153
+ do {
154
+ const page = await list({ ...listOptions, ...(pageToken ? { pageToken } : {}) });
155
+ yield* page.tasks;
156
+ pageToken = page.nextPageToken || undefined;
157
+ } while (pageToken);
158
+ },
159
+ cancel(taskId, cancelOptions = {}) {
160
+ return call("CancelTask", { id: taskId }, cancelOptions.signal ? { signal: cancelOptions.signal } : {});
161
+ },
162
+ async answer(answer) {
163
+ const hasApproval = answer.approved !== undefined;
164
+ const hasValue = answer.value !== undefined;
165
+ if (hasApproval === hasValue)
166
+ throw new TypeError("answer: give exactly one of approved and value");
167
+ const reply = hasApproval ? { approved: answer.approved } : { value: answer.value };
168
+ return sendMessage({
169
+ messageId: answer.messageId,
170
+ role: "ROLE_USER",
171
+ taskId: answer.taskId,
172
+ contextId: answer.contextId,
173
+ parts: [{ data: reply }],
174
+ extensions: [A2A_EXTENSIONS.interactions],
175
+ metadata: {
176
+ [A2A_EXTENSIONS.interactions]: { interactionId: answer.interactionId, ...reply },
177
+ },
178
+ }, answer.signal ? { signal: answer.signal } : {});
179
+ },
180
+ async card(cardOptions = {}) {
181
+ const { "A2A-Extensions": _extensions, ...headers } = base;
182
+ const response = await fetcher(cardUrl, {
183
+ headers,
184
+ redirect: "error",
185
+ ...(cardOptions.signal ? { signal: cardOptions.signal } : {}),
186
+ });
187
+ const body = await readBody(response);
188
+ if (!response.ok)
189
+ throw new RuntimeError(response.status, body);
190
+ return body;
191
+ },
192
+ };
193
+ }
194
+ /**
195
+ * `GET /v1/turns`: the turns that changed after `changedAfter`, in the order they changed. Call
196
+ * it on an interval with the last `cursor` to find what a lost push would have said.
197
+ */
198
+ export async function listTurnChanges(options) {
199
+ const baseUrl = options.baseUrl.replace(/\/+$/, "");
200
+ if (!/^https?:\/\//.test(baseUrl))
201
+ throw new TypeError("listTurnChanges: baseUrl must be an http(s) URL");
202
+ const query = new URLSearchParams();
203
+ if (options.changedAfter !== undefined)
204
+ query.set("changedAfter", options.changedAfter);
205
+ if (options.sessionId !== undefined)
206
+ query.set("sessionId", options.sessionId);
207
+ if (options.limit !== undefined)
208
+ query.set("limit", String(options.limit));
209
+ const headers = { ...requestHeaders(options.key) };
210
+ if (options.subject !== undefined) {
211
+ const parsed = parseSubjectHeaders(options.subject, [...(options.scopes ?? ["sessions:own"])].join(" "));
212
+ if (!parsed.ok)
213
+ throw new TypeError(parsed.message);
214
+ headers[SUBJECT_HEADER] = options.subject;
215
+ headers[SCOPES_HEADER] = [...parsed.scopes].join(" ");
216
+ }
217
+ const search = query.size ? `?${query}` : "";
218
+ const response = await fetcherOf(options.fetch)(`${baseUrl}/v1/turns${search}`, {
219
+ headers,
220
+ redirect: "error",
221
+ ...(options.signal ? { signal: options.signal } : {}),
222
+ });
223
+ const body = await readBody(response);
224
+ if (!response.ok)
225
+ throw new RuntimeError(response.status, body);
226
+ return body;
227
+ }
@@ -104,7 +104,13 @@ export function createA2aHandler(options) {
104
104
  if (response.ok)
105
105
  return new Response(response.body, {
106
106
  status: response.status,
107
- headers: { "content-type": response.headers.get("content-type") ?? "application/json" },
107
+ headers: {
108
+ "content-type": response.headers.get("content-type") ?? "application/json",
109
+ // The extensions the agent activated (A2A §4.6), echoed to the partner.
110
+ ...(response.headers.get("a2a-extensions")
111
+ ? { "A2A-Extensions": response.headers.get("a2a-extensions") }
112
+ : {}),
113
+ },
108
114
  });
109
115
  await response.body?.cancel().catch(() => { });
110
116
  if (response.status === 404)
@@ -1,8 +1,11 @@
1
1
  /**
2
2
  * `@nylorun/agents/a2a`: serve the Tenant's agents to A2A clients through the app's own server
3
- * (gateway mode). The handler forwards A2A requests to the Runtime, which speaks the protocol;
4
- * this entry point loads no A2A package.
3
+ * (gateway mode), and call A2A agents with the Nylorun profile v2 (the client, push
4
+ * verification, the extensions' readers and the change feed). The handler forwards A2A requests
5
+ * to the Runtime, which speaks the protocol; this entry point loads no A2A package.
5
6
  */
6
7
  export { createA2aHandler } from "./handler.js";
7
8
  export type { A2aCaller, A2aCardOptions, A2aHandler, A2aHandlerOptions, } from "./handler.js";
8
9
  export { toNodeListener } from "../ag-ui/node.js";
10
+ export * from "./client-api.js";
11
+ export type { AgentCapabilities, AgentCard, AgentExtension, AgentInterface, AgentSkill, Artifact, ListTasksResult, Message, Metadata, Part, PushNotificationConfig, Role, StreamResponse, Task, TaskArtifactUpdateEvent, TaskState, TaskStatus, TaskStatusUpdateEvent, } from "./types.js";