@assistant-ui/react-ag-ui 0.0.70 → 0.1.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 (54) hide show
  1. package/README.md +1 -1
  2. package/dist/runtime/{AgUiThreadRuntimeCore.d.ts → AgUiThreadController.d.ts} +8 -12
  3. package/dist/runtime/AgUiThreadController.d.ts.map +1 -0
  4. package/dist/runtime/{AgUiThreadRuntimeCore.js → AgUiThreadController.js} +302 -162
  5. package/dist/runtime/AgUiThreadController.js.map +1 -0
  6. package/dist/runtime/adapter/conversions.d.ts +4 -4
  7. package/dist/runtime/adapter/conversions.d.ts.map +1 -1
  8. package/dist/runtime/adapter/conversions.js +111 -33
  9. package/dist/runtime/adapter/conversions.js.map +1 -1
  10. package/dist/runtime/adapter/run-aggregator.d.ts +6 -5
  11. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  12. package/dist/runtime/adapter/run-aggregator.js +50 -62
  13. package/dist/runtime/adapter/run-aggregator.js.map +1 -1
  14. package/dist/runtime/adapter/subscriber.d.ts +0 -24
  15. package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
  16. package/dist/runtime/adapter/subscriber.js +1 -22
  17. package/dist/runtime/adapter/subscriber.js.map +1 -1
  18. package/dist/runtime/agUiThreadState.d.ts +60 -0
  19. package/dist/runtime/agUiThreadState.d.ts.map +1 -0
  20. package/dist/runtime/agUiThreadState.js +95 -0
  21. package/dist/runtime/agUiThreadState.js.map +1 -0
  22. package/dist/runtime/event-parser.d.ts.map +1 -1
  23. package/dist/runtime/event-parser.js +45 -48
  24. package/dist/runtime/event-parser.js.map +1 -1
  25. package/dist/runtime/mcp-tool-result.d.ts +3 -1
  26. package/dist/runtime/mcp-tool-result.d.ts.map +1 -1
  27. package/dist/runtime/mcp-tool-result.js +6 -3
  28. package/dist/runtime/mcp-tool-result.js.map +1 -1
  29. package/dist/runtime/types.d.ts +13 -35
  30. package/dist/runtime/types.d.ts.map +1 -1
  31. package/dist/runtime/types.js.map +1 -1
  32. package/dist/useAgUiRuntime.d.ts.map +1 -1
  33. package/dist/useAgUiRuntime.js +4 -2
  34. package/dist/useAgUiRuntime.js.map +1 -1
  35. package/package.json +13 -12
  36. package/src/runtime/{AgUiThreadRuntimeCore.interrupt-cancel.test.ts → AgUiThreadController.interrupt-cancel.test.ts} +4 -4
  37. package/src/runtime/{AgUiThreadRuntimeCore.test.ts → AgUiThreadController.test.ts} +35 -6
  38. package/src/runtime/{AgUiThreadRuntimeCore.ts → AgUiThreadController.ts} +471 -169
  39. package/src/runtime/adapter/conversions.content.test.ts +37 -5
  40. package/src/runtime/adapter/conversions.tools.test.ts +2 -1
  41. package/src/runtime/adapter/conversions.ts +235 -58
  42. package/src/runtime/adapter/run-aggregator.ts +84 -114
  43. package/src/runtime/adapter/subscriber.ts +1 -44
  44. package/src/runtime/agUiThreadState.test.ts +205 -0
  45. package/src/runtime/agUiThreadState.ts +130 -0
  46. package/src/runtime/event-parser.ts +40 -58
  47. package/src/runtime/mcp-tool-result.ts +16 -2
  48. package/src/runtime/types.ts +18 -28
  49. package/src/useAgUiRuntime.external-store.test.tsx +39 -0
  50. package/src/useAgUiRuntime.fast-refresh.test.tsx +3 -3
  51. package/src/useAgUiRuntime.thread-switch.test.tsx +4 -4
  52. package/src/useAgUiRuntime.ts +5 -3
  53. package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +0 -1
  54. package/dist/runtime/AgUiThreadRuntimeCore.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { parseMcpToolCallResult, readMcpAppResourceUri } from "../mcp-tool-result.js";
