@aphrody/m3-ai 3.3.2 → 3.3.4

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 (48) hide show
  1. package/README.md +7 -7
  2. package/dist/A2APeerInspector.d.ts +41 -0
  3. package/dist/A2APeerInspector.d.ts.map +1 -0
  4. package/dist/A2ATaskTimeline.d.ts +28 -0
  5. package/dist/A2ATaskTimeline.d.ts.map +1 -0
  6. package/dist/AgUiEventStream.d.ts +87 -0
  7. package/dist/AgUiEventStream.d.ts.map +1 -0
  8. package/dist/GeminiAccountMenu.d.ts +55 -0
  9. package/dist/GeminiAccountMenu.d.ts.map +1 -0
  10. package/dist/GeminiPersonalContextEdit.d.ts +29 -0
  11. package/dist/GeminiPersonalContextEdit.d.ts.map +1 -0
  12. package/dist/GoogleSpotlightBar.d.ts +32 -0
  13. package/dist/GoogleSpotlightBar.d.ts.map +1 -0
  14. package/dist/Markdown.d.ts +2 -1
  15. package/dist/Markdown.d.ts.map +1 -1
  16. package/dist/McpAppCard.d.ts +24 -0
  17. package/dist/McpAppCard.d.ts.map +1 -0
  18. package/dist/ModelSelector.d.ts.map +1 -1
  19. package/dist/TaskRow.d.ts.map +1 -1
  20. package/dist/ToolCall.d.ts.map +1 -1
  21. package/dist/index.d.ts +15 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +3629 -1950
  24. package/dist/index.js.map +32 -1
  25. package/package.json +17 -20
  26. package/src/A2APeerInspector.tsx +378 -0
  27. package/src/A2ATaskTimeline.tsx +218 -0
  28. package/src/AgUiEventStream.ts +488 -0
  29. package/src/ChatComposer.tsx +239 -0
  30. package/src/GeminiAccountMenu.tsx +212 -0
  31. package/src/GeminiPersonalContextEdit.tsx +147 -0
  32. package/src/GoogleSpotlightBar.tsx +165 -0
  33. package/src/InlineBranchCard.tsx +97 -0
  34. package/src/InlineWorkerCard.tsx +182 -0
  35. package/src/Markdown.tsx +701 -0
  36. package/src/McpAppCard.tsx +161 -0
  37. package/src/MessageBubble.tsx +95 -0
  38. package/src/ModelSelector.tsx +300 -0
  39. package/src/TaskCreateForm.tsx +123 -0
  40. package/src/TaskDetail.tsx +224 -0
  41. package/src/TaskList.tsx +136 -0
  42. package/src/TaskPriorityIcon.tsx +39 -0
  43. package/src/TaskRow.tsx +177 -0
  44. package/src/TaskStatusIcon.tsx +39 -0
  45. package/src/ToolCall.tsx +639 -0
  46. package/src/index.ts +107 -0
  47. package/src/internal.tsx +107 -0
  48. package/src/types.ts +212 -0
