@datalayer/agent-runtimes 1.3.74 → 1.3.75

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 (55) hide show
  1. package/lib/chat/ChatFloating.d.ts +17 -1
  2. package/lib/chat/ChatFloating.js +52 -5
  3. package/lib/chat/assistant/AssistantContextMenu.d.ts +67 -0
  4. package/lib/chat/assistant/AssistantContextMenu.js +68 -0
  5. package/lib/chat/assistant/AssistantStage.d.ts +95 -1
  6. package/lib/chat/assistant/AssistantStage.js +227 -7
  7. package/lib/chat/assistant/BalloonParts.d.ts +15 -1
  8. package/lib/chat/assistant/BalloonParts.js +26 -9
  9. package/lib/chat/assistant/BalloonVisual.d.ts +10 -2
  10. package/lib/chat/assistant/BalloonVisual.js +3 -3
  11. package/lib/chat/assistant/SpeechBalloon.d.ts +47 -1
  12. package/lib/chat/assistant/SpeechBalloon.js +60 -8
  13. package/lib/chat/base/ChatBase.js +51 -1
  14. package/lib/chat/indicators/TypingDots.d.ts +13 -0
  15. package/lib/chat/indicators/TypingDots.js +21 -0
  16. package/lib/chat/messages/ChatMessageList.js +1 -15
  17. package/lib/components/inspector/AgentInspector.d.ts +59 -0
  18. package/lib/components/inspector/AgentInspector.js +220 -0
  19. package/lib/components/inspector/AgentInspectorDialog.d.ts +19 -0
  20. package/lib/components/inspector/AgentInspectorDialog.js +9 -0
  21. package/lib/components/inspector/JsonTree.d.ts +24 -0
  22. package/lib/components/inspector/JsonTree.js +89 -0
  23. package/lib/components/inspector/a2aInspect.d.ts +56 -0
  24. package/lib/components/inspector/a2aInspect.js +478 -0
  25. package/lib/components/inspector/agentInspector.d.ts +179 -0
  26. package/lib/components/inspector/agentInspector.js +267 -0
  27. package/lib/components/inspector/chatInspect.d.ts +38 -0
  28. package/lib/components/inspector/chatInspect.js +65 -0
  29. package/lib/components/inspector/classify.d.ts +19 -0
  30. package/lib/components/inspector/classify.js +42 -0
  31. package/lib/components/inspector/index.d.ts +14 -0
  32. package/lib/components/inspector/index.js +18 -0
  33. package/lib/components/teams/A2ATeamGraph.d.ts +42 -1
  34. package/lib/components/teams/A2ATeamGraph.js +24 -7
  35. package/lib/components/teams/NotebookPreviewView.js +9 -1
  36. package/lib/components/teams/TeamNotebook.d.ts +3 -1
  37. package/lib/components/teams/TeamNotebook.js +3 -3
  38. package/lib/components/teams/jupyterThemeVariables.d.ts +9 -0
  39. package/lib/components/teams/jupyterThemeVariables.js +441 -0
  40. package/lib/components/teams/teamConnections.d.ts +9 -1
  41. package/lib/components/teams/teamConnections.js +20 -0
  42. package/lib/components/teams/useA2ATeam.d.ts +17 -4
  43. package/lib/components/teams/useA2ATeam.js +87 -23
  44. package/lib/examples/AgentA2ATeamExample.d.ts +13 -0
  45. package/lib/examples/AgentA2ATeamExample.js +61 -13
  46. package/lib/examples/AssistantExample.js +62 -2
  47. package/lib/loop/apps/AppRenderer.js +1 -1
  48. package/lib/loop/core/index.d.ts +17 -0
  49. package/lib/loop/core/index.js +1 -0
  50. package/lib/loop/plugins/assistant/index.d.ts +1 -1
  51. package/lib/loop/plugins/assistant/index.js +8 -3
  52. package/lib/runtimes/browser/a2aPeer.d.ts +4 -1
  53. package/lib/types/chat.d.ts +7 -0
  54. package/package.json +1 -1
  55. package/scripts/codegen/generate_jupyter_theme_variables.mjs +57 -0
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The A2A traffic between two agents, as the Agent Inspector records it: a
3
+ * fetch that hands every request on and records what went (the JSON-RPC
4
+ * request: its method, its message) and what came back — a JSON-RPC
5
+ * response, or each server-sent event of a stream (the task, its status
6
+ * updates, its artifact updates, a message) — with its task, context and
7
+ * state, whole.
8
+ *
9
+ * It sits under the A2A SDK (`ClientFactory`'s `JsonRpcTransportFactory`
10
+ * takes a `fetchImpl`): the SDK reads the response it is handed, and the
11
+ * inspector reads a copy of its body. Nothing it records changes what the
12
+ * SDK sees, and a key sent as a header is never recorded.
13
+ *
14
+ * A peer served by agent-runtimes tells each tool call of its agent on a
15
+ * `working` status (`{tool_call}`, then `{tool_result}`): each call is
16
+ * recorded besides as one entry of its own, the peer's, from its start to
17
+ * its end, linked to the two statuses — an MCP call, a skill, or a tool its
18
+ * runtime runs, by who claims its name. The text chunks of one artifact are
19
+ * one entry, their count kept.
20
+ *
21
+ * {@link linkA2APeerEvent} attaches what the asker made of an event (an
22
+ * `A2APeerEvent`) to the entry it came from.
23
+ *
24
+ * @module components/inspector/a2aInspect
25
+ */
26
+ import type { A2APeerEvent } from '../../runtimes/browser/a2aPeer';
27
+ import { type AgentInspectorEntry, type AgentInspectorSink, type ToolClass } from './agentInspector';
28
+ export type A2AInspectOptions = {
29
+ sink: AgentInspectorSink;
30
+ /** The agent that asks: `Sales`. */
31
+ asker: string;
32
+ /** The agent asked: `Accounting`. */
33
+ peer: string;
34
+ /**
35
+ * Where a peer's tool call comes from — an MCP server's, a skill, … — and
36
+ * its mark (`classifyToolCall`, or a team's `teamToolClassifier`).
37
+ * Without it, a tool the peer's runtime runs (`backend-tool`).
38
+ */
39
+ classifyTool?: (name: string, args?: Record<string, unknown>) => ToolClass;
40
+ };
41
+ /** The text of an A2A 1.0 message's or artifact's parts, as JSON carries them. */
42
+ export declare function partsText(parts: unknown): string;
43
+ /** `TASK_STATE_WORKING` is `working`. */
44
+ export declare function stateWord(state: unknown): string;
45
+ /**
46
+ * A fetch that records the A2A traffic it carries into `options.sink`, and
47
+ * hands the SDK what the network gave.
48
+ */
49
+ export declare function inspectA2AFetch(base: typeof globalThis.fetch, options: A2AInspectOptions): typeof globalThis.fetch;
50
+ /**
51
+ * The linker the A2A capture adds to its sink: an `A2APeerEvent` noted
52
+ * (`sink.note`) is attached to the entry it came from.
53
+ */
54
+ export declare function a2aEventLinker(event: unknown): ((entry: AgentInspectorEntry) => boolean) | undefined;
55
+ /** Attach what the asker made of an event to the A2A entry it came from. */
56
+ export declare function linkA2APeerEvent(sink: AgentInspectorSink, event: A2APeerEvent): void;
@@ -0,0 +1,478 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ import { argumentsLine, formatBytes, jsonBytes, shortLine, } from './agentInspector';
6
+ const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
7
+ /** The text of an A2A 1.0 message's or artifact's parts, as JSON carries them. */
8
+ export function partsText(parts) {
9
+ return Array.isArray(parts)
10
+ ? parts
11
+ .map(part => isObject(part) && typeof part.text === 'string' ? part.text : '')
12
+ .join('')
13
+ : '';
14
+ }
15
+ /** `TASK_STATE_WORKING` is `working`. */
16
+ export function stateWord(state) {
17
+ return typeof state === 'string'
18
+ ? state
19
+ .replace(/^TASK_STATE_/, '')
20
+ .toLowerCase()
21
+ .replace(/_/g, ' ')
22
+ : '';
23
+ }
24
+ /** A JSON-RPC request's body, if the request carries one. */
25
+ function rpcOf(body) {
26
+ if (typeof body !== 'string') {
27
+ return null;
28
+ }
29
+ try {
30
+ const parsed = JSON.parse(body);
31
+ return isObject(parsed) && parsed.jsonrpc === '2.0' ? parsed : null;
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ /** What a request asks, in a line. */
38
+ function requestSummary(method, params) {
39
+ const message = isObject(params) ? params.message : undefined;
40
+ const text = isObject(message) ? partsText(message.parts) : '';
41
+ if (text) {
42
+ return shortLine(text);
43
+ }
44
+ if (isObject(params) && typeof params.id === 'string') {
45
+ return `${method} ${params.id}`;
46
+ }
47
+ return method;
48
+ }
49
+ /** The step a status message carries: a tool call or its end. */
50
+ function stepOf(message) {
51
+ if (!isObject(message) || !Array.isArray(message.parts)) {
52
+ return undefined;
53
+ }
54
+ for (const part of message.parts) {
55
+ const data = isObject(part) ? part.data : undefined;
56
+ if (!isObject(data)) {
57
+ continue;
58
+ }
59
+ const call = isObject(data.tool_call) ? data.tool_call : undefined;
60
+ const done = isObject(data.tool_result) ? data.tool_result : undefined;
61
+ const told = call ?? done;
62
+ if (told && typeof told.name === 'string') {
63
+ return {
64
+ ...(typeof told.id === 'string' ? { toolId: told.id } : {}),
65
+ name: told.name,
66
+ ended: !call,
67
+ ...(call && isObject(call.arguments)
68
+ ? { args: call.arguments }
69
+ : {}),
70
+ ...(done && 'result' in done ? { result: done.result } : {}),
71
+ ...(done && done.error ? { error: String(done.error) } : {}),
72
+ ...(typeof told.note === 'string' && told.note
73
+ ? { note: told.note }
74
+ : {}),
75
+ };
76
+ }
77
+ }
78
+ return undefined;
79
+ }
80
+ /** Records the stream of one request, event by event. */
81
+ class StreamRecorder {
82
+ options;
83
+ method;
84
+ rpcId;
85
+ /** The entry of each artifact being streamed, by its id. */
86
+ artifacts = new Map();
87
+ constructor(options, method, rpcId) {
88
+ this.options = options;
89
+ this.method = method;
90
+ this.rpcId = rpcId;
91
+ }
92
+ classify(name, args) {
93
+ return (this.options.classifyTool?.(name, args) ?? { source: 'backend-tool' });
94
+ }
95
+ /** One JSON-RPC message of the response: a response, or an event of a stream. */
96
+ record(message, streamed) {
97
+ const { sink, asker, peer } = this.options;
98
+ const direction = { from: peer, to: asker };
99
+ const base = { actor: peer, source: 'a2a', direction };
100
+ if (!isObject(message)) {
101
+ return;
102
+ }
103
+ if (isObject(message.error)) {
104
+ const error = message.error;
105
+ sink.push({
106
+ ...base,
107
+ kind: 'error',
108
+ name: this.method,
109
+ status: 'failed',
110
+ summary: shortLine(`${String(error.code ?? '')} ${String(error.message ?? '')}`),
111
+ error: String(error.message ?? ''),
112
+ a2a: { method: this.method, rpcId: this.rpcId },
113
+ payload: message,
114
+ });
115
+ return;
116
+ }
117
+ const result = isObject(message.result) ? message.result : undefined;
118
+ if (!result) {
119
+ sink.push({
120
+ ...base,
121
+ kind: 'response',
122
+ name: this.method,
123
+ summary: this.method,
124
+ a2a: { method: this.method, rpcId: this.rpcId },
125
+ payload: message,
126
+ });
127
+ return;
128
+ }
129
+ const a2a = { method: this.method, rpcId: this.rpcId };
130
+ if (isObject(result.task)) {
131
+ const task = result.task;
132
+ const status = isObject(task.status) ? task.status : {};
133
+ sink.push({
134
+ ...base,
135
+ kind: 'task',
136
+ summary: `Task ${stateWord(status.state)}`,
137
+ a2a: {
138
+ ...a2a,
139
+ taskId: String(task.id ?? ''),
140
+ contextId: String(task.contextId ?? ''),
141
+ state: String(status.state ?? ''),
142
+ },
143
+ payload: message,
144
+ });
145
+ }
146
+ else if (isObject(result.statusUpdate)) {
147
+ this.status(result.statusUpdate, message, a2a);
148
+ }
149
+ else if (isObject(result.artifactUpdate)) {
150
+ this.artifact(result.artifactUpdate, message, a2a);
151
+ }
152
+ else if (isObject(result.message)) {
153
+ const reply = result.message;
154
+ sink.push({
155
+ ...base,
156
+ kind: 'message',
157
+ summary: shortLine(partsText(reply.parts)) || 'A message',
158
+ a2a: {
159
+ ...a2a,
160
+ taskId: String(reply.taskId ?? ''),
161
+ contextId: String(reply.contextId ?? ''),
162
+ },
163
+ payload: message,
164
+ });
165
+ }
166
+ else {
167
+ sink.push({
168
+ ...base,
169
+ kind: streamed ? 'event' : 'response',
170
+ summary: this.method,
171
+ a2a,
172
+ payload: message,
173
+ });
174
+ }
175
+ }
176
+ status(update, message, a2a) {
177
+ const { sink, asker, peer } = this.options;
178
+ const status = isObject(update.status) ? update.status : {};
179
+ const taskId = String(update.taskId ?? '');
180
+ const state = String(status.state ?? '');
181
+ const step = stepOf(status.message);
182
+ const text = partsText(isObject(status.message) ? status.message.parts : undefined);
183
+ const summary = step
184
+ ? step.ended
185
+ ? `${stateWord(state)} · ${step.error ? `${step.name} failed` : `${step.name} returned`}`
186
+ : `${stateWord(state)} · calls ${step.name}(${argumentsLine(step.args)})`
187
+ : `${stateWord(state)}${text ? ` · ${shortLine(text)}` : ''}`;
188
+ const failed = /FAILED|REJECTED|CANCELED/.test(state);
189
+ const id = sink.push({
190
+ actor: peer,
191
+ source: 'a2a',
192
+ direction: { from: peer, to: asker },
193
+ kind: 'status-update',
194
+ summary,
195
+ ...(failed ? { status: 'failed' } : {}),
196
+ a2a: {
197
+ ...a2a,
198
+ taskId,
199
+ contextId: String(update.contextId ?? ''),
200
+ state,
201
+ ...(step
202
+ ? {
203
+ step: { toolId: step.toolId, name: step.name, ended: step.ended },
204
+ }
205
+ : {}),
206
+ },
207
+ payload: message,
208
+ });
209
+ if (!step) {
210
+ return;
211
+ }
212
+ // The call, as one entry of its own, the peer's: from its start to its end.
213
+ const key = `a2a:${taskId}:${step.toolId ?? step.name}`;
214
+ if (!step.ended) {
215
+ const kind = this.classify(step.name, step.args);
216
+ sink.start({
217
+ key,
218
+ actor: peer,
219
+ source: kind.source,
220
+ ...(kind.icon ? { icon: kind.icon } : {}),
221
+ ...(kind.emoji ? { emoji: kind.emoji } : {}),
222
+ kind: 'tool-call',
223
+ name: step.name,
224
+ summary: step.note ?? argumentsLine(step.args),
225
+ payload: step.args ?? {},
226
+ links: [id],
227
+ });
228
+ }
229
+ else if (sink.running(key)) {
230
+ sink.end(key, {
231
+ status: step.error ? 'failed' : 'done',
232
+ result: step.result,
233
+ ...(step.error ? { error: step.error } : {}),
234
+ links: [id],
235
+ });
236
+ }
237
+ }
238
+ artifact(update, message, a2a) {
239
+ const { sink, asker, peer } = this.options;
240
+ const artifact = isObject(update.artifact) ? update.artifact : {};
241
+ const artifactId = String(artifact.artifactId ?? '');
242
+ const text = partsText(artifact.parts);
243
+ const parts = Array.isArray(artifact.parts) ? artifact.parts : [];
244
+ const media = parts
245
+ .map(part => (isObject(part) ? part.mediaType : undefined))
246
+ .find(type => typeof type === 'string' && type && !type.startsWith('text/'));
247
+ const known = this.artifacts.get(artifactId);
248
+ if (known && update.append && !update.lastChunk) {
249
+ // A chunk of an artifact being streamed: one entry, its text growing.
250
+ known.text += text;
251
+ known.events.push(message);
252
+ const chunks = known.events.length;
253
+ const words = known.text;
254
+ sink.update(known.id, entry => ({
255
+ summary: `${String(artifact.name ?? 'artifact')} · ${shortLine(words)}`,
256
+ payload: [...known.events],
257
+ a2a: { ...entry.a2a, chunks },
258
+ }));
259
+ return;
260
+ }
261
+ const name = String(artifact.name ?? (artifactId || 'artifact'));
262
+ const summary = media
263
+ ? `${name} · ${String(media)}, ${formatBytes(jsonBytes(message))}`
264
+ : `${name} · ${shortLine(text)}`;
265
+ const id = sink.push({
266
+ actor: peer,
267
+ source: 'a2a',
268
+ direction: { from: peer, to: asker },
269
+ kind: 'artifact-update',
270
+ name,
271
+ summary,
272
+ a2a: {
273
+ ...a2a,
274
+ taskId: String(update.taskId ?? ''),
275
+ contextId: String(update.contextId ?? ''),
276
+ artifactId,
277
+ chunks: 1,
278
+ },
279
+ payload: message,
280
+ });
281
+ if (update.append && !update.lastChunk) {
282
+ this.artifacts.set(artifactId, { id, text, events: [message] });
283
+ }
284
+ else {
285
+ this.artifacts.delete(artifactId);
286
+ }
287
+ }
288
+ }
289
+ /** The JSON of each event of a server-sent stream, as it arrives. */
290
+ async function readEvents(body, onEvent) {
291
+ const reader = body.getReader();
292
+ const decoder = new TextDecoder();
293
+ let buffer = '';
294
+ const flush = (block) => {
295
+ const data = block
296
+ .split(/\r?\n/)
297
+ .filter(line => line.startsWith('data:'))
298
+ .map(line => line.slice(5).replace(/^ /, ''))
299
+ .join('\n');
300
+ if (!data) {
301
+ return;
302
+ }
303
+ try {
304
+ onEvent(JSON.parse(data));
305
+ }
306
+ catch {
307
+ onEvent({ unparsed: data });
308
+ }
309
+ };
310
+ for (;;) {
311
+ const { done, value } = await reader.read();
312
+ if (done) {
313
+ break;
314
+ }
315
+ buffer += decoder.decode(value, { stream: true });
316
+ let at;
317
+ while ((at = buffer.search(/\r?\n\r?\n/)) >= 0) {
318
+ const block = buffer.slice(0, at);
319
+ buffer = buffer.slice(at).replace(/^\r?\n\r?\n/, '');
320
+ flush(block);
321
+ }
322
+ }
323
+ if (buffer.trim()) {
324
+ flush(buffer);
325
+ }
326
+ }
327
+ /**
328
+ * A fetch that records the A2A traffic it carries into `options.sink`, and
329
+ * hands the SDK what the network gave.
330
+ */
331
+ export function inspectA2AFetch(base, options) {
332
+ const { sink, asker, peer } = options;
333
+ // What the asker makes of the events (`A2APeerEvent`) is linked to them.
334
+ sink.addLinker(a2aEventLinker);
335
+ return async (input, init) => {
336
+ const rpc = rpcOf(init?.body);
337
+ const method = rpc ? String(rpc.method ?? '') : '';
338
+ const rpcId = rpc ? rpc.id : undefined;
339
+ const url = typeof input === 'string'
340
+ ? input
341
+ : input instanceof URL
342
+ ? input.href
343
+ : input.url;
344
+ if (rpc) {
345
+ sink.push({
346
+ actor: asker,
347
+ source: 'a2a',
348
+ direction: { from: asker, to: peer },
349
+ kind: 'request',
350
+ name: method,
351
+ summary: requestSummary(method, rpc.params),
352
+ a2a: { method, rpcId },
353
+ payload: rpc,
354
+ });
355
+ }
356
+ let response;
357
+ try {
358
+ response = await base(input, init);
359
+ }
360
+ catch (reason) {
361
+ sink.push({
362
+ actor: asker,
363
+ source: 'a2a',
364
+ direction: { from: asker, to: peer },
365
+ kind: 'error',
366
+ name: method || undefined,
367
+ status: 'failed',
368
+ summary: shortLine(`${method || url}: ${reason instanceof Error ? reason.message : String(reason)}`),
369
+ error: reason instanceof Error ? reason.message : String(reason),
370
+ a2a: { method, rpcId },
371
+ });
372
+ throw reason;
373
+ }
374
+ const type = response.headers.get('content-type') ?? '';
375
+ const recorder = new StreamRecorder(options, method, rpcId);
376
+ if (!response.ok) {
377
+ const copy = response.clone();
378
+ void copy.text().then(text => sink.push({
379
+ actor: peer,
380
+ source: 'a2a',
381
+ direction: { from: peer, to: asker },
382
+ kind: 'error',
383
+ name: method || undefined,
384
+ status: 'failed',
385
+ summary: shortLine(`${response.status} ${response.statusText} ${text}`),
386
+ error: `${response.status} ${response.statusText}`,
387
+ a2a: { method, rpcId },
388
+ payload: { status: response.status, body: text },
389
+ }));
390
+ return response;
391
+ }
392
+ if (type.includes('text/event-stream') && response.body) {
393
+ const [mine, theirs] = response.body.tee();
394
+ void readEvents(mine, data => recorder.record(data, true)).catch(() => undefined);
395
+ return new Response(theirs, {
396
+ status: response.status,
397
+ statusText: response.statusText,
398
+ headers: response.headers,
399
+ });
400
+ }
401
+ if (type.includes('json')) {
402
+ const copy = response.clone();
403
+ void copy
404
+ .json()
405
+ .then(data => {
406
+ if (rpc) {
407
+ recorder.record(data, false);
408
+ }
409
+ else if (isObject(data) && Array.isArray(data.skills)) {
410
+ // The agent card.
411
+ sink.push({
412
+ actor: peer,
413
+ source: 'a2a',
414
+ direction: { from: peer, to: asker },
415
+ kind: 'agent-card',
416
+ name: String(data.name ?? peer),
417
+ summary: shortLine(`${String(data.name ?? peer)} · ${data.skills
418
+ .map(skill => isObject(skill) ? String(skill.name ?? skill.id) : '')
419
+ .join(', ')}`),
420
+ payload: data,
421
+ });
422
+ }
423
+ })
424
+ .catch(() => undefined);
425
+ }
426
+ return response;
427
+ };
428
+ }
429
+ /** The entry an event of the asker's came from. */
430
+ function matchOf(event) {
431
+ switch (event.phase) {
432
+ case 'asked':
433
+ return entry => entry.source === 'a2a' &&
434
+ entry.kind === 'request' &&
435
+ /Message/.test(entry.a2a?.method ?? '') &&
436
+ entry.events.length === 0;
437
+ case 'working':
438
+ if (event.tool) {
439
+ const tool = event.tool;
440
+ return entry => entry.kind === 'status-update' &&
441
+ entry.a2a?.taskId === event.taskId &&
442
+ entry.a2a?.step?.name === tool.name &&
443
+ entry.a2a?.step?.ended === tool.ended &&
444
+ (tool.id === undefined || entry.a2a?.step?.toolId === tool.id);
445
+ }
446
+ if (event.note) {
447
+ return entry => entry.kind === 'status-update' &&
448
+ entry.a2a?.taskId === event.taskId &&
449
+ !entry.a2a?.step &&
450
+ entry.events.length === 0;
451
+ }
452
+ return entry => (entry.kind === 'task' || entry.kind === 'status-update') &&
453
+ entry.a2a?.taskId === event.taskId &&
454
+ entry.events.length === 0;
455
+ case 'answered':
456
+ return entry => entry.kind === 'status-update' &&
457
+ entry.a2a?.taskId === event.taskId &&
458
+ entry.a2a?.state === 'TASK_STATE_COMPLETED';
459
+ default:
460
+ return entry => entry.source === 'a2a' && entry.direction?.to !== undefined;
461
+ }
462
+ }
463
+ /** Whether a value is an `A2APeerEvent`. */
464
+ function isPeerEvent(event) {
465
+ return (isObject(event) &&
466
+ ['asked', 'working', 'answered', 'failed'].includes(String(event.phase)));
467
+ }
468
+ /**
469
+ * The linker the A2A capture adds to its sink: an `A2APeerEvent` noted
470
+ * (`sink.note`) is attached to the entry it came from.
471
+ */
472
+ export function a2aEventLinker(event) {
473
+ return isPeerEvent(event) ? matchOf(event) : undefined;
474
+ }
475
+ /** Attach what the asker made of an event to the A2A entry it came from. */
476
+ export function linkA2APeerEvent(sink, event) {
477
+ sink.attach(matchOf(event), event);
478
+ }