@nxisai/agent-harness 0.5.0 → 0.6.0
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/README.md +22 -1
- package/dist/handoff/a2a.d.ts +152 -0
- package/dist/handoff/a2a.d.ts.map +1 -0
- package/dist/handoff/a2a.js +227 -0
- package/dist/handoff/a2a.js.map +1 -0
- package/dist/handoff/controller.d.ts +181 -0
- package/dist/handoff/controller.d.ts.map +1 -0
- package/dist/handoff/controller.js +543 -0
- package/dist/handoff/controller.js.map +1 -0
- package/dist/handoff/envelope.d.ts +130 -0
- package/dist/handoff/envelope.d.ts.map +1 -0
- package/dist/handoff/envelope.js +178 -0
- package/dist/handoff/envelope.js.map +1 -0
- package/dist/handoff/index.d.ts +16 -0
- package/dist/handoff/index.d.ts.map +1 -0
- package/dist/handoff/index.js +12 -0
- package/dist/handoff/index.js.map +1 -0
- package/dist/handoff/tools.d.ts +36 -0
- package/dist/handoff/tools.d.ts.map +1 -0
- package/dist/handoff/tools.js +148 -0
- package/dist/handoff/tools.js.map +1 -0
- package/dist/harness/harness.d.ts +4 -2
- package/dist/harness/harness.d.ts.map +1 -1
- package/dist/harness/harness.js +30 -7
- package/dist/harness/harness.js.map +1 -1
- package/dist/harness/router/coordinator.d.ts +13 -0
- package/dist/harness/router/coordinator.d.ts.map +1 -1
- package/dist/harness/router/coordinator.js +7 -0
- package/dist/harness/router/coordinator.js.map +1 -1
- package/dist/harness/tools/discovery.d.ts +12 -1
- package/dist/harness/tools/discovery.d.ts.map +1 -1
- package/dist/harness/tools/discovery.js +19 -1
- package/dist/harness/tools/discovery.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/ui/agent-fullscreen.d.ts.map +1 -1
- package/dist/ui/agent-fullscreen.js +264 -1
- package/dist/ui/agent-fullscreen.js.map +1 -1
- package/package.json +1 -1
- package/src/handoff/a2a.ts +326 -0
- package/src/handoff/controller.ts +661 -0
- package/src/handoff/envelope.ts +216 -0
- package/src/handoff/index.ts +53 -0
- package/src/handoff/tools.ts +178 -0
- package/src/harness/harness.ts +28 -3
- package/src/harness/router/coordinator.ts +21 -0
- package/src/harness/tools/discovery.ts +20 -2
- package/src/index.ts +3 -0
- package/src/ui/agent-fullscreen.ts +280 -2
package/README.md
CHANGED
|
@@ -211,6 +211,7 @@ Both flags go false at a specialist handoff, which swaps the system prompt and t
|
|
|
211
211
|
| `preload` | `'eager' \| 'idle' \| 'none'` | `'idle'` | When weights are fetched. Local provider only. Settable mid-session. |
|
|
212
212
|
| `debug` | `boolean` | `false` | Shows tool receipts and route labels. |
|
|
213
213
|
| `preferences` | `AgentPreferences` | session-scoped | See [Preferences](#preferences). |
|
|
214
|
+
| `handoff` | `HandoffConfig \| null` | `null` | Hand conversations to other agents and people. See [Handoff](#handoff-to-an-agent-or-a-person). |
|
|
214
215
|
|
|
215
216
|
### Attributes
|
|
216
217
|
|
|
@@ -435,7 +436,7 @@ const card = viewJourney(steps); // { title, steps, outcome, active, summary }
|
|
|
435
436
|
### `AgentCoordinator`
|
|
436
437
|
|
|
437
438
|
```ts
|
|
438
|
-
new AgentCoordinator({ agents, rules, fallback, provider?, descriptions?, onRoute? })
|
|
439
|
+
new AgentCoordinator({ agents, rules, fallback, provider?, descriptions?, onRoute?, handoff? })
|
|
439
440
|
coordinator.turn(goal, messages): Promise<TurnResult>
|
|
440
441
|
coordinator.stop(): void // the classifier and the agent that took the turn
|
|
441
442
|
coordinator.turn(goal, messages): Promise<TurnResult>
|
|
@@ -574,6 +575,26 @@ import { emitAsk, emitStep, emitConfirm, toV09 } from '@nxisai/agent-harness/a2u
|
|
|
574
575
|
|
|
575
576
|
Messages are authored against A2UI v1.0 and downgraded with `toV09` for the published `@a2ui/lit` renderer.
|
|
576
577
|
|
|
578
|
+
## Handoff to an agent or a person
|
|
579
|
+
|
|
580
|
+
The assistant can hand a conversation to another agent that can finish what it can't, or to a person on the site's team. Both are reached over [A2A 1.0](https://a2a-protocol.org/latest/specification/), through a relay the site hosts on its own origin, so the page holds no credential and never talks to a target directly.
|
|
581
|
+
|
|
582
|
+
```ts
|
|
583
|
+
agent.handoff = {
|
|
584
|
+
relay: '/api/handoff',
|
|
585
|
+
targets: {
|
|
586
|
+
booking: { kind: 'agent', label: 'Booking', description: 'books a service visit' },
|
|
587
|
+
team: { kind: 'person', label: 'Our team' },
|
|
588
|
+
},
|
|
589
|
+
};
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
That offers two tools, `transfer_to_agent` and `talk_to_a_person`, listing only the targets both this config and the relay name. They are consequential: the visitor sees who the conversation goes to and the summary that goes with it, and presses **Send**. From then on what the visitor types goes to the target — its replies appear under its name, a bar above the input says who is listening, and **Back to the assistant** ends it. When the target finishes, the assistant carries on from its hand-back note ("Booked for Thursday, reference CL-4KDQ-7M2X"). A handoff survives page changes and reloads.
|
|
593
|
+
|
|
594
|
+
Headless, `HandoffController` runs the same handoffs and reports them as `handoff` events; `registerHandoffTools` offers the tools; `AgentCoordinator`'s `handoff` option routes a handed-off conversation's turns to its target.
|
|
595
|
+
|
|
596
|
+
[docs/handoff.md](docs/handoff.md) is the contract: the relay's endpoints, the handoff extension (`https://nxis.ai/a2a/handoff/v1`: the envelope, queue, operator and hand-back parts), the lifecycle, what makes a good target, and the adapter contract for putting a help desk behind A2A. [Copperline](https://github.com/nxisai/copperline-home-services) has a reference of each part on Netlify: the relay, an A2A booking agent built on `@a2a-js/sdk`, and a human desk with an operator console.
|
|
597
|
+
|
|
577
598
|
## Requirements
|
|
578
599
|
|
|
579
600
|
- WebGPU: Chrome or Edge 113+, or Safari 18+.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small A2A 1.0 client, over JSON-RPC, for talking to a site's handoff relay.
|
|
3
|
+
*
|
|
4
|
+
* Only what a handoff needs: send a message (streamed or not), read a task,
|
|
5
|
+
* subscribe to one, cancel one. The official `@a2a-js/sdk` covers the whole
|
|
6
|
+
* protocol, but brings a JWT library and protobuf and gRPC peers with it, and
|
|
7
|
+
* this harness ships nothing it doesn't use.
|
|
8
|
+
*
|
|
9
|
+
* The wire format is A2A 1.0's JSON: parts and stream events are told apart by
|
|
10
|
+
* which member they carry (`text`, `data`, `statusUpdate`, …), and enums are
|
|
11
|
+
* their names (`TASK_STATE_WORKING`, `ROLE_USER`). Servers still on 0.3 send
|
|
12
|
+
* `kind` discriminators and lower-case states; those are read too.
|
|
13
|
+
*
|
|
14
|
+
* @see https://a2a-protocol.org/latest/specification/
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
/** The A2A protocol version this client speaks, sent as `A2A-Version`. */
|
|
18
|
+
export declare const A2A_VERSION = "1.0";
|
|
19
|
+
/** A task's state, by its A2A 1.0 name. */
|
|
20
|
+
export type A2ATaskState = 'TASK_STATE_UNSPECIFIED' | 'TASK_STATE_SUBMITTED' | 'TASK_STATE_WORKING' | 'TASK_STATE_COMPLETED' | 'TASK_STATE_FAILED' | 'TASK_STATE_CANCELED' | 'TASK_STATE_INPUT_REQUIRED' | 'TASK_STATE_REJECTED' | 'TASK_STATE_AUTH_REQUIRED';
|
|
21
|
+
/** One piece of a message or artifact. Which member is present says what it is. */
|
|
22
|
+
export interface A2APart {
|
|
23
|
+
text?: string;
|
|
24
|
+
data?: unknown;
|
|
25
|
+
url?: string;
|
|
26
|
+
raw?: string;
|
|
27
|
+
filename?: string;
|
|
28
|
+
mediaType?: string;
|
|
29
|
+
metadata?: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
/** A message, from the visitor (`ROLE_USER`) or the target (`ROLE_AGENT`). */
|
|
32
|
+
export interface A2AMessage {
|
|
33
|
+
messageId: string;
|
|
34
|
+
role: 'ROLE_USER' | 'ROLE_AGENT';
|
|
35
|
+
parts: A2APart[];
|
|
36
|
+
contextId?: string;
|
|
37
|
+
taskId?: string;
|
|
38
|
+
metadata?: Record<string, unknown>;
|
|
39
|
+
extensions?: string[];
|
|
40
|
+
}
|
|
41
|
+
/** Where a task stands, and the message that goes with it, if any. */
|
|
42
|
+
export interface A2ATaskStatus {
|
|
43
|
+
state: A2ATaskState;
|
|
44
|
+
message?: A2AMessage;
|
|
45
|
+
timestamp?: string;
|
|
46
|
+
}
|
|
47
|
+
/** Output a task produced. */
|
|
48
|
+
export interface A2AArtifact {
|
|
49
|
+
artifactId?: string;
|
|
50
|
+
name?: string;
|
|
51
|
+
parts: A2APart[];
|
|
52
|
+
metadata?: Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
/** A unit of work at the target. */
|
|
55
|
+
export interface A2ATask {
|
|
56
|
+
id: string;
|
|
57
|
+
contextId?: string;
|
|
58
|
+
status: A2ATaskStatus;
|
|
59
|
+
artifacts?: A2AArtifact[];
|
|
60
|
+
history?: A2AMessage[];
|
|
61
|
+
metadata?: Record<string, unknown>;
|
|
62
|
+
}
|
|
63
|
+
/** One event on a stream, or the result of a send: exactly one member is set. */
|
|
64
|
+
export interface A2AStreamResponse {
|
|
65
|
+
task?: A2ATask;
|
|
66
|
+
message?: A2AMessage;
|
|
67
|
+
statusUpdate?: {
|
|
68
|
+
taskId: string;
|
|
69
|
+
contextId?: string;
|
|
70
|
+
status: A2ATaskStatus;
|
|
71
|
+
metadata?: Record<string, unknown>;
|
|
72
|
+
};
|
|
73
|
+
artifactUpdate?: {
|
|
74
|
+
taskId: string;
|
|
75
|
+
contextId?: string;
|
|
76
|
+
artifact: A2AArtifact;
|
|
77
|
+
append?: boolean;
|
|
78
|
+
lastChunk?: boolean;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** An error the relay or the target answered with. */
|
|
82
|
+
export declare class A2AError extends Error {
|
|
83
|
+
/** The JSON-RPC error code, or the HTTP status when the relay refused before A2A. */
|
|
84
|
+
readonly code: number;
|
|
85
|
+
/** The HTTP status of the response. */
|
|
86
|
+
readonly status?: number | undefined;
|
|
87
|
+
/** Seconds to wait, from `Retry-After`, when the relay is rate limiting. */
|
|
88
|
+
readonly retryAfter?: number | undefined;
|
|
89
|
+
constructor(message: string,
|
|
90
|
+
/** The JSON-RPC error code, or the HTTP status when the relay refused before A2A. */
|
|
91
|
+
code: number,
|
|
92
|
+
/** The HTTP status of the response. */
|
|
93
|
+
status?: number | undefined,
|
|
94
|
+
/** Seconds to wait, from `Retry-After`, when the relay is rate limiting. */
|
|
95
|
+
retryAfter?: number | undefined);
|
|
96
|
+
}
|
|
97
|
+
/** Options for {@link A2AClient}. */
|
|
98
|
+
export interface A2AClientOptions {
|
|
99
|
+
/** Where requests go: the relay's URL for one target, `/api/handoff/booking`. */
|
|
100
|
+
endpoint: string;
|
|
101
|
+
/** Extensions to announce in `A2A-Extensions`. */
|
|
102
|
+
extensions?: string[];
|
|
103
|
+
/** `fetch` to use. Defaults to the global. */
|
|
104
|
+
fetchImpl?: typeof fetch;
|
|
105
|
+
}
|
|
106
|
+
/** What a call returned, with the response headers the relay may have set. */
|
|
107
|
+
export interface A2AReply<T> {
|
|
108
|
+
result: T;
|
|
109
|
+
headers: Headers;
|
|
110
|
+
}
|
|
111
|
+
/** A stream of events, and the response headers it opened with. */
|
|
112
|
+
export interface A2AStream {
|
|
113
|
+
events: AsyncGenerator<A2AStreamResponse, void, undefined>;
|
|
114
|
+
headers: Headers;
|
|
115
|
+
}
|
|
116
|
+
/** A2A over JSON-RPC to one endpoint. */
|
|
117
|
+
export declare class A2AClient {
|
|
118
|
+
private readonly opts;
|
|
119
|
+
constructor(opts: A2AClientOptions);
|
|
120
|
+
/**
|
|
121
|
+
* Call a method that answers once: `SendMessage`, `GetTask`, `CancelTask`.
|
|
122
|
+
*
|
|
123
|
+
* @throws {@link A2AError} when the relay refuses or the target answers with an error.
|
|
124
|
+
*/
|
|
125
|
+
call<T>(method: string, params: unknown, init?: {
|
|
126
|
+
signal?: AbortSignal;
|
|
127
|
+
headers?: Record<string, string>;
|
|
128
|
+
}): Promise<A2AReply<T>>;
|
|
129
|
+
/**
|
|
130
|
+
* Call a method that answers with a stream: `SendStreamingMessage`, `SubscribeToTask`.
|
|
131
|
+
*
|
|
132
|
+
* The generator ends when the server closes the stream, which it does at a
|
|
133
|
+
* terminal state, or earlier if the host cuts the response off.
|
|
134
|
+
*
|
|
135
|
+
* @throws {@link A2AError} when the relay refuses; an error event on the stream
|
|
136
|
+
* is thrown from the generator.
|
|
137
|
+
*/
|
|
138
|
+
stream(method: string, params: unknown, init?: {
|
|
139
|
+
signal?: AbortSignal;
|
|
140
|
+
headers?: Record<string, string>;
|
|
141
|
+
}): Promise<A2AStream>;
|
|
142
|
+
private post;
|
|
143
|
+
}
|
|
144
|
+
/** A task state by its 1.0 name, whichever version sent it. */
|
|
145
|
+
export declare function taskState(raw: unknown): A2ATaskState;
|
|
146
|
+
/** A result in 1.0's shape, whether it came as 1.0 or 0.3 (`kind` discriminators). */
|
|
147
|
+
export declare function normaliseResponse(raw: A2AStreamResponse | Record<string, unknown>): A2AStreamResponse;
|
|
148
|
+
/** Whether a state ends the task. */
|
|
149
|
+
export declare function isTerminal(state: A2ATaskState): boolean;
|
|
150
|
+
/** A message id, unique enough for one conversation. */
|
|
151
|
+
export declare function newMessageId(): string;
|
|
152
|
+
//# sourceMappingURL=a2a.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"a2a.d.ts","sourceRoot":"","sources":["../../src/handoff/a2a.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,0EAA0E;AAC1E,eAAO,MAAM,WAAW,QAAQ,CAAC;AAEjC,2CAA2C;AAC3C,MAAM,MAAM,YAAY,GACpB,wBAAwB,GACxB,sBAAsB,GACtB,oBAAoB,GACpB,sBAAsB,GACtB,mBAAmB,GACnB,qBAAqB,GACrB,2BAA2B,GAC3B,qBAAqB,GACrB,0BAA0B,CAAC;AAE/B,mFAAmF;AACnF,MAAM,WAAW,OAAO;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,8EAA8E;AAC9E,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,WAAW,GAAG,YAAY,CAAC;IACjC,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,sEAAsE;AACtE,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,YAAY,CAAC;IACpB,OAAO,CAAC,EAAE,UAAU,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,8BAA8B;AAC9B,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,oCAAoC;AACpC,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,aAAa,CAAC;IACtB,SAAS,CAAC,EAAE,WAAW,EAAE,CAAC;IAC1B,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,iFAAiF;AACjF,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,UAAU,CAAC;IACrB,YAAY,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,aAAa,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC;IACjH,cAAc,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,WAAW,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CACvH;AAED,sDAAsD;AACtD,qBAAa,QAAS,SAAQ,KAAK;IAG/B,qFAAqF;IACrF,QAAQ,CAAC,IAAI,EAAE,MAAM;IACrB,uCAAuC;IACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM;IACxB,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM;gBAN5B,OAAO,EAAE,MAAM;IACf,qFAAqF;IAC5E,IAAI,EAAE,MAAM;IACrB,uCAAuC;IAC9B,MAAM,CAAC,EAAE,MAAM,YAAA;IACxB,4EAA4E;IACnE,UAAU,CAAC,EAAE,MAAM,YAAA;CAK/B;AAED,qCAAqC;AACrC,MAAM,WAAW,gBAAgB;IAC/B,iFAAiF;IACjF,QAAQ,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,8CAA8C;IAC9C,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED,8EAA8E;AAC9E,MAAM,WAAW,QAAQ,CAAC,CAAC;IACzB,MAAM,EAAE,CAAC,CAAC;IACV,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,mEAAmE;AACnE,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,cAAc,CAAC,iBAAiB,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;IAC3D,OAAO,EAAE,OAAO,CAAC;CAClB;AAID,yCAAyC;AACzC,qBAAa,SAAS;IACR,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,gBAAgB;IAEnD;;;;OAIG;IACG,IAAI,CAAC,CAAC,EACV,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,OAAO,EACf,IAAI,GAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAO,GACpE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IAQvB;;;;;;;;OAQG;IACG,MAAM,CACV,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,OAAO,EACf,IAAI,GAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAO,GACpE,OAAO,CAAC,SAAS,CAAC;YAqBP,IAAI;CAkCnB;AA4DD,+DAA+D;AAC/D,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,YAAY,CAIpD;AAED,sFAAsF;AACtF,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,iBAAiB,CAwBrG;AAED,qCAAqC;AACrC,wBAAgB,UAAU,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAOvD;AAED,wDAAwD;AACxD,wBAAgB,YAAY,IAAI,MAAM,CAIrC"}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small A2A 1.0 client, over JSON-RPC, for talking to a site's handoff relay.
|
|
3
|
+
*
|
|
4
|
+
* Only what a handoff needs: send a message (streamed or not), read a task,
|
|
5
|
+
* subscribe to one, cancel one. The official `@a2a-js/sdk` covers the whole
|
|
6
|
+
* protocol, but brings a JWT library and protobuf and gRPC peers with it, and
|
|
7
|
+
* this harness ships nothing it doesn't use.
|
|
8
|
+
*
|
|
9
|
+
* The wire format is A2A 1.0's JSON: parts and stream events are told apart by
|
|
10
|
+
* which member they carry (`text`, `data`, `statusUpdate`, …), and enums are
|
|
11
|
+
* their names (`TASK_STATE_WORKING`, `ROLE_USER`). Servers still on 0.3 send
|
|
12
|
+
* `kind` discriminators and lower-case states; those are read too.
|
|
13
|
+
*
|
|
14
|
+
* @see https://a2a-protocol.org/latest/specification/
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
/** The A2A protocol version this client speaks, sent as `A2A-Version`. */
|
|
18
|
+
export const A2A_VERSION = '1.0';
|
|
19
|
+
/** An error the relay or the target answered with. */
|
|
20
|
+
export class A2AError extends Error {
|
|
21
|
+
constructor(message,
|
|
22
|
+
/** The JSON-RPC error code, or the HTTP status when the relay refused before A2A. */
|
|
23
|
+
code,
|
|
24
|
+
/** The HTTP status of the response. */
|
|
25
|
+
status,
|
|
26
|
+
/** Seconds to wait, from `Retry-After`, when the relay is rate limiting. */
|
|
27
|
+
retryAfter) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.code = code;
|
|
30
|
+
this.status = status;
|
|
31
|
+
this.retryAfter = retryAfter;
|
|
32
|
+
this.name = 'A2AError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
let nextId = 1;
|
|
36
|
+
/** A2A over JSON-RPC to one endpoint. */
|
|
37
|
+
export class A2AClient {
|
|
38
|
+
constructor(opts) {
|
|
39
|
+
this.opts = opts;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Call a method that answers once: `SendMessage`, `GetTask`, `CancelTask`.
|
|
43
|
+
*
|
|
44
|
+
* @throws {@link A2AError} when the relay refuses or the target answers with an error.
|
|
45
|
+
*/
|
|
46
|
+
async call(method, params, init = {}) {
|
|
47
|
+
const res = await this.post(method, params, 'application/json', init);
|
|
48
|
+
const body = (await res.json().catch(() => null));
|
|
49
|
+
if (!body)
|
|
50
|
+
throw new A2AError(`Empty answer to ${method}`, -32603, res.status);
|
|
51
|
+
if (body.error)
|
|
52
|
+
throw new A2AError(body.error.message, body.error.code, res.status);
|
|
53
|
+
return { result: body.result, headers: res.headers };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Call a method that answers with a stream: `SendStreamingMessage`, `SubscribeToTask`.
|
|
57
|
+
*
|
|
58
|
+
* The generator ends when the server closes the stream, which it does at a
|
|
59
|
+
* terminal state, or earlier if the host cuts the response off.
|
|
60
|
+
*
|
|
61
|
+
* @throws {@link A2AError} when the relay refuses; an error event on the stream
|
|
62
|
+
* is thrown from the generator.
|
|
63
|
+
*/
|
|
64
|
+
async stream(method, params, init = {}) {
|
|
65
|
+
const res = await this.post(method, params, 'text/event-stream', init);
|
|
66
|
+
const type = res.headers.get('content-type') ?? '';
|
|
67
|
+
// A server may answer a streaming request with a single JSON response: an
|
|
68
|
+
// error, or a target without streaming that replied at once.
|
|
69
|
+
if (!type.includes('text/event-stream')) {
|
|
70
|
+
const body = (await res.json().catch(() => null));
|
|
71
|
+
if (body?.error)
|
|
72
|
+
throw new A2AError(body.error.message, body.error.code, res.status);
|
|
73
|
+
const once = body?.result;
|
|
74
|
+
return {
|
|
75
|
+
headers: res.headers,
|
|
76
|
+
events: (async function* () {
|
|
77
|
+
if (once)
|
|
78
|
+
yield normaliseResponse(once);
|
|
79
|
+
})(),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return { headers: res.headers, events: readEvents(res, init.signal) };
|
|
83
|
+
}
|
|
84
|
+
async post(method, params, accept, init) {
|
|
85
|
+
const doFetch = this.opts.fetchImpl ?? fetch;
|
|
86
|
+
const res = await doFetch(this.opts.endpoint, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
credentials: 'same-origin',
|
|
89
|
+
signal: init.signal,
|
|
90
|
+
headers: {
|
|
91
|
+
'Content-Type': 'application/json',
|
|
92
|
+
Accept: accept,
|
|
93
|
+
'A2A-Version': A2A_VERSION,
|
|
94
|
+
...(this.opts.extensions?.length ? { 'A2A-Extensions': this.opts.extensions.join(',') } : {}),
|
|
95
|
+
...init.headers,
|
|
96
|
+
},
|
|
97
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: nextId++, method, params }),
|
|
98
|
+
});
|
|
99
|
+
if (!res.ok) {
|
|
100
|
+
const retry = Number(res.headers.get('retry-after'));
|
|
101
|
+
let message = `${method} refused (${res.status})`;
|
|
102
|
+
try {
|
|
103
|
+
const body = (await res.json());
|
|
104
|
+
const text = typeof body.error === 'string' ? body.error : (body.error?.message ?? body.message);
|
|
105
|
+
if (text)
|
|
106
|
+
message = text;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Not JSON: keep the status line.
|
|
110
|
+
}
|
|
111
|
+
throw new A2AError(message, res.status, res.status, Number.isFinite(retry) && retry > 0 ? retry : undefined);
|
|
112
|
+
}
|
|
113
|
+
return res;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** Server-sent events off a response body, as A2A stream responses. */
|
|
117
|
+
async function* readEvents(res, signal) {
|
|
118
|
+
if (!res.body)
|
|
119
|
+
return;
|
|
120
|
+
const reader = res.body.getReader();
|
|
121
|
+
const decoder = new TextDecoder();
|
|
122
|
+
let buffer = '';
|
|
123
|
+
try {
|
|
124
|
+
while (true) {
|
|
125
|
+
if (signal?.aborted)
|
|
126
|
+
return;
|
|
127
|
+
const { value, done } = await reader.read();
|
|
128
|
+
if (done)
|
|
129
|
+
break;
|
|
130
|
+
buffer += decoder.decode(value, { stream: true });
|
|
131
|
+
let cut;
|
|
132
|
+
while ((cut = boundary(buffer)) >= 0) {
|
|
133
|
+
const block = buffer.slice(0, cut);
|
|
134
|
+
buffer = buffer.slice(cut).replace(/^(\r?\n){2}/, '');
|
|
135
|
+
const event = parseBlock(block);
|
|
136
|
+
if (event)
|
|
137
|
+
yield event;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const tail = parseBlock(buffer);
|
|
141
|
+
if (tail)
|
|
142
|
+
yield tail;
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
reader.releaseLock?.();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** Where the first complete event block ends, or -1. */
|
|
149
|
+
function boundary(buffer) {
|
|
150
|
+
const m = /\r?\n\r?\n/.exec(buffer);
|
|
151
|
+
return m ? m.index : -1;
|
|
152
|
+
}
|
|
153
|
+
/** One SSE block's `data:` lines, as a stream response; null for comments and keep-alives. */
|
|
154
|
+
function parseBlock(block) {
|
|
155
|
+
const data = block
|
|
156
|
+
.split(/\r?\n/)
|
|
157
|
+
.filter((line) => line.startsWith('data:'))
|
|
158
|
+
.map((line) => line.slice(5).replace(/^ /, ''))
|
|
159
|
+
.join('\n');
|
|
160
|
+
if (!data.trim())
|
|
161
|
+
return null;
|
|
162
|
+
const body = JSON.parse(data);
|
|
163
|
+
if (body.error)
|
|
164
|
+
throw new A2AError(body.error.message, body.error.code);
|
|
165
|
+
return body.result ? normaliseResponse(body.result) : null;
|
|
166
|
+
}
|
|
167
|
+
/** 0.3 event and state names, read as 1.0's. */
|
|
168
|
+
const LEGACY_STATES = {
|
|
169
|
+
submitted: 'TASK_STATE_SUBMITTED',
|
|
170
|
+
working: 'TASK_STATE_WORKING',
|
|
171
|
+
completed: 'TASK_STATE_COMPLETED',
|
|
172
|
+
failed: 'TASK_STATE_FAILED',
|
|
173
|
+
canceled: 'TASK_STATE_CANCELED',
|
|
174
|
+
'input-required': 'TASK_STATE_INPUT_REQUIRED',
|
|
175
|
+
rejected: 'TASK_STATE_REJECTED',
|
|
176
|
+
'auth-required': 'TASK_STATE_AUTH_REQUIRED',
|
|
177
|
+
};
|
|
178
|
+
/** A task state by its 1.0 name, whichever version sent it. */
|
|
179
|
+
export function taskState(raw) {
|
|
180
|
+
const s = String(raw ?? '');
|
|
181
|
+
if (s.startsWith('TASK_STATE_'))
|
|
182
|
+
return s;
|
|
183
|
+
return LEGACY_STATES[s] ?? 'TASK_STATE_UNSPECIFIED';
|
|
184
|
+
}
|
|
185
|
+
/** A result in 1.0's shape, whether it came as 1.0 or 0.3 (`kind` discriminators). */
|
|
186
|
+
export function normaliseResponse(raw) {
|
|
187
|
+
const r = raw;
|
|
188
|
+
if (r.task || r.message || r.statusUpdate || r.artifactUpdate) {
|
|
189
|
+
const out = { ...r };
|
|
190
|
+
if (out.task)
|
|
191
|
+
out.task = { ...out.task, status: { ...out.task.status, state: taskState(out.task.status?.state) } };
|
|
192
|
+
if (out.statusUpdate) {
|
|
193
|
+
out.statusUpdate = { ...out.statusUpdate, status: { ...out.statusUpdate.status, state: taskState(out.statusUpdate.status?.state) } };
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
switch (r.kind) {
|
|
198
|
+
case 'task':
|
|
199
|
+
return normaliseResponse({ task: r });
|
|
200
|
+
case 'message':
|
|
201
|
+
return { message: r };
|
|
202
|
+
case 'status-update':
|
|
203
|
+
return normaliseResponse({ statusUpdate: r });
|
|
204
|
+
case 'artifact-update':
|
|
205
|
+
return { artifactUpdate: r };
|
|
206
|
+
default:
|
|
207
|
+
// A bare task, as `GetTask` returns one.
|
|
208
|
+
if (typeof r.id === 'string' && r.status)
|
|
209
|
+
return normaliseResponse({ task: r });
|
|
210
|
+
return {};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/** Whether a state ends the task. */
|
|
214
|
+
export function isTerminal(state) {
|
|
215
|
+
return (state === 'TASK_STATE_COMPLETED' ||
|
|
216
|
+
state === 'TASK_STATE_FAILED' ||
|
|
217
|
+
state === 'TASK_STATE_CANCELED' ||
|
|
218
|
+
state === 'TASK_STATE_REJECTED');
|
|
219
|
+
}
|
|
220
|
+
/** A message id, unique enough for one conversation. */
|
|
221
|
+
export function newMessageId() {
|
|
222
|
+
const c = globalThis.crypto;
|
|
223
|
+
if (c?.randomUUID)
|
|
224
|
+
return c.randomUUID();
|
|
225
|
+
return `m-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
226
|
+
}
|
|
227
|
+
//# sourceMappingURL=a2a.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"a2a.js","sourceRoot":"","sources":["../../src/handoff/a2a.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { type HandBack, type HandoffEnvelope } from './envelope.js';
|
|
2
|
+
/** Who a target is: software, or a person behind a desk. */
|
|
3
|
+
export type HandoffKind = 'agent' | 'person';
|
|
4
|
+
/** A target as the page knows it: a name, and how to present it. */
|
|
5
|
+
export interface HandoffTargetConfig {
|
|
6
|
+
kind: HandoffKind;
|
|
7
|
+
/** What the visitor reads: "You're talking with *Booking*". */
|
|
8
|
+
label: string;
|
|
9
|
+
/** What it handles, for the model choosing between targets. */
|
|
10
|
+
description?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Where handoffs go. */
|
|
13
|
+
export interface HandoffConfig {
|
|
14
|
+
/** The relay on the site's own origin, `/api/handoff`. */
|
|
15
|
+
relay: string;
|
|
16
|
+
/** The targets, by the name the relay knows them by. */
|
|
17
|
+
targets: Record<string, HandoffTargetConfig>;
|
|
18
|
+
}
|
|
19
|
+
/** A target as the relay reports it, merged with the page's config. */
|
|
20
|
+
export interface HandoffTarget extends HandoffTargetConfig {
|
|
21
|
+
name: string;
|
|
22
|
+
/** Whether it can take a handoff now. A desk outside its hours cannot. */
|
|
23
|
+
available: boolean;
|
|
24
|
+
/** When a desk is staffed, as a person should read it. */
|
|
25
|
+
hours?: string;
|
|
26
|
+
/** How many are waiting at a desk. */
|
|
27
|
+
queueLength?: number;
|
|
28
|
+
/** Expected wait at a desk, in minutes. */
|
|
29
|
+
waitMinutes?: number | null;
|
|
30
|
+
/** Whether the relay allows the recent transcript to be shared with it. */
|
|
31
|
+
transcript: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** Where a handoff stands. See the lifecycle table in `docs/handoff.md`. */
|
|
34
|
+
export type HandoffState = 'connecting' | 'queued' | 'active' | 'awaiting-visitor' | 'handed-back' | 'ended' | 'failed';
|
|
35
|
+
/** Whether a handoff in this state is over. */
|
|
36
|
+
export declare const isHandoffOver: (state: HandoffState) => boolean;
|
|
37
|
+
/** One conversation's handoff, as kept between pages. */
|
|
38
|
+
export interface HandoffSession {
|
|
39
|
+
conversationId: string;
|
|
40
|
+
target: string;
|
|
41
|
+
label: string;
|
|
42
|
+
kind: HandoffKind;
|
|
43
|
+
state: HandoffState;
|
|
44
|
+
taskId?: string;
|
|
45
|
+
contextId?: string;
|
|
46
|
+
/** The relay's `Nxis-Handoff-Key`, proving this browser started the task. */
|
|
47
|
+
key?: string;
|
|
48
|
+
/** Who is speaking at the target, once a person or agent has said. */
|
|
49
|
+
operator?: string;
|
|
50
|
+
/** Place in a desk's queue, while queued. */
|
|
51
|
+
queue?: {
|
|
52
|
+
position?: number;
|
|
53
|
+
waitMinutes?: number;
|
|
54
|
+
};
|
|
55
|
+
handBack?: HandBack;
|
|
56
|
+
/** Why it failed, in words for the visitor. */
|
|
57
|
+
error?: string;
|
|
58
|
+
startedAt: number;
|
|
59
|
+
updatedAt: number;
|
|
60
|
+
/** Message ids already shown, so a resumed task's history is not shown twice. */
|
|
61
|
+
seen: string[];
|
|
62
|
+
}
|
|
63
|
+
/** What the controller reports, as the `detail` of a `handoff` event. */
|
|
64
|
+
export type HandoffEvent = {
|
|
65
|
+
type: 'state';
|
|
66
|
+
conversationId: string;
|
|
67
|
+
session: HandoffSession;
|
|
68
|
+
state: HandoffState;
|
|
69
|
+
} | {
|
|
70
|
+
type: 'message';
|
|
71
|
+
conversationId: string;
|
|
72
|
+
session: HandoffSession;
|
|
73
|
+
/** Who said it: the operator's name, or the target's label. */
|
|
74
|
+
from: string;
|
|
75
|
+
fromKind: HandoffKind;
|
|
76
|
+
text: string;
|
|
77
|
+
} | {
|
|
78
|
+
type: 'handback';
|
|
79
|
+
conversationId: string;
|
|
80
|
+
session: HandoffSession;
|
|
81
|
+
handBack: HandBack;
|
|
82
|
+
};
|
|
83
|
+
/** Options for {@link HandoffController}. */
|
|
84
|
+
export interface HandoffControllerOptions extends HandoffConfig {
|
|
85
|
+
/** `fetch` to use. Defaults to the global. */
|
|
86
|
+
fetchImpl?: typeof fetch;
|
|
87
|
+
/** Where sessions are kept. Defaults to `localStorage`; null keeps them in memory only. */
|
|
88
|
+
storage?: Storage | null;
|
|
89
|
+
/** The clock. */
|
|
90
|
+
now?: () => number;
|
|
91
|
+
/** First wait before picking up a stream the host cut off. Doubles up to 15 s. */
|
|
92
|
+
resumeDelayMs?: number;
|
|
93
|
+
}
|
|
94
|
+
/** The response header a relay ties a task to this browser with. */
|
|
95
|
+
export declare const HANDOFF_KEY_HEADER = "Nxis-Handoff-Key";
|
|
96
|
+
/** Runs handoffs for every conversation on the page. Dispatches `handoff` events. */
|
|
97
|
+
export declare class HandoffController extends EventTarget {
|
|
98
|
+
private readonly opts;
|
|
99
|
+
private readonly sessions;
|
|
100
|
+
/** The stream open for each conversation, so a new one can close it first. */
|
|
101
|
+
private readonly live;
|
|
102
|
+
/** Consecutive resumes that brought nothing, per conversation, for backoff. */
|
|
103
|
+
private readonly idle;
|
|
104
|
+
private readonly timers;
|
|
105
|
+
private targetsCache;
|
|
106
|
+
/** Events reported so far, so a stream can tell whether it brought anything new. */
|
|
107
|
+
private reported;
|
|
108
|
+
private readonly storage;
|
|
109
|
+
private loaded;
|
|
110
|
+
constructor(opts: HandoffControllerOptions);
|
|
111
|
+
/** The page's target config. */
|
|
112
|
+
get config(): HandoffConfig;
|
|
113
|
+
/**
|
|
114
|
+
* The targets that can be offered: those both in the page's config and on
|
|
115
|
+
* the relay's list, with the relay's availability.
|
|
116
|
+
*
|
|
117
|
+
* Cached for thirty seconds. A relay that can't be reached offers none.
|
|
118
|
+
*/
|
|
119
|
+
targets(): Promise<HandoffTarget[]>;
|
|
120
|
+
private fetchTargets;
|
|
121
|
+
/** A conversation's handoff, open or finished, if it has one. */
|
|
122
|
+
session(conversationId: string): HandoffSession | null;
|
|
123
|
+
/** A conversation's handoff while it is open, else null. */
|
|
124
|
+
active(conversationId: string | undefined): HandoffSession | null;
|
|
125
|
+
/**
|
|
126
|
+
* Hand a conversation to a target.
|
|
127
|
+
*
|
|
128
|
+
* Resolves once the relay has accepted it; what the target says arrives as
|
|
129
|
+
* events. Call only after the visitor has confirmed what is being sent.
|
|
130
|
+
*
|
|
131
|
+
* @throws When the target is not configured, the conversation is already
|
|
132
|
+
* handed off, or the relay refuses. The session is marked failed first.
|
|
133
|
+
*/
|
|
134
|
+
start(conversationId: string, target: string, envelope: HandoffEnvelope): Promise<HandoffSession>;
|
|
135
|
+
/**
|
|
136
|
+
* Send what the visitor typed to the target handling this conversation.
|
|
137
|
+
*
|
|
138
|
+
* @throws When nothing is handed off, or the target hasn't answered yet.
|
|
139
|
+
*/
|
|
140
|
+
send(conversationId: string, text: string): Promise<void>;
|
|
141
|
+
/**
|
|
142
|
+
* End a handoff at the visitor's request: "Back to the assistant".
|
|
143
|
+
*
|
|
144
|
+
* Cancels the task at the target. A target that won't cancel still loses the
|
|
145
|
+
* visitor: the handoff is ended here either way.
|
|
146
|
+
*/
|
|
147
|
+
end(conversationId: string): Promise<void>;
|
|
148
|
+
/**
|
|
149
|
+
* Pick up an open handoff after a page change or reload.
|
|
150
|
+
*
|
|
151
|
+
* Subscribes to the task while it runs, or reads it once when it is waiting
|
|
152
|
+
* on the visitor. Safe to call for any conversation, at any time: it does
|
|
153
|
+
* nothing when there is no open handoff or a stream is already open.
|
|
154
|
+
*/
|
|
155
|
+
resume(conversationId: string): Promise<void>;
|
|
156
|
+
/** Stop every stream and pending resume. The sessions stay saved for the next page. */
|
|
157
|
+
dispose(): void;
|
|
158
|
+
private client;
|
|
159
|
+
private keyHeader;
|
|
160
|
+
/** A fresh abort signal for a conversation's stream, closing the one before it. */
|
|
161
|
+
private openLive;
|
|
162
|
+
private closeLive;
|
|
163
|
+
/** Read a stream to its end, then resume it if the task is still running. */
|
|
164
|
+
private pump;
|
|
165
|
+
/** Read a task once, for a handoff that can't or needn't be streamed. */
|
|
166
|
+
private readOnce;
|
|
167
|
+
private apply;
|
|
168
|
+
private adopt;
|
|
169
|
+
private status;
|
|
170
|
+
private stateFor;
|
|
171
|
+
/** One message from the target: who is speaking, a hand-back, and the words. */
|
|
172
|
+
private message;
|
|
173
|
+
private parts;
|
|
174
|
+
private remember;
|
|
175
|
+
private setState;
|
|
176
|
+
private emit;
|
|
177
|
+
private now;
|
|
178
|
+
private load;
|
|
179
|
+
private save;
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/handoff/controller.ts"],"names":[],"mappings":"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"}
|