2
+ import { parseMcpToolCallResult, readMcpAppResourceUri, toolContentText } from "../mcp-tool-result.js";
3
3
  import { AG_UI_METADATA_NAMESPACE } from "../types.js";
4
4
  import { projectAgUiToolApprovals } from "./tool-approval.js";
5
5
  import { generateId } from "@assistant-ui/core";
@@ -21,11 +21,11 @@ function readLegacyAgUiNamespace(metadata) {
21
21
  if (!isObject(namespaced)) return {};
22
22
  return "interrupts" in namespaced ? { interrupts: namespaced.interrupts } : {};
23
23
  }
24
- function withoutPersistedInterrupts(wire) {
24
+ function withoutPersistedStatusMetadata(wire) {
25
25
  if (!isObject(wire.custom)) return wire;
26
26
  const namespaced = wire.custom[AG_UI_METADATA_NAMESPACE];
27
- if (!isObject(namespaced) || !("interrupts" in namespaced)) return wire;
28
- const { interrupts: _interrupts, ...remainingNamespace } = namespaced;
27
+ if (!isObject(namespaced) || !("interrupts" in namespaced) && !("pendingToolCallIds" in namespaced)) return wire;
28
+ const { interrupts: _interrupts, pendingToolCallIds: _pendingToolCallIds, ...remainingNamespace } = namespaced;
29
29
  const { [AG_UI_METADATA_NAMESPACE]: _namespace, ...remainingCustom } = wire.custom;
30
30
  if (Object.keys(remainingNamespace).length > 0) remainingCustom[AG_UI_METADATA_NAMESPACE] = remainingNamespace;
31
31
  const { custom: _custom, ...remainingWire } = wire;
@@ -35,10 +35,11 @@ function withoutPersistedInterrupts(wire) {
35
35
  } : remainingWire;
36
36
  }
37
37
  function toSnapshotCustom(metadata, state = {}) {
38
- const wire = withoutPersistedInterrupts(toAgUiWireMetadata(metadata) ?? {});
38
+ const wire = withoutPersistedStatusMetadata(toAgUiWireMetadata(metadata) ?? {});
39
39
  const needsWireSlot = isObject(metadata) && (Object.keys(metadata).length === 0 || "agui" in wire);
40
40
  const namespaced = {
41
41
  ...readLegacyAgUiNamespace(metadata),
42
+ ...readPersistedPendingToolCallIds(metadata) !== void 0 ? { pendingToolCallIds: readPersistedPendingToolCallIds(metadata) } : {},
42
43
  ...state,
43
44
  ...needsWireSlot ? { messageMetadata: wire } : {}
44
45
  };
@@ -89,14 +90,14 @@ const readAgUiReasoningMeta = (providerMetadata) => {
89
90
  messageMetadata: toAgUiWireMetadata(namespaced.messageMetadata)
90
91
  };
91
92
  };
92
- const buildInputMetadata = (part, filename) => {
93
+ const buildPartMetadata = (part, filename) => {
93
94
  const metadata = {
94
95
  ...readAgUiNamespace(part.providerMetadata),
95
96
  ...filename !== void 0 && { filename }
96
97
  };
97
98
  return Object.keys(metadata).length > 0 ? metadata : void 0;
98
99
  };
99
- const readInputMetadata = (metadata) => {
100
+ const readPartMetadata = (metadata) => {
100
101
  if (!isObject(metadata)) return {};
101
102
  const { filename: _filename, ...rest } = metadata;
102
103
  return {
@@ -139,7 +140,7 @@ function mediaTypeForMime(mimeType) {
139
140
  if (mimeType?.startsWith("video/")) return "video";
140
141
  return "document";
141
142
  }
142
- function buildInputSource(value, declaredMimeType, sourceType) {
143
+ function buildPartSource(value, declaredMimeType, sourceType) {
143
144
  const source = resolveFilePartSource({
144
145
  data: value,
145
146
  mimeType: declaredMimeType ?? "application/octet-stream",
@@ -159,7 +160,7 @@ function buildInputSource(value, declaredMimeType, sourceType) {
159
160
  mimeType: source.mimeType
160
161
  };
161
162
  }
162
- function toInputContent(part, fallbackMimeType) {
163
+ function toContentPart(part, fallbackMimeType) {
163
164
  if (!isObject(part)) return null;
164
165
  const type = getString(part, "type");
165
166
  if (type === "text") {
@@ -173,8 +174,8 @@ function toInputContent(part, fallbackMimeType) {
173
174
  if (type === "image") {
174
175
  const image = getString(part, "image");
175
176
  if (image === void 0) return null;
176
- const metadata = buildInputMetadata(part, getString(part, "filename"));
177
- const source = buildInputSource(image, fallbackMimeType);
177
+ const metadata = buildPartMetadata(part, getString(part, "filename"));
178
+ const source = buildPartSource(image, fallbackMimeType);
178
179
  return {
179
180
  type: "image",
180
181
  source: source.type === "data" ? {
@@ -189,8 +190,8 @@ function toInputContent(part, fallbackMimeType) {
189
190
  if (data === void 0) return null;
190
191
  const declaredMimeType = getString(part, "mimeType") || fallbackMimeType;
191
192
  const filename = getString(part, "filename");
192
- const source = buildInputSource(data, declaredMimeType, getString(part, "sourceType"));
193
- const metadata = buildInputMetadata(part, filename);
193
+ const source = buildPartSource(data, declaredMimeType, getString(part, "sourceType"));
194
+ const metadata = buildPartMetadata(part, filename);
194
195
  switch (mediaTypeForMime(source.mimeType)) {
195
196
  case "image": return {
196
197
  type: "image",
@@ -222,7 +223,7 @@ function toInputContent(part, fallbackMimeType) {
222
223
  if (data === void 0 || format === void 0) return null;
223
224
  return {
224
225
  type: "audio",
225
- source: buildInputSource(parseDataUrl(data)?.data ?? data, `audio/${format}`)
226
+ source: buildPartSource(parseDataUrl(data)?.data ?? data, `audio/${format}`)
226
227
  };
227
228
  }
228
229
  return null;
@@ -233,7 +234,7 @@ const mediaInputTypes = /* @__PURE__ */ new Set([
233
234
  "video",
234
235
  "document"
235
236
  ]);
236
- function inputSourceToString(value) {
237
+ function partSourceToString(value) {
237
238
  if (!isObject(value)) return null;
238
239
  const sourceValue = getString(value, "value");
239
240
  if (sourceValue === void 0) return null;
@@ -275,18 +276,51 @@ function upgradeBinaryInputPart(part) {
275
276
  ...filename && { metadata: { filename } }
276
277
  };
277
278
  }
279
+ function normalizeInputPart(rawPart) {
280
+ return getString(rawPart, "type") === "binary" ? upgradeBinaryInputPart(rawPart) : rawPart;
281
+ }
282
+ function toUserContentLayout(content) {
283
+ const fileSourceParts = [];
284
+ const textParts = [];
285
+ if (!Array.isArray(content)) return {
286
+ fileSourceParts,
287
+ textParts
288
+ };
289
+ let mediaIndex = 0;
290
+ for (const rawPart of content) {
291
+ if (!isObject(rawPart)) continue;
292
+ if (rawPart.type === "text" && typeof rawPart.text === "string") {
293
+ textParts.push({
294
+ index: textParts.length + mediaIndex,
295
+ text: rawPart.text
296
+ });
297
+ continue;
298
+ }
299
+ const part = normalizeInputPart(rawPart);
300
+ if (!part || !mediaInputTypes.has(getString(part, "type") ?? "")) continue;
301
+ if (isObject(part.source) && part.source.type === "file" && typeof part.source.value === "string") fileSourceParts.push({
302
+ index: mediaIndex++,
303
+ part
304
+ });
305
+ else if (partSourceToString(part.source)) mediaIndex++;
306
+ }
307
+ return {
308
+ fileSourceParts,
309
+ textParts: textParts.length === 0 || textParts.length === 1 && textParts[0].index === 0 && textParts[0].text !== "" ? [] : textParts
310
+ };
311
+ }
278
312
  function toSnapshotAttachments(content) {
279
313
  if (!Array.isArray(content)) return [];
280
314
  const attachments = [];
281
315
  for (const rawPart of content) {
282
316
  if (!isObject(rawPart)) continue;
283
- const part = getString(rawPart, "type") === "binary" ? upgradeBinaryInputPart(rawPart) : rawPart;
317
+ const part = normalizeInputPart(rawPart);
284
318
  if (!part) continue;
285
319
  const type = getString(part, "type");
286
320
  if (type === void 0 || !mediaInputTypes.has(type)) continue;
287
- const source = inputSourceToString(part.source);
321
+ const source = partSourceToString(part.source);
288
322
  if (!source) continue;
289
- const { filename, providerMetadata } = readInputMetadata(part.metadata);
323
+ const { filename, providerMetadata } = readPartMetadata(part.metadata);
290
324
  const id = attachments.length.toString();
291
325
  if (type === "image") {
292
326
  attachments.push({
@@ -323,6 +357,13 @@ function toSnapshotAttachments(content) {
323
357
  }
324
358
  return attachments;
325
359
  }
360
+ function readContentSlots(value) {
361
+ if (!Array.isArray(value)) return [];
362
+ return value.flatMap((entry) => isObject(entry) && typeof entry.index === "number" && Number.isInteger(entry.index) && entry.index >= 0 ? [{
363
+ index: entry.index,
364
+ entry
365
+ }] : []);
366
+ }
326
367
  function buildUserContent(message) {
327
368
  const contentParts = Array.isArray(message.content) ? message.content : [];
328
369
  const attachments = message.attachments ?? [];
@@ -332,7 +373,7 @@ function buildUserContent(message) {
332
373
  text: message.content
333
374
  });
334
375
  for (const part of contentParts) {
335
- const input = toInputContent(part, void 0);
376
+ const input = toContentPart(part, void 0);
336
377
  if (input) converted.push(input);
337
378
  }
338
379
  for (const attachment of attachments) {
@@ -341,11 +382,31 @@ function buildUserContent(message) {
341
382
  if (!Array.isArray(attachmentContent)) continue;
342
383
  const fallbackMime = getString(attachment, "contentType");
343
384
  for (const part of attachmentContent) {
344
- const input = toInputContent(part, fallbackMime);
385
+ const input = toContentPart(part, fallbackMime);
345
386
  if (input) converted.push(input);
346
387
  }
347
388
  }
348
- if (converted.some((part) => part.type !== "text")) return converted;
389
+ const namespaced = readNamespacedMetadata(message.metadata);
390
+ const textSlots = readContentSlots(namespaced.textParts).flatMap(({ index, entry }) => typeof entry.text === "string" ? [{
391
+ index,
392
+ part: {
393
+ type: "text",
394
+ text: entry.text
395
+ }
396
+ }] : []);
397
+ const mergedText = converted.flatMap((part) => part.type === "text" ? [part.text] : []).join("\n");
398
+ const restoresText = textSlots.length > 0 && textSlots.map(({ part }) => part.text).join("\n") === mergedText;
399
+ const fileSourceSlots = readContentSlots(namespaced.fileSourceParts).flatMap(({ index, entry: { part } }) => isObject(part) && mediaInputTypes.has(getString(part, "type") ?? "") && isObject(part.source) && part.source.type === "file" && typeof part.source.value === "string" ? [{
400
+ index,
401
+ part
402
+ }] : []);
403
+ const ordered = restoresText ? converted.filter((part) => part.type !== "text") : converted;
404
+ for (const { index, part } of fileSourceSlots.sort((a, b) => a.index - b.index)) {
405
+ const mediaPositions = ordered.flatMap((contentPart, position) => contentPart.type !== "text" ? [position] : []);
406
+ ordered.splice(mediaPositions[index] ?? ordered.length, 0, part);
407
+ }
408
+ if (restoresText) for (const { index, part } of textSlots.sort((a, b) => a.index - b.index)) ordered.splice(Math.min(index, ordered.length), 0, part);
409
+ if (restoresText || ordered.some((part) => part.type !== "text")) return ordered;
349
410
  if (converted.length === 0) return extractText(message.content);
350
411
  return converted.filter((part) => part.type === "text").map((part) => part.text).join("\n");
351
412
  }
@@ -406,6 +467,10 @@ function readPersistedInterrupts(metadata) {
406
467
  if (!isObject(namespaced)) return void 0;
407
468
  return validateInterrupts(namespaced.interrupts);
408
469
  }
470
+ function readPersistedPendingToolCallIds(metadata) {
471
+ const value = readNamespacedMetadata(metadata).pendingToolCallIds;
472
+ return Array.isArray(value) && value.length > 0 && value.every((id) => typeof id === "string") ? value : void 0;
473
+ }
409
474
  function readOpaqueReasoning(metadata) {
410
475
  if (!isObject(metadata)) return [];
411
476
  const custom = metadata.custom;
@@ -480,7 +545,15 @@ function toAssistantSnapshotMessage(rawMessage) {
480
545
  }
481
546
  function toUserOrSystemSnapshotMessage(role, rawMessage) {
482
547
  const messageName = getString(rawMessage, "name");
483
- const custom = toSnapshotCustom(rawMessage.metadata, role === "developer" ? { role: "developer" } : {});
548
+ const { fileSourceParts, textParts } = role === "user" ? toUserContentLayout(rawMessage.content) : {
549
+ fileSourceParts: [],
550
+ textParts: []
551
+ };
552
+ const custom = toSnapshotCustom(rawMessage.metadata, {
553
+ ...role === "developer" ? { role: "developer" } : {},
554
+ ...fileSourceParts.length > 0 ? { fileSourceParts } : {},
555
+ ...textParts.length > 0 ? { textParts } : {}
556
+ });
484
557
  const attachments = role === "user" ? toSnapshotAttachments(rawMessage.content) : [];
485
558
  return {
486
559
  id: getString(rawMessage, "id") ?? generateId(),
@@ -603,13 +676,13 @@ function fromAgUiMessages(messages, options) {
603
676
  flushPendingReasoning();
604
677
  const toolCallId = getToolCallId(rawMessage) ?? `tool-${generateId()}`;
605
678
  const toolMessageId = getString(rawMessage, "id");
606
- const modelContent = extractText(rawMessage.content);
607
- const mcpResult = parseMcpToolCallResult(rawMessage, modelContent);
608
- const mcpModelContent = mcpResult ? [{
679
+ const modelContent = Array.isArray(rawMessage.content) ? toolContentText(rawMessage.content) : extractText(rawMessage.content);
680
+ const mcpResult = parseMcpToolCallResult(rawMessage, Array.isArray(rawMessage.content) ? rawMessage.content : modelContent);
681
+ const toolModelContent = mcpResult ? [{
609
682
  type: "text",
610
683
  text: modelContent
611
684
  }] : void 0;
612
- const result = mcpResult ?? (rawMessage.result !== void 0 ? rawMessage.result : typeof rawMessage.content === "string" ? parseJSONText(rawMessage.content) : rawMessage.content);
685
+ const result = mcpResult ?? (Array.isArray(rawMessage.content) ? rawMessage.content : rawMessage.result !== void 0 ? rawMessage.result : typeof rawMessage.content === "string" ? parseJSONText(rawMessage.content) : rawMessage.content);
613
686
  const isError = typeof rawMessage.error === "string" || rawMessage.isError === true || rawMessage.status === "error" ? true : rawMessage.isError === false ? false : void 0;
614
687
  const mcpAppUri = readMcpAppResourceUri(mcpResult?._meta);
615
688
  const mcpApp = mcpAppUri !== void 0 ? { resourceUri: mcpAppUri } : void 0;
@@ -622,10 +695,11 @@ function fromAgUiMessages(messages, options) {
622
695
  const part = message.content[partIndex];
623
696
  if (!isObject(part) || part.type !== "tool-call") continue;
624
697
  if (getString(part, "toolCallId") !== toolCallId) continue;
698
+ const { modelContent: _modelContent, ...partWithoutModelContent } = part;
625
699
  const updatedPart = {
626
- ...part,
700
+ ...Array.isArray(rawMessage.content) && !mcpResult ? partWithoutModelContent : part,
627
701
  result,
628
- ...mcpModelContent ? { modelContent: mcpModelContent } : {},
702
+ ...toolModelContent ? { modelContent: toolModelContent } : {},
629
703
  ...isError !== void 0 ? { isError } : {},
630
704
  ...toolMessageId !== void 0 ? { unstable_toolMessageId: toolMessageId } : {},
631
705
  ...mcpApp ? { mcp: { app: mcpApp } } : {},
@@ -653,7 +727,7 @@ function fromAgUiMessages(messages, options) {
653
727
  args: {},
654
728
  argsText: "{}",
655
729
  result,
656
- ...mcpModelContent ? { modelContent: mcpModelContent } : {},
730
+ ...toolModelContent ? { modelContent: toolModelContent } : {},
657
731
  ...isError !== void 0 ? { isError } : {},
658
732
  ...toolMessageId !== void 0 ? { unstable_toolMessageId: toolMessageId } : {},
659
733
  ...mcpApp ? { mcp: { app: mcpApp } } : {},
@@ -803,7 +877,8 @@ function fromAgUiMessages(messages, options) {
803
877
  const message = folded[i];
804
878
  if (message.role !== "assistant") continue;
805
879
  const hasInterrupt = readPersistedInterrupts(message.metadata) !== void 0;
806
- const hasPendingToolCall = Array.isArray(message.content) && message.content.some((part) => isObject(part) && part.type === "tool-call" && part.result === void 0);
880
+ const pendingToolCallIds = readPersistedPendingToolCallIds(message.metadata);
881
+ const hasPendingToolCall = Array.isArray(message.content) && message.content.some((part) => isObject(part) && part.type === "tool-call" && part.result === void 0 && (pendingToolCallIds === void 0 || pendingToolCallIds.includes(getString(part, "toolCallId") ?? "")));
807
882
  if (hasInterrupt || hasPendingToolCall) folded[i] = {
808
883
  ...message,
809
884
  status: getAutoStatus(false, false, hasInterrupt, hasPendingToolCall, void 0)
@@ -857,21 +932,24 @@ function convertAssistantMessage(message, converted) {
857
932
  emitToolResult(toolCallId, part, converted);
858
933
  }
859
934
  }
935
+ function isToolContentParts(value) {
936
+ return Array.isArray(value) && value.length > 0 && value.every((part) => isObject(part) && (part.type === "text" ? typeof part.text === "string" : mediaInputTypes.has(getString(part, "type") ?? "") && isObject(part.source) && typeof part.source.value === "string"));
937
+ }
860
938
  function emitToolResult(toolCallId, part, converted) {
861
939
  if (part.result === void 0) return;
862
- const resultContent = part.modelContent !== void 0 ? extractText(part.modelContent) : typeof part.result === "string" ? part.result : JSON.stringify(part.result);
940
+ const resultContent = part.modelContent !== void 0 ? extractText(part.modelContent) : isToolContentParts(part.result) ? part.result : typeof part.result === "string" ? part.result : JSON.stringify(part.result);
863
941
  const messageMetadata = readAgUiPartMessageMetadata(part.providerMetadata);
864
942
  converted.push({
865
943
  id: part.unstable_toolMessageId ?? `${toolCallId}:tool`,
866
944
  role: "tool",
867
945
  content: resultContent,
868
946
  toolCallId,
869
- ...part.isError ? { error: resultContent } : {},
947
+ ...part.isError ? { error: Array.isArray(resultContent) ? toolContentText(resultContent) : resultContent } : {},
870
948
  ...messageMetadata ? { metadata: messageMetadata } : {}
871
949
  });
872
950
  }
873
951
  function convertToolMessage(message, converted) {
874
- const content = extractText(message.content);
952
+ const content = isToolContentParts(message.content) ? message.content : extractText(message.content);
875
953
  const toolCallId = message.toolCallId ?? generateId();
876
954
  converted.push({
877
955
  id: message.id,