@nxisai/agent-harness 0.4.1 → 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 +25 -2
- 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 +61 -16
- 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/action-tools.d.ts.map +1 -1
- package/dist/harness/tools/action-tools.js +7 -5
- package/dist/harness/tools/action-tools.js.map +1 -1
- package/dist/harness/tools/bridge.d.ts.map +1 -1
- package/dist/harness/tools/bridge.js +33 -12
- package/dist/harness/tools/bridge.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/harness/tools/local-context.d.ts +49 -9
- package/dist/harness/tools/local-context.d.ts.map +1 -1
- package/dist/harness/tools/local-context.js +377 -22
- package/dist/harness/tools/local-context.js.map +1 -1
- package/dist/harness/tools/open-page.d.ts +16 -0
- package/dist/harness/tools/open-page.d.ts.map +1 -1
- package/dist/harness/tools/open-page.js +45 -9
- package/dist/harness/tools/open-page.js.map +1 -1
- package/dist/harness/tools/retry.d.ts +53 -0
- package/dist/harness/tools/retry.d.ts.map +1 -0
- package/dist/harness/tools/retry.js +96 -0
- package/dist/harness/tools/retry.js.map +1 -0
- package/dist/harness/tools/schema.d.ts +18 -0
- package/dist/harness/tools/schema.d.ts.map +1 -1
- package/dist/harness/tools/schema.js +38 -0
- package/dist/harness/tools/schema.js.map +1 -1
- package/dist/harness/tools/site-actions.d.ts.map +1 -1
- package/dist/harness/tools/site-actions.js +3 -2
- package/dist/harness/tools/site-actions.js.map +1 -1
- package/dist/harness/tools/site-nav.d.ts +168 -0
- package/dist/harness/tools/site-nav.d.ts.map +1 -0
- package/dist/harness/tools/site-nav.js +316 -0
- package/dist/harness/tools/site-nav.js.map +1 -0
- package/dist/harness/tools/site-pages.d.ts +4 -0
- package/dist/harness/tools/site-pages.d.ts.map +1 -1
- package/dist/harness/tools/site-pages.js +19 -3
- package/dist/harness/tools/site-pages.js.map +1 -1
- package/dist/harness/tools/source.d.ts +9 -0
- package/dist/harness/tools/source.d.ts.map +1 -1
- package/dist/harness/tools/source.js +54 -14
- package/dist/harness/tools/source.js.map +1 -1
- package/dist/harness/tools/webmcp.d.ts +39 -3
- package/dist/harness/tools/webmcp.d.ts.map +1 -1
- package/dist/harness/tools/webmcp.js +93 -14
- package/dist/harness/tools/webmcp.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/agent-fullscreen.d.ts.map +1 -1
- package/dist/ui/agent-fullscreen.js +276 -2
- package/dist/ui/agent-fullscreen.js.map +1 -1
- package/dist/ui/tailwind.generated.d.ts +1 -1
- package/dist/ui/tailwind.generated.d.ts.map +1 -1
- package/dist/ui/tailwind.generated.js +1 -1
- package/dist/ui/tailwind.generated.js.map +1 -1
- package/package.json +2 -1
- package/src/a2ui/cards.ts +177 -0
- package/src/a2ui/emit.ts +147 -0
- package/src/a2ui/form.ts +117 -0
- package/src/a2ui/host.ts +73 -0
- package/src/a2ui/index.ts +22 -0
- package/src/a2ui/stepper.ts +211 -0
- package/src/a2ui/theme.ts +134 -0
- package/src/a2ui/to-v09.ts +118 -0
- package/src/engine.ts +701 -0
- 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 +981 -0
- package/src/harness/plan.ts +91 -0
- package/src/harness/policy.ts +59 -0
- package/src/harness/renderer.ts +208 -0
- package/src/harness/router/coordinator.ts +241 -0
- package/src/harness/router/rules.ts +65 -0
- package/src/harness/slots.ts +117 -0
- package/src/harness/steps.ts +156 -0
- package/src/harness/tools/action-tools.ts +870 -0
- package/src/harness/tools/bridge.ts +393 -0
- package/src/harness/tools/client.ts +26 -0
- package/src/harness/tools/discovery.ts +73 -0
- package/src/harness/tools/local-context.ts +480 -0
- package/src/harness/tools/open-page.ts +273 -0
- package/src/harness/tools/pending-action.ts +232 -0
- package/src/harness/tools/resume.ts +134 -0
- package/src/harness/tools/retry.ts +117 -0
- package/src/harness/tools/schema.ts +245 -0
- package/src/harness/tools/site-actions.ts +356 -0
- package/src/harness/tools/site-nav.ts +432 -0
- package/src/harness/tools/site-pages.ts +288 -0
- package/src/harness/tools/source.ts +353 -0
- package/src/harness/tools/webmcp.ts +200 -0
- package/src/index.ts +196 -0
- package/src/providers/capability.ts +167 -0
- package/src/providers/proxy.ts +191 -0
- package/src/providers/types.ts +150 -0
- package/src/types.ts +258 -0
- package/src/ui/agent-fullscreen.ts +4262 -0
- package/src/ui/agent.tw.css +49 -0
- package/src/ui/config.ts +138 -0
- package/src/ui/index.ts +32 -0
- package/src/ui/journey.ts +57 -0
- package/src/ui/tailwind.generated.ts +2 -0
- package/src/webmcp-contract.ts +65 -0
|
@@ -0,0 +1,326 @@
|
|
|
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
|
+
|
|
18
|
+
/** The A2A protocol version this client speaks, sent as `A2A-Version`. */
|
|
19
|
+
export const A2A_VERSION = '1.0';
|
|
20
|
+
|
|
21
|
+
/** A task's state, by its A2A 1.0 name. */
|
|
22
|
+
export type A2ATaskState =
|
|
23
|
+
| 'TASK_STATE_UNSPECIFIED'
|
|
24
|
+
| 'TASK_STATE_SUBMITTED'
|
|
25
|
+
| 'TASK_STATE_WORKING'
|
|
26
|
+
| 'TASK_STATE_COMPLETED'
|
|
27
|
+
| 'TASK_STATE_FAILED'
|
|
28
|
+
| 'TASK_STATE_CANCELED'
|
|
29
|
+
| 'TASK_STATE_INPUT_REQUIRED'
|
|
30
|
+
| 'TASK_STATE_REJECTED'
|
|
31
|
+
| 'TASK_STATE_AUTH_REQUIRED';
|
|
32
|
+
|
|
33
|
+
/** One piece of a message or artifact. Which member is present says what it is. */
|
|
34
|
+
export interface A2APart {
|
|
35
|
+
text?: string;
|
|
36
|
+
data?: unknown;
|
|
37
|
+
url?: string;
|
|
38
|
+
raw?: string;
|
|
39
|
+
filename?: string;
|
|
40
|
+
mediaType?: string;
|
|
41
|
+
metadata?: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A message, from the visitor (`ROLE_USER`) or the target (`ROLE_AGENT`). */
|
|
45
|
+
export interface A2AMessage {
|
|
46
|
+
messageId: string;
|
|
47
|
+
role: 'ROLE_USER' | 'ROLE_AGENT';
|
|
48
|
+
parts: A2APart[];
|
|
49
|
+
contextId?: string;
|
|
50
|
+
taskId?: string;
|
|
51
|
+
metadata?: Record<string, unknown>;
|
|
52
|
+
extensions?: string[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Where a task stands, and the message that goes with it, if any. */
|
|
56
|
+
export interface A2ATaskStatus {
|
|
57
|
+
state: A2ATaskState;
|
|
58
|
+
message?: A2AMessage;
|
|
59
|
+
timestamp?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Output a task produced. */
|
|
63
|
+
export interface A2AArtifact {
|
|
64
|
+
artifactId?: string;
|
|
65
|
+
name?: string;
|
|
66
|
+
parts: A2APart[];
|
|
67
|
+
metadata?: Record<string, unknown>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A unit of work at the target. */
|
|
71
|
+
export interface A2ATask {
|
|
72
|
+
id: string;
|
|
73
|
+
contextId?: string;
|
|
74
|
+
status: A2ATaskStatus;
|
|
75
|
+
artifacts?: A2AArtifact[];
|
|
76
|
+
history?: A2AMessage[];
|
|
77
|
+
metadata?: Record<string, unknown>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** One event on a stream, or the result of a send: exactly one member is set. */
|
|
81
|
+
export interface A2AStreamResponse {
|
|
82
|
+
task?: A2ATask;
|
|
83
|
+
message?: A2AMessage;
|
|
84
|
+
statusUpdate?: { taskId: string; contextId?: string; status: A2ATaskStatus; metadata?: Record<string, unknown> };
|
|
85
|
+
artifactUpdate?: { taskId: string; contextId?: string; artifact: A2AArtifact; append?: boolean; lastChunk?: boolean };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** An error the relay or the target answered with. */
|
|
89
|
+
export class A2AError extends Error {
|
|
90
|
+
constructor(
|
|
91
|
+
message: string,
|
|
92
|
+
/** The JSON-RPC error code, or the HTTP status when the relay refused before A2A. */
|
|
93
|
+
readonly code: number,
|
|
94
|
+
/** The HTTP status of the response. */
|
|
95
|
+
readonly status?: number,
|
|
96
|
+
/** Seconds to wait, from `Retry-After`, when the relay is rate limiting. */
|
|
97
|
+
readonly retryAfter?: number,
|
|
98
|
+
) {
|
|
99
|
+
super(message);
|
|
100
|
+
this.name = 'A2AError';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Options for {@link A2AClient}. */
|
|
105
|
+
export interface A2AClientOptions {
|
|
106
|
+
/** Where requests go: the relay's URL for one target, `/api/handoff/booking`. */
|
|
107
|
+
endpoint: string;
|
|
108
|
+
/** Extensions to announce in `A2A-Extensions`. */
|
|
109
|
+
extensions?: string[];
|
|
110
|
+
/** `fetch` to use. Defaults to the global. */
|
|
111
|
+
fetchImpl?: typeof fetch;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** What a call returned, with the response headers the relay may have set. */
|
|
115
|
+
export interface A2AReply<T> {
|
|
116
|
+
result: T;
|
|
117
|
+
headers: Headers;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** A stream of events, and the response headers it opened with. */
|
|
121
|
+
export interface A2AStream {
|
|
122
|
+
events: AsyncGenerator<A2AStreamResponse, void, undefined>;
|
|
123
|
+
headers: Headers;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let nextId = 1;
|
|
127
|
+
|
|
128
|
+
/** A2A over JSON-RPC to one endpoint. */
|
|
129
|
+
export class A2AClient {
|
|
130
|
+
constructor(private readonly opts: A2AClientOptions) {}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Call a method that answers once: `SendMessage`, `GetTask`, `CancelTask`.
|
|
134
|
+
*
|
|
135
|
+
* @throws {@link A2AError} when the relay refuses or the target answers with an error.
|
|
136
|
+
*/
|
|
137
|
+
async call<T>(
|
|
138
|
+
method: string,
|
|
139
|
+
params: unknown,
|
|
140
|
+
init: { signal?: AbortSignal; headers?: Record<string, string> } = {},
|
|
141
|
+
): Promise<A2AReply<T>> {
|
|
142
|
+
const res = await this.post(method, params, 'application/json', init);
|
|
143
|
+
const body = (await res.json().catch(() => null)) as { result?: T; error?: { code: number; message: string } } | null;
|
|
144
|
+
if (!body) throw new A2AError(`Empty answer to ${method}`, -32603, res.status);
|
|
145
|
+
if (body.error) throw new A2AError(body.error.message, body.error.code, res.status);
|
|
146
|
+
return { result: body.result as T, headers: res.headers };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Call a method that answers with a stream: `SendStreamingMessage`, `SubscribeToTask`.
|
|
151
|
+
*
|
|
152
|
+
* The generator ends when the server closes the stream, which it does at a
|
|
153
|
+
* terminal state, or earlier if the host cuts the response off.
|
|
154
|
+
*
|
|
155
|
+
* @throws {@link A2AError} when the relay refuses; an error event on the stream
|
|
156
|
+
* is thrown from the generator.
|
|
157
|
+
*/
|
|
158
|
+
async stream(
|
|
159
|
+
method: string,
|
|
160
|
+
params: unknown,
|
|
161
|
+
init: { signal?: AbortSignal; headers?: Record<string, string> } = {},
|
|
162
|
+
): Promise<A2AStream> {
|
|
163
|
+
const res = await this.post(method, params, 'text/event-stream', init);
|
|
164
|
+
const type = res.headers.get('content-type') ?? '';
|
|
165
|
+
// A server may answer a streaming request with a single JSON response: an
|
|
166
|
+
// error, or a target without streaming that replied at once.
|
|
167
|
+
if (!type.includes('text/event-stream')) {
|
|
168
|
+
const body = (await res.json().catch(() => null)) as
|
|
169
|
+
| { result?: A2AStreamResponse; error?: { code: number; message: string } }
|
|
170
|
+
| null;
|
|
171
|
+
if (body?.error) throw new A2AError(body.error.message, body.error.code, res.status);
|
|
172
|
+
const once = body?.result;
|
|
173
|
+
return {
|
|
174
|
+
headers: res.headers,
|
|
175
|
+
events: (async function* () {
|
|
176
|
+
if (once) yield normaliseResponse(once);
|
|
177
|
+
})(),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return { headers: res.headers, events: readEvents(res, init.signal) };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private async post(
|
|
184
|
+
method: string,
|
|
185
|
+
params: unknown,
|
|
186
|
+
accept: string,
|
|
187
|
+
init: { signal?: AbortSignal; headers?: Record<string, string> },
|
|
188
|
+
): Promise<Response> {
|
|
189
|
+
const doFetch = this.opts.fetchImpl ?? fetch;
|
|
190
|
+
const res = await doFetch(this.opts.endpoint, {
|
|
191
|
+
method: 'POST',
|
|
192
|
+
credentials: 'same-origin',
|
|
193
|
+
signal: init.signal,
|
|
194
|
+
headers: {
|
|
195
|
+
'Content-Type': 'application/json',
|
|
196
|
+
Accept: accept,
|
|
197
|
+
'A2A-Version': A2A_VERSION,
|
|
198
|
+
...(this.opts.extensions?.length ? { 'A2A-Extensions': this.opts.extensions.join(',') } : {}),
|
|
199
|
+
...init.headers,
|
|
200
|
+
},
|
|
201
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: nextId++, method, params }),
|
|
202
|
+
});
|
|
203
|
+
if (!res.ok) {
|
|
204
|
+
const retry = Number(res.headers.get('retry-after'));
|
|
205
|
+
let message = `${method} refused (${res.status})`;
|
|
206
|
+
try {
|
|
207
|
+
const body = (await res.json()) as { error?: { message?: string } | string; message?: string };
|
|
208
|
+
const text = typeof body.error === 'string' ? body.error : (body.error?.message ?? body.message);
|
|
209
|
+
if (text) message = text;
|
|
210
|
+
} catch {
|
|
211
|
+
// Not JSON: keep the status line.
|
|
212
|
+
}
|
|
213
|
+
throw new A2AError(message, res.status, res.status, Number.isFinite(retry) && retry > 0 ? retry : undefined);
|
|
214
|
+
}
|
|
215
|
+
return res;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Server-sent events off a response body, as A2A stream responses. */
|
|
220
|
+
async function* readEvents(res: Response, signal?: AbortSignal): AsyncGenerator<A2AStreamResponse, void, undefined> {
|
|
221
|
+
if (!res.body) return;
|
|
222
|
+
const reader = res.body.getReader();
|
|
223
|
+
const decoder = new TextDecoder();
|
|
224
|
+
let buffer = '';
|
|
225
|
+
try {
|
|
226
|
+
while (true) {
|
|
227
|
+
if (signal?.aborted) return;
|
|
228
|
+
const { value, done } = await reader.read();
|
|
229
|
+
if (done) break;
|
|
230
|
+
buffer += decoder.decode(value, { stream: true });
|
|
231
|
+
let cut: number;
|
|
232
|
+
while ((cut = boundary(buffer)) >= 0) {
|
|
233
|
+
const block = buffer.slice(0, cut);
|
|
234
|
+
buffer = buffer.slice(cut).replace(/^(\r?\n){2}/, '');
|
|
235
|
+
const event = parseBlock(block);
|
|
236
|
+
if (event) yield event;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const tail = parseBlock(buffer);
|
|
240
|
+
if (tail) yield tail;
|
|
241
|
+
} finally {
|
|
242
|
+
reader.releaseLock?.();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Where the first complete event block ends, or -1. */
|
|
247
|
+
function boundary(buffer: string): number {
|
|
248
|
+
const m = /\r?\n\r?\n/.exec(buffer);
|
|
249
|
+
return m ? m.index : -1;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** One SSE block's `data:` lines, as a stream response; null for comments and keep-alives. */
|
|
253
|
+
function parseBlock(block: string): A2AStreamResponse | null {
|
|
254
|
+
const data = block
|
|
255
|
+
.split(/\r?\n/)
|
|
256
|
+
.filter((line) => line.startsWith('data:'))
|
|
257
|
+
.map((line) => line.slice(5).replace(/^ /, ''))
|
|
258
|
+
.join('\n');
|
|
259
|
+
if (!data.trim()) return null;
|
|
260
|
+
const body = JSON.parse(data) as { result?: A2AStreamResponse; error?: { code: number; message: string } };
|
|
261
|
+
if (body.error) throw new A2AError(body.error.message, body.error.code);
|
|
262
|
+
return body.result ? normaliseResponse(body.result) : null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** 0.3 event and state names, read as 1.0's. */
|
|
266
|
+
const LEGACY_STATES: Record<string, A2ATaskState> = {
|
|
267
|
+
submitted: 'TASK_STATE_SUBMITTED',
|
|
268
|
+
working: 'TASK_STATE_WORKING',
|
|
269
|
+
completed: 'TASK_STATE_COMPLETED',
|
|
270
|
+
failed: 'TASK_STATE_FAILED',
|
|
271
|
+
canceled: 'TASK_STATE_CANCELED',
|
|
272
|
+
'input-required': 'TASK_STATE_INPUT_REQUIRED',
|
|
273
|
+
rejected: 'TASK_STATE_REJECTED',
|
|
274
|
+
'auth-required': 'TASK_STATE_AUTH_REQUIRED',
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/** A task state by its 1.0 name, whichever version sent it. */
|
|
278
|
+
export function taskState(raw: unknown): A2ATaskState {
|
|
279
|
+
const s = String(raw ?? '');
|
|
280
|
+
if (s.startsWith('TASK_STATE_')) return s as A2ATaskState;
|
|
281
|
+
return LEGACY_STATES[s] ?? 'TASK_STATE_UNSPECIFIED';
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** A result in 1.0's shape, whether it came as 1.0 or 0.3 (`kind` discriminators). */
|
|
285
|
+
export function normaliseResponse(raw: A2AStreamResponse | Record<string, unknown>): A2AStreamResponse {
|
|
286
|
+
const r = raw as Record<string, unknown>;
|
|
287
|
+
if (r.task || r.message || r.statusUpdate || r.artifactUpdate) {
|
|
288
|
+
const out = { ...(r as A2AStreamResponse) };
|
|
289
|
+
if (out.task) out.task = { ...out.task, status: { ...out.task.status, state: taskState(out.task.status?.state) } };
|
|
290
|
+
if (out.statusUpdate) {
|
|
291
|
+
out.statusUpdate = { ...out.statusUpdate, status: { ...out.statusUpdate.status, state: taskState(out.statusUpdate.status?.state) } };
|
|
292
|
+
}
|
|
293
|
+
return out;
|
|
294
|
+
}
|
|
295
|
+
switch (r.kind) {
|
|
296
|
+
case 'task':
|
|
297
|
+
return normaliseResponse({ task: r as unknown as A2ATask });
|
|
298
|
+
case 'message':
|
|
299
|
+
return { message: r as unknown as A2AMessage };
|
|
300
|
+
case 'status-update':
|
|
301
|
+
return normaliseResponse({ statusUpdate: r as unknown as A2AStreamResponse['statusUpdate'] });
|
|
302
|
+
case 'artifact-update':
|
|
303
|
+
return { artifactUpdate: r as unknown as A2AStreamResponse['artifactUpdate'] };
|
|
304
|
+
default:
|
|
305
|
+
// A bare task, as `GetTask` returns one.
|
|
306
|
+
if (typeof r.id === 'string' && r.status) return normaliseResponse({ task: r as unknown as A2ATask });
|
|
307
|
+
return {};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Whether a state ends the task. */
|
|
312
|
+
export function isTerminal(state: A2ATaskState): boolean {
|
|
313
|
+
return (
|
|
314
|
+
state === 'TASK_STATE_COMPLETED' ||
|
|
315
|
+
state === 'TASK_STATE_FAILED' ||
|
|
316
|
+
state === 'TASK_STATE_CANCELED' ||
|
|
317
|
+
state === 'TASK_STATE_REJECTED'
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** A message id, unique enough for one conversation. */
|
|
322
|
+
export function newMessageId(): string {
|
|
323
|
+
const c = (globalThis as { crypto?: Crypto }).crypto;
|
|
324
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
325
|
+
return `m-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
326
|
+
}
|