@@ -0,0 +1,488 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * @aphrody/m3-ai - AgUiEventStream
4
+ *
5
+ * Full-duplex Agent-to-User (A2U) streaming and A2A fleet bridge:
6
+ * - Maps AG-UI v1.0 protocol events (text chunks, tool calls, reasoning, state, subagents)
7
+ * into native Material Design 3 TranscriptSteps and TranscriptItems.
8
+ * - Bridges Aphrody A2A CoordEnvelope fleet messages (ask/fact/ack/ping/error) to and from AG-UI events.
9
+ * - Provides reactive state accumulator and streaming parser.
10
+ */
11
+
12
+ import {
13
+ EventType,
14
+ type BaseEvent,
15
+ type TextMessageContentEvent,
16
+ type ToolCallStartEvent,
17
+ type ToolCallArgsEvent,
18
+ type ToolCallResultEvent,
19
+ type ReasoningMessageContentEvent,
20
+ type SubagentStartedEvent,
21
+ type SubagentFinishedEvent,
22
+ type SubagentErrorEvent,
23
+ type RunErrorEvent,
24
+ } from "@aphrody/ag-ui";
25
+ import {
26
+ type TranscriptStep,
27
+ type TranscriptItem,
28
+ pairTranscriptSteps,
29
+ tryParseJson,
30
+ } from "./types";
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // A2A Coordination Fleet Types
34
+ // ---------------------------------------------------------------------------
35
+
36
+ export type EnvelopeKind = "ask" | "fact" | "ack" | "ping" | "error";
37
+
38
+ export interface CoordEnvelope {
39
+ v?: number;
40
+ ts: string;
41
+ id: string;
42
+ from: string;
43
+ to?: string;
44
+ kind: EnvelopeKind | string;
45
+ topic: string;
46
+ body: string;
47
+ re?: string;
48
+ channel_hint?: string[];
49
+ [key: string]: unknown;
50
+ }
51
+
52
+ export interface SubagentTrace {
53
+ id: string;
54
+ name: string;
55
+ status: "running" | "completed" | "error";
56
+ parentRunId?: string;
57
+ error?: string;
58
+ }
59
+
60
+ export interface AgUiStreamState {
61
+ status: "idle" | "streaming" | "completed" | "error";
62
+ items: TranscriptItem[];
63
+ steps: TranscriptStep[];
64
+ reasoning: string;
65
+ currentText: string;
66
+ error?: string;
67
+ subagents: SubagentTrace[];
68
+ a2uiSurfaces: Map<string, Record<string, unknown>>;
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // AG-UI Events -> Transcript Steps Mapper
73
+ // ---------------------------------------------------------------------------
74
+
75
+ export class AgUiEventAccumulator {
76
+ private steps: TranscriptStep[] = [];
77
+ private activeText: string = "";
78
+ private activeReasoning: string = "";
79
+ private pendingToolCalls = new Map<string, { name: string; args: string }>();
80
+ private toolCallNames = new Map<string, string>();
81
+ private subagents = new Map<string, SubagentTrace>();
82
+ private a2uiSurfaces = new Map<string, Record<string, unknown>>();
83
+ private isFinished = false;
84
+ private hasError = false;
85
+ private errorMessage?: string;
86
+
87
+ /**
88
+ * Process a single AG-UI event and update accumulator state.
89
+ */
90
+ public push(event: BaseEvent): void {
91
+ switch (event.type) {
92
+ case EventType.RUN_STARTED: {
93
+ this.isFinished = false;
94
+ this.hasError = false;
95
+ break;
96
+ }
97
+
98
+ case EventType.TEXT_MESSAGE_CONTENT: {
99
+ const textEvent = event as TextMessageContentEvent;
100
+ if (textEvent.delta) {
101
+ this.activeText += textEvent.delta;
102
+ }
103
+ break;
104
+ }
105
+
106
+ case EventType.TEXT_MESSAGE_END: {
107
+ if (this.activeText.length > 0) {
108
+ this.steps.push({
109
+ type: "action",
110
+ content: [{ type: "text", text: this.activeText }],
111
+ });
112
+ this.activeText = "";
113
+ }
114
+ break;
115
+ }
116
+
117
+ case EventType.REASONING_MESSAGE_CONTENT: {
118
+ const reasoningEvent = event as ReasoningMessageContentEvent;
119
+ if (reasoningEvent.delta) {
120
+ this.activeReasoning += reasoningEvent.delta;
121
+ }
122
+ break;
123
+ }
124
+
125
+ case EventType.TOOL_CALL_START: {
126
+ const tcStart = event as ToolCallStartEvent;
127
+ this.pendingToolCalls.set(tcStart.toolCallId, {
128
+ name: tcStart.toolCallName,
129
+ args: "",
130
+ });
131
+ this.toolCallNames.set(tcStart.toolCallId, tcStart.toolCallName);
132
+ break;
133
+ }
134
+
135
+ case EventType.TOOL_CALL_ARGS: {
136
+ const tcArgs = event as ToolCallArgsEvent;
137
+ const pending = this.pendingToolCalls.get(tcArgs.toolCallId);
138
+ if (pending && tcArgs.delta) {
139
+ pending.args += tcArgs.delta;
140
+ }
141
+ break;
142
+ }
143
+
144
+ case EventType.TOOL_CALL_END: {
145
+ const tcEnd = event as ToolCallArgsEvent;
146
+ const pending = this.pendingToolCalls.get(tcEnd.toolCallId);
147
+ if (pending) {
148
+ this.steps.push({
149
+ type: "action",
150
+ content: [
151
+ {
152
+ type: "tool_call",
153
+ id: tcEnd.toolCallId,
154
+ name: pending.name,
155
+ args: pending.args,
156
+ },
157
+ ],
158
+ });
159
+ this.pendingToolCalls.delete(tcEnd.toolCallId);
160
+ }
161
+ break;
162
+ }
163
+
164
+ case EventType.TOOL_CALL_RESULT: {
165
+ const tcResult = event as ToolCallResultEvent;
166
+ const resultContent = typeof tcResult.content === "string"
167
+ ? tcResult.content
168
+ : JSON.stringify(tcResult.content);
169
+
170
+ // Check if tool result contains A2UI operations
171
+ this.inspectA2uiOperations(tcResult.content);
172
+
173
+ const toolName = this.toolCallNames.get(tcResult.toolCallId) || (tcResult.metadata?.toolName as string) || "tool";
174
+ this.steps.push({
175
+ type: "tool_result",
176
+ call_id: tcResult.toolCallId,
177
+ name: toolName,
178
+ text: resultContent,
179
+ });
180
+ break;
181
+ }
182
+
183
+ case EventType.SUBAGENT_STARTED: {
184
+ const subEvent = event as SubagentStartedEvent;
185
+ this.subagents.set(subEvent.subagentRunId, {
186
+ id: subEvent.subagentRunId,
187
+ name: subEvent.name,
188
+ status: "running",
189
+ parentRunId: subEvent.parentSubagentRunId,
190
+ });
191
+ break;
192
+ }
193
+
194
+ case EventType.SUBAGENT_FINISHED: {
195
+ const subEvent = event as SubagentFinishedEvent;
196
+ const existing = this.subagents.get(subEvent.subagentRunId);
197
+ if (existing) {
198
+ existing.status = "completed";
199
+ }
200
+ break;
201
+ }
202
+
203
+ case EventType.SUBAGENT_ERROR: {
204
+ const subEvent = event as SubagentErrorEvent;
205
+ const existing = this.subagents.get(subEvent.subagentRunId);
206
+ if (existing) {
207
+ existing.status = "error";
208
+ existing.error = subEvent.message;
209
+ }
210
+ break;
211
+ }
212
+
213
+ case EventType.RUN_FINISHED: {
214
+ // Flush any remaining text
215
+ if (this.activeText.length > 0) {
216
+ this.steps.push({
217
+ type: "action",
218
+ content: [{ type: "text", text: this.activeText }],
219
+ });
220
+ this.activeText = "";
221
+ }
222
+ this.isFinished = true;
223
+ break;
224
+ }
225
+
226
+ case EventType.RUN_ERROR: {
227
+ const errEvent = event as RunErrorEvent;
228
+ this.hasError = true;
229
+ this.errorMessage = errEvent.message;
230
+ this.isFinished = true;
231
+ break;
232
+ }
233
+
234
+ default:
235
+ // Other AG-UI events (ACTIVITY, STATE_DELTA, etc.) are kept in state
236
+ break;
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Parse A2UI operations if returned by a tool result
242
+ */
243
+ private inspectA2uiOperations(content: unknown): void {
244
+ if (!content) return;
245
+ const parsed = typeof content === "string" ? tryParseJson(content) : content;
246
+ if (parsed && typeof parsed === "object" && "a2ui_operations" in parsed) {
247
+ const ops = (parsed as { a2ui_operations: Array<Record<string, unknown>> }).a2ui_operations;
248
+ if (Array.isArray(ops)) {
249
+ for (const op of ops) {
250
+ if (op.createSurface && typeof op.createSurface === "object") {
251
+ const surface = op.createSurface as { surfaceId: string; catalogId?: string };
252
+ this.a2uiSurfaces.set(surface.surfaceId, {
253
+ surfaceId: surface.surfaceId,
254
+ catalogId: surface.catalogId,
255
+ components: [],
256
+ dataModel: {},
257
+ });
258
+ } else if (op.updateComponents && typeof op.updateComponents === "object") {
259
+ const update = op.updateComponents as { surfaceId: string; components: unknown[] };
260
+ const existing = this.a2uiSurfaces.get(update.surfaceId);
261
+ if (existing) {
262
+ existing.components = update.components;
263
+ }
264
+ } else if (op.updateDataModel && typeof op.updateDataModel === "object") {
265
+ const update = op.updateDataModel as { surfaceId: string; value: unknown };
266
+ const existing = this.a2uiSurfaces.get(update.surfaceId);
267
+ if (existing) {
268
+ existing.dataModel = update.value;
269
+ }
270
+ }
271
+ }
272
+ }
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Produces the snapshot of current streaming state.
278
+ */
279
+ public snapshot(): AgUiStreamState {
280
+ // If active text is streaming, include a draft action step
281
+ const currentSteps = [...this.steps];
282
+ if (this.activeText.length > 0) {
283
+ currentSteps.push({
284
+ type: "action",
285
+ content: [{ type: "text", text: this.activeText }],
286
+ });
287
+ }
288
+
289
+ const items = pairTranscriptSteps(currentSteps);
290
+
291
+ let status: AgUiStreamState["status"] = "idle";
292
+ if (this.hasError) {
293
+ status = "error";
294
+ } else if (this.isFinished) {
295
+ status = "completed";
296
+ } else if (currentSteps.length > 0 || this.activeReasoning.length > 0) {
297
+ status = "streaming";
298
+ }
299
+
300
+ return {
301
+ status,
302
+ items,
303
+ steps: currentSteps,
304
+ reasoning: this.activeReasoning,
305
+ currentText: this.activeText,
306
+ error: this.errorMessage,
307
+ subagents: Array.from(this.subagents.values()),
308
+ a2uiSurfaces: new Map(this.a2uiSurfaces),
309
+ };
310
+ }
311
+
312
+ /**
313
+ * Alias for snapshot() for ergonomics.
314
+ */
315
+ public getState(): AgUiStreamState {
316
+ return this.snapshot();
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Converts a static list of AG-UI events into TranscriptSteps.
322
+ */
323
+ export function agUiEventsToTranscriptSteps(events: BaseEvent[]): TranscriptStep[] {
324
+ const acc = new AgUiEventAccumulator();
325
+ for (const event of events) {
326
+ acc.push(event);
327
+ }
328
+ return acc.snapshot().steps;
329
+ }
330
+
331
+ /**
332
+ * Converts a static list of AG-UI events into paired TranscriptItems.
333
+ */
334
+ export function agUiEventsToTranscriptItems(events: BaseEvent[]): TranscriptItem[] {
335
+ const acc = new AgUiEventAccumulator();
336
+ for (const event of events) {
337
+ acc.push(event);
338
+ }
339
+ return acc.snapshot().items;
340
+ }
341
+
342
+ // ---------------------------------------------------------------------------
343
+ // A2A CoordEnvelope <-> AG-UI Event Bidirectional Bridge
344
+ // ---------------------------------------------------------------------------
345
+
346
+ /**
347
+ * Translates an Aphrody A2A CoordEnvelope mailbox message into an AG-UI BaseEvent.
348
+ */
349
+ export function coordEnvelopeToAgUiEvent(envelope: CoordEnvelope): BaseEvent {
350
+ switch (envelope.kind) {
351
+ case "ask":
352
+ return {
353
+ type: EventType.CUSTOM,
354
+ name: "a2a_ask",
355
+ value: {
356
+ id: envelope.id,
357
+ from: envelope.from,
358
+ to: envelope.to,
359
+ topic: envelope.topic,
360
+ body: envelope.body,
361
+ channel_hint: envelope.channel_hint,
362
+ re: envelope.re,
363
+ },
364
+ };
365
+
366
+ case "fact":
367
+ return {
368
+ type: EventType.CUSTOM,
369
+ name: "a2a_fact",
370
+ value: {
371
+ id: envelope.id,
372
+ from: envelope.from,
373
+ to: envelope.to,
374
+ topic: envelope.topic,
375
+ body: envelope.body,
376
+ re: envelope.re,
377
+ },
378
+ };
379
+
380
+ case "ack":
381
+ return {
382
+ type: EventType.CUSTOM,
383
+ name: "a2a_ack",
384
+ value: {
385
+ id: envelope.id,
386
+ from: envelope.from,
387
+ to: envelope.to,
388
+ topic: envelope.topic,
389
+ re: envelope.re,
390
+ },
391
+ };
392
+
393
+ case "error":
394
+ return {
395
+ type: EventType.RUN_ERROR,
396
+ message: envelope.body || envelope.topic || "Coordination error",
397
+ code: envelope.id,
398
+ };
399
+
400
+ case "ping":
401
+ return {
402
+ type: EventType.CUSTOM,
403
+ name: "a2a_ping",
404
+ value: {
405
+ id: envelope.id,
406
+ from: envelope.from,
407
+ to: envelope.to,
408
+ },
409
+ };
410
+
411
+ default:
412
+ return {
413
+ type: EventType.CUSTOM,
414
+ name: `a2a_${envelope.kind}`,
415
+ value: envelope,
416
+ };
417
+ }
418
+ }
419
+
420
+ /**
421
+ * Translates an AG-UI BaseEvent into an Aphrody A2A CoordEnvelope mailbox message.
422
+ */
423
+ export function agUiEventToCoordEnvelope(
424
+ event: BaseEvent,
425
+ from: string,
426
+ to: string = "fleet",
427
+ ): CoordEnvelope {
428
+ const ts = new Date().toISOString();
429
+ const id = `env-${Date.now().toString(16)}`;
430
+
431
+ if (event.type === EventType.CUSTOM && (event as any).name?.startsWith("a2a_")) {
432
+ const val = (event as any).value || {};
433
+ const kind = (event as any).name.replace(/^a2a_/, "");
434
+ return {
435
+ v: 1,
436
+ ts: val.ts || ts,
437
+ id: val.id || id,
438
+ from: val.from || from,
439
+ to: val.to || to,
440
+ kind: (kind as EnvelopeKind),
441
+ topic: val.topic || "a2a-coordination",
442
+ body: val.body || "",
443
+ re: val.re,
444
+ channel_hint: val.channel_hint,
445
+ };
446
+ }
447
+
448
+ if (event.type === EventType.RUN_ERROR) {
449
+ const err = event as RunErrorEvent;
450
+ return {
451
+ v: 1,
452
+ ts,
453
+ id,
454
+ from,
455
+ to,
456
+ kind: "error",
457
+ topic: "run-error",
458
+ body: err.message,
459
+ };
460
+ }
461
+
462
+ if (event.type === EventType.TOOL_CALL_RESULT) {
463
+ const res = event as ToolCallResultEvent;
464
+ return {
465
+ v: 1,
466
+ ts,
467
+ id,
468
+ from,
469
+ to,
470
+ kind: "fact",
471
+ topic: (res.metadata?.toolName as string) || `tool:${res.toolCallId}`,
472
+ body: typeof res.content === "string" ? res.content : JSON.stringify(res.content),
473
+ re: res.toolCallId,
474
+ };
475
+ }
476
+
477
+ // Default envelope mapping for arbitrary events
478
+ return {
479
+ v: 1,
480
+ ts,
481
+ id,
482
+ from,
483
+ to,
484
+ kind: "fact",
485
+ topic: `event:${event.type}`,
486
+ body: JSON.stringify(event),
487
+ };
488
+ }