@nxisai/agent-harness 0.5.0 → 0.7.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.
Files changed (78) hide show
  1. package/README.md +38 -4
  2. package/dist/handoff/a2a.d.ts +152 -0
  3. package/dist/handoff/a2a.d.ts.map +1 -0
  4. package/dist/handoff/a2a.js +227 -0
  5. package/dist/handoff/a2a.js.map +1 -0
  6. package/dist/handoff/controller.d.ts +181 -0
  7. package/dist/handoff/controller.d.ts.map +1 -0
  8. package/dist/handoff/controller.js +543 -0
  9. package/dist/handoff/controller.js.map +1 -0
  10. package/dist/handoff/envelope.d.ts +130 -0
  11. package/dist/handoff/envelope.d.ts.map +1 -0
  12. package/dist/handoff/envelope.js +178 -0
  13. package/dist/handoff/envelope.js.map +1 -0
  14. package/dist/handoff/index.d.ts +16 -0
  15. package/dist/handoff/index.d.ts.map +1 -0
  16. package/dist/handoff/index.js +12 -0
  17. package/dist/handoff/index.js.map +1 -0
  18. package/dist/handoff/tools.d.ts +36 -0
  19. package/dist/handoff/tools.d.ts.map +1 -0
  20. package/dist/handoff/tools.js +155 -0
  21. package/dist/handoff/tools.js.map +1 -0
  22. package/dist/harness/harness.d.ts +4 -2
  23. package/dist/harness/harness.d.ts.map +1 -1
  24. package/dist/harness/harness.js +53 -12
  25. package/dist/harness/harness.js.map +1 -1
  26. package/dist/harness/renderer.d.ts +20 -0
  27. package/dist/harness/renderer.d.ts.map +1 -1
  28. package/dist/harness/router/coordinator.d.ts +13 -0
  29. package/dist/harness/router/coordinator.d.ts.map +1 -1
  30. package/dist/harness/router/coordinator.js +7 -0
  31. package/dist/harness/router/coordinator.js.map +1 -1
  32. package/dist/harness/tools/action-tools.d.ts +29 -0
  33. package/dist/harness/tools/action-tools.d.ts.map +1 -1
  34. package/dist/harness/tools/action-tools.js +64 -4
  35. package/dist/harness/tools/action-tools.js.map +1 -1
  36. package/dist/harness/tools/discovery.d.ts +12 -1
  37. package/dist/harness/tools/discovery.d.ts.map +1 -1
  38. package/dist/harness/tools/discovery.js +19 -1
  39. package/dist/harness/tools/discovery.js.map +1 -1
  40. package/dist/harness/tools/site-actions.d.ts +27 -0
  41. package/dist/harness/tools/site-actions.d.ts.map +1 -1
  42. package/dist/harness/tools/site-actions.js +47 -0
  43. package/dist/harness/tools/site-actions.js.map +1 -1
  44. package/dist/harness/tools/source.d.ts.map +1 -1
  45. package/dist/harness/tools/source.js +5 -2
  46. package/dist/harness/tools/source.js.map +1 -1
  47. package/dist/index.d.ts +2 -1
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +3 -1
  50. package/dist/index.js.map +1 -1
  51. package/dist/ui/action-card.d.ts +141 -0
  52. package/dist/ui/action-card.d.ts.map +1 -0
  53. package/dist/ui/action-card.js +384 -0
  54. package/dist/ui/action-card.js.map +1 -0
  55. package/dist/ui/agent-fullscreen.d.ts.map +1 -1
  56. package/dist/ui/agent-fullscreen.js +795 -17
  57. package/dist/ui/agent-fullscreen.js.map +1 -1
  58. package/dist/ui/tailwind.generated.d.ts +1 -1
  59. package/dist/ui/tailwind.generated.d.ts.map +1 -1
  60. package/dist/ui/tailwind.generated.js +1 -1
  61. package/dist/ui/tailwind.generated.js.map +1 -1
  62. package/package.json +1 -1
  63. package/src/handoff/a2a.ts +326 -0
  64. package/src/handoff/controller.ts +661 -0
  65. package/src/handoff/envelope.ts +216 -0
  66. package/src/handoff/index.ts +53 -0
  67. package/src/handoff/tools.ts +182 -0
  68. package/src/harness/harness.ts +55 -14
  69. package/src/harness/renderer.ts +22 -0
  70. package/src/harness/router/coordinator.ts +21 -0
  71. package/src/harness/tools/action-tools.ts +74 -4
  72. package/src/harness/tools/discovery.ts +20 -2
  73. package/src/harness/tools/site-actions.ts +58 -0
  74. package/src/harness/tools/source.ts +5 -2
  75. package/src/index.ts +4 -1
  76. package/src/ui/action-card.ts +496 -0
  77. package/src/ui/agent-fullscreen.ts +842 -16
  78. package/src/ui/tailwind.generated.ts +1 -1
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
 
@@ -221,6 +222,7 @@ Both flags go false at a specialist handoff, which swaps the system prompt and t
221
222
  | `mode` | `fullscreen` (default), `window`, `inline` | Cover the page; dock in the corner at `--window-w` × `--window-h` (400 × 620); or fill the box the host puts it in and stay open — for a copilot rail the host lays out. Below 520px a window covers the page anyway. |
222
223
  | `discover` | `"false"` | Skip remote MCP discovery. |
223
224
  | `site-actions` | `"false"` | Don't fetch the actions on other pages of the site. |
225
+ | `prompt-style` | `"classic"` | Use the separate ask, "Ready to send" and "Submitted" cards of earlier versions instead of the action card. |
224
226
  | `cache` | `"false"` | Do not store model files in Cache Storage. |
225
227
  | `model-host` | URL | Fetch weights from another origin. |
226
228
  | `model-path-template` | string | Path template under `model-host`. |
@@ -283,9 +285,19 @@ Given the tool the model picked and everything already known, the harness asks w
283
285
  |---|---|---|
284
286
  | read (`readOnlyHint`) | none | runs, no UI |
285
287
  | read | some | asks only for those, then runs |
286
- | write | none | confirm card, one Submit |
287
- | write | one | asks for one field, then confirms |
288
- | write | several | stepper, then confirms |
288
+ | write | none | action card: what will be sent, and Send |
289
+ | write | some | action card: what it has, the fields still needed, then Send |
290
+
291
+ ### The action card
292
+
293
+ A write, and a handoff to an agent or a person, is one message in the conversation rather than a series of cards. The card names who it goes to (the business the form belongs to, or the team or agent and whether they are available now), then moves through:
294
+
295
+ 1. **Fill in**: what the agent already has, editable, under "What I have", and anything still needed under "Still needed" (for a handoff to a team that is away, "Where should they reply?"). The button says what is missing until nothing is.
296
+ 2. **Review**: one sentence saying what will happen ("I'll send this quote form to Hatzel & Sons Plumbing. They'll reply to dana@example.com."), the details behind "What they'll get", and Send or Not now. A form on another page says it will be sent from there.
297
+ 3. **Sending**: the steps as they run, on this page or the one it opens.
298
+ 4. **Sent**, **Handed to …**, or **It didn't go through** with the form's reason and "Change and try again".
299
+
300
+ The card is saved with the conversation and replaces the step-by-step journey card for that request. `prompt-style="classic"` brings back the earlier cards; a custom renderer opts in to the same behaviour with `inlinePrompts` and `left` (see [`HarnessRenderer`](#harnessrenderer)).
289
301
 
290
302
  Values merge in this order, last wins: schema default, page context, saved preferences, model arguments.
291
303
 
@@ -397,6 +409,8 @@ interface HarnessRenderer {
397
409
  step?(step: AgentStep, all: AgentStep[]): void;
398
410
  dismissPrompt?(): void;
399
411
  stoppable?(canStop: boolean): void; // false once a tool call has gone out
412
+ inlinePrompts?: boolean; // prompts speak for themselves: no "One more thing for …" line
413
+ left?(tool, destination): void; // a confirmed call is leaving for its page, instead of "Opening …"
400
414
  }
401
415
  ```
402
416
 
@@ -435,7 +449,7 @@ const card = viewJourney(steps); // { title, steps, outcome, active, summary }
435
449
  ### `AgentCoordinator`
436
450
 
437
451
  ```ts
438
- new AgentCoordinator({ agents, rules, fallback, provider?, descriptions?, onRoute? })
452
+ new AgentCoordinator({ agents, rules, fallback, provider?, descriptions?, onRoute?, handoff? })
439
453
  coordinator.turn(goal, messages): Promise<TurnResult>
440
454
  coordinator.stop(): void // the classifier and the agent that took the turn
441
455
  coordinator.turn(goal, messages): Promise<TurnResult>
@@ -574,6 +588,26 @@ import { emitAsk, emitStep, emitConfirm, toV09 } from '@nxisai/agent-harness/a2u
574
588
 
575
589
  Messages are authored against A2UI v1.0 and downgraded with `toV09` for the published `@a2ui/lit` renderer.
576
590
 
591
+ ## Handoff to an agent or a person
592
+
593
+ 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.
594
+
595
+ ```ts
596
+ agent.handoff = {
597
+ relay: '/api/handoff',
598
+ targets: {
599
+ booking: { kind: 'agent', label: 'Booking', description: 'books a service visit' },
600
+ team: { kind: 'person', label: 'Our team' },
601
+ },
602
+ };
603
+ ```
604
+
605
+ 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.
606
+
607
+ 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.
608
+
609
+ [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.
610
+
577
611
  ## Requirements
578
612
 
579
613
  - 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":"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"}
@@ -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