@nylorun/agents 0.30.0-beta → 0.31.1-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 +37 -0
- package/README.md +134 -6
- package/dist/a2a/client-api.d.ts +10 -0
- package/dist/a2a/client-api.js +7 -0
- package/dist/a2a/client.d.ts +178 -0
- package/dist/a2a/client.js +227 -0
- package/dist/a2a/handler.js +7 -1
- package/dist/a2a/index.d.ts +5 -2
- package/dist/a2a/index.js +4 -2
- package/dist/a2a/profile.d.ts +88 -0
- package/dist/a2a/profile.js +133 -0
- package/dist/a2a/push.d.ts +35 -0
- package/dist/a2a/push.js +96 -0
- package/dist/a2a/types.d.ts +145 -0
- package/dist/a2a/types.js +7 -0
- package/dist/compose.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/session-client.d.ts +5 -0
- package/dist/session-client.js +9 -1
- package/dist/sse.d.ts +19 -2
- package/dist/sse.js +41 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# @nylorun/agents
|
|
2
2
|
|
|
3
|
+
## 0.31.1-beta
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Pin core to the tested release.
|
|
8
|
+
- Updated dependencies [63a6217]
|
|
9
|
+
- @nylorun/core@0.33.0-beta
|
|
10
|
+
|
|
11
|
+
## 0.31.0-beta
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- d48b6ef: An A2A client for the Nylorun profile v2 (A2A-E12, E13, E16), on `@nylorun/agents/a2a` and the package root.
|
|
16
|
+
|
|
17
|
+
- **`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`.
|
|
18
|
+
- **`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.
|
|
19
|
+
- **`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`).
|
|
20
|
+
- **`listTurnChanges({ baseUrl, key, changedAfter?, sessionId?, limit? })`**: the Runtime's change feed, `GET /v1/turns` (Host feature `turns`).
|
|
21
|
+
- **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`.
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- d48b6ef: An agent manifest names the model its agent runs on (Host feature `agent-model`).
|
|
26
|
+
|
|
27
|
+
- **`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.
|
|
28
|
+
- **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.
|
|
29
|
+
- `agentModelChoice` and `AgentModelSchema` in `@nylorun/core/contracts`.
|
|
30
|
+
- 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.
|
|
31
|
+
|
|
32
|
+
- Pin core to the tested release.
|
|
33
|
+
- Updated dependencies [d48b6ef]
|
|
34
|
+
- Updated dependencies [d48b6ef]
|
|
35
|
+
- Updated dependencies [d48b6ef]
|
|
36
|
+
- Updated dependencies [d48b6ef]
|
|
37
|
+
- Updated dependencies [d48b6ef]
|
|
38
|
+
- @nylorun/core@0.32.0-beta
|
|
39
|
+
|
|
3
40
|
## 0.30.0-beta
|
|
4
41
|
|
|
5
42
|
### 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
|
-
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
+
}
|
package/dist/a2a/handler.js
CHANGED
|
@@ -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: {
|
|
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)
|
package/dist/a2a/index.d.ts
CHANGED
|
@@ -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)
|
|
4
|
-
*
|
|
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";
|