@dudousxd/nestjs-agent-react 0.35.1 → 0.35.3

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/dist/index.cjs CHANGED
@@ -1380,6 +1380,39 @@ function useAmbientRenderUi() {
1380
1380
  // src/genui/generative-ui.tsx
1381
1381
  var import_react4 = require("react");
1382
1382
 
1383
+ // src/genui/share-structure.ts
1384
+ function isPlainObject(value) {
1385
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
1386
+ const prototype = Object.getPrototypeOf(value);
1387
+ return prototype === Object.prototype || prototype === null;
1388
+ }
1389
+ function shareStructure(previous, next) {
1390
+ if (Object.is(previous, next)) return next;
1391
+ if (Array.isArray(next)) {
1392
+ if (!Array.isArray(previous)) return next;
1393
+ let same = previous.length === next.length;
1394
+ const shared3 = next.map((value, index) => {
1395
+ const kept = shareStructure(previous[index], value);
1396
+ if (kept !== previous[index]) same = false;
1397
+ return kept;
1398
+ });
1399
+ return same ? previous : shared3;
1400
+ }
1401
+ if (isPlainObject(next)) {
1402
+ if (!isPlainObject(previous)) return next;
1403
+ const keys = Object.keys(next);
1404
+ let same = keys.length === Object.keys(previous).length;
1405
+ const shared3 = {};
1406
+ for (const key of keys) {
1407
+ const kept = shareStructure(previous[key], next[key]);
1408
+ if (kept !== previous[key] || !Object.hasOwn(previous, key)) same = false;
1409
+ shared3[key] = kept;
1410
+ }
1411
+ return same ? previous : shared3;
1412
+ }
1413
+ return next;
1414
+ }
1415
+
1383
1416
  // src/genui/use-generative-ui.ts
1384
1417
  var import_react3 = require("react");
1385
1418
 
@@ -1551,10 +1584,10 @@ var TreeScopeContext = (0, import_react4.createContext)(null);
1551
1584
  var GenuiNodeContext = sharedContext(
1552
1585
  "@dudousxd/nestjs-agent-react:genui-node"
1553
1586
  );
1554
- function renderPlaceholder(scope, node) {
1555
- const { placeholder } = scope;
1587
+ function renderPlaceholder(look, node) {
1588
+ const { placeholder } = look;
1556
1589
  if (typeof placeholder === "function") return placeholder(node);
1557
- return placeholder !== void 0 ? placeholder : scope.loading;
1590
+ return placeholder !== void 0 ? placeholder : look.loading;
1558
1591
  }
1559
1592
  function renderFallback(fallback2, problem) {
1560
1593
  if (typeof fallback2 === "function") return fallback2(problem);
@@ -1588,9 +1621,33 @@ var ItemBoundary = class extends import_react4.Component {
1588
1621
  function nodeItem(node, id) {
1589
1622
  return { id, component: node.type, props: node.props ?? {}, version: null, toolCallId: null };
1590
1623
  }
1591
- function TreeNode({ node, id }) {
1624
+ function failsWhole(scope) {
1625
+ return !scope.hasFallback && scope.fallbackText !== void 0;
1626
+ }
1627
+ var TreeNode = (0, import_react4.memo)(
1628
+ function TreeNode2({ node, id, bounded }) {
1629
+ const scope = (0, import_react4.useContext)(TreeScopeContext);
1630
+ const item = (0, import_react4.useMemo)(() => nodeItem(node, id), [node, id]);
1631
+ if (scope === null) return null;
1632
+ if (!bounded || failsWhole(scope)) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TreeNodeBody, { node, id, item });
1633
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1634
+ ItemBoundary,
1635
+ {
1636
+ item,
1637
+ fallback: scope.look.current.fallback,
1638
+ onError: scope.look.current.onError,
1639
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TreeNodeBody, { node, id, item })
1640
+ }
1641
+ );
1642
+ },
1643
+ (previous, next) => previous.node === next.node && previous.id === next.id && previous.bounded === next.bounded
1644
+ );
1645
+ function TreeNodeBody({
1646
+ node,
1647
+ id,
1648
+ item
1649
+ }) {
1592
1650
  const scope = (0, import_react4.useContext)(TreeScopeContext);
1593
- const item = (0, import_react4.useMemo)(() => nodeItem(node, id), [node, id]);
1594
1651
  const incomplete = node.incomplete === true;
1595
1652
  const held = node.held === true;
1596
1653
  const state = (0, import_react4.useMemo)(
@@ -1612,45 +1669,37 @@ function TreeNode({ node, id }) {
1612
1669
  item.props
1613
1670
  );
1614
1671
  if (scope === null) return null;
1672
+ const look = scope.look.current;
1615
1673
  if (held) {
1616
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GenuiNodeContext.Provider, { value: state, children: renderPlaceholder(scope, state) });
1674
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GenuiNodeContext.Provider, { value: state, children: renderPlaceholder(look, state) });
1617
1675
  }
1618
- if (resolution.status === "loading" || validation.status === "pending") return scope.loading;
1676
+ if (resolution.status === "loading" || validation.status === "pending") return look.loading;
1619
1677
  if (resolution.status === "unknown") {
1620
- if (scope.fallback === void 0 && scope.fallbackText !== void 0)
1621
- throw new Error("Unknown tree component");
1622
- return renderFallback(scope.fallback, { reason: "unknown", item });
1678
+ if (failsWhole(scope)) throw new Error("Unknown tree component");
1679
+ return renderFallback(look.fallback, { reason: "unknown", item });
1623
1680
  }
1624
1681
  if (resolution.status === "error") {
1625
- if (scope.fallback === void 0 && scope.fallbackText !== void 0) throw resolution.error;
1626
- return renderFallback(scope.fallback, { reason: "error", item, error: resolution.error });
1682
+ if (failsWhole(scope)) throw resolution.error;
1683
+ return renderFallback(look.fallback, { reason: "error", item, error: resolution.error });
1627
1684
  }
1628
1685
  if (validation.status === "invalid") {
1629
- if (scope.fallback === void 0 && scope.fallbackText !== void 0)
1630
- throw new Error("Invalid tree component");
1631
- return renderFallback(scope.fallback, { reason: "invalid", item, issues: validation.issues });
1686
+ if (failsWhole(scope)) throw new Error("Invalid tree component");
1687
+ return renderFallback(look.fallback, { reason: "invalid", item, issues: validation.issues });
1632
1688
  }
1633
1689
  const Renderer = resolution.Component;
1634
1690
  const children = Array.isArray(node.children) ? node.children : [];
1635
1691
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GenuiNodeContext.Provider, { value: state, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Renderer, { ...validation.props, children: children.length > 0 ? children.map((child, index) => {
1636
1692
  const childId = `${id}.${index}`;
1637
- if (scope.fallback === void 0 && scope.fallbackText !== void 0)
1638
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TreeNode, { node: child, id: childId }, childId);
1639
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1640
- ItemBoundary,
1641
- {
1642
- item: nodeItem(child, childId),
1643
- fallback: scope.fallback,
1644
- onError: scope.onError,
1645
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TreeNode, { node: child, id: childId })
1646
- },
1647
- childId
1648
- );
1693
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TreeNode, { node: child, id: childId, bounded: true }, childId);
1649
1694
  }) : void 0 }) });
1650
1695
  }
1651
1696
  function GenuiTree({ root }) {
1652
- if (root === void 0 || root === null || typeof root.type !== "string") return null;
1653
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TreeNode, { node: root, id: "root" });
1697
+ const previous = (0, import_react4.useRef)(void 0);
1698
+ const valid = root !== void 0 && root !== null && typeof root.type === "string";
1699
+ const shared3 = valid ? shareStructure(previous.current, root) : void 0;
1700
+ previous.current = shared3;
1701
+ if (shared3 === void 0) return null;
1702
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TreeNode, { node: shared3, id: "root", bounded: false });
1654
1703
  }
1655
1704
  function GenerativeUIScope({
1656
1705
  registry,
@@ -1664,6 +1713,12 @@ function GenerativeUIScope({
1664
1713
  onError,
1665
1714
  children
1666
1715
  }) {
1716
+ const versionsRef = (0, import_react4.useRef)(void 0);
1717
+ const versions = shareStructure(versionsRef.current, componentVersions);
1718
+ versionsRef.current = versions;
1719
+ const look = (0, import_react4.useRef)({ fallback: fallback2, loading, placeholder, onError });
1720
+ look.current = { fallback: fallback2, loading, placeholder, onError };
1721
+ const hasFallback = fallback2 !== void 0;
1667
1722
  const scope = (0, import_react4.useMemo)(
1668
1723
  () => ({
1669
1724
  options: {
@@ -1671,24 +1726,12 @@ function GenerativeUIScope({
1671
1726
  ...catalog !== void 0 ? { catalog } : {},
1672
1727
  ...resolveComponent !== void 0 ? { resolveComponent } : {}
1673
1728
  },
1674
- fallback: fallback2,
1675
- ...componentVersions !== void 0 ? { componentVersions } : {},
1729
+ hasFallback,
1730
+ ...versions !== void 0 ? { componentVersions: versions } : {},
1676
1731
  ...fallbackText !== void 0 ? { fallbackText } : {},
1677
- loading,
1678
- placeholder,
1679
- onError
1732
+ look
1680
1733
  }),
1681
- [
1682
- registry,
1683
- catalog,
1684
- resolveComponent,
1685
- fallback2,
1686
- fallbackText,
1687
- componentVersions,
1688
- loading,
1689
- placeholder,
1690
- onError
1691
- ]
1734
+ [registry, catalog, resolveComponent, hasFallback, fallbackText, versions]
1692
1735
  );
1693
1736
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TreeScopeContext.Provider, { value: scope, children });
1694
1737
  }
@@ -4245,6 +4288,7 @@ function useTranscriptItems({
4245
4288
  );
4246
4289
  const callbackCache = (0, import_react17.useRef)(/* @__PURE__ */ new Map());
4247
4290
  const reasoningToggleCache = (0, import_react17.useRef)(/* @__PURE__ */ new Map());
4291
+ const uiBlocks = (0, import_react17.useRef)(/* @__PURE__ */ new Map());
4248
4292
  const toggleReasoning = (0, import_react17.useCallback)((key, open) => {
4249
4293
  setOpenReasoning((current) => {
4250
4294
  const next = new Map(current);
@@ -4439,6 +4483,14 @@ function useTranscriptItems({
4439
4483
  }, []);
4440
4484
  const lastAssistantId = findLastAssistant(messages)?.id ?? null;
4441
4485
  const items = [];
4486
+ const previousUi = uiBlocks.current;
4487
+ const keptUi = /* @__PURE__ */ new Map();
4488
+ const keepUiIdentity = (blocks) => blocks.map((block) => {
4489
+ if (block.kind !== "ui") return block;
4490
+ const kept = shareStructure(previousUi.get(block.key), block);
4491
+ keptUi.set(block.key, kept);
4492
+ return kept;
4493
+ });
4442
4494
  for (let index = visibleStart; index < messages.length; index++) {
4443
4495
  const message = messages[index];
4444
4496
  if (!message) {
@@ -4461,37 +4513,39 @@ function useTranscriptItems({
4461
4513
  isAssistant,
4462
4514
  isLastAssistant,
4463
4515
  isStreaming: message.id === streamingMessageId,
4464
- blocks: buildTranscriptBlocks(message, {
4465
- isReasoningOpen: (key, isStreamingRun) => openReasoning.get(key) ?? isStreamingRun,
4466
- toggleReasoning: (key, open) => stableToggle(key)(open),
4467
- ...options.sources !== void 0 ? { sources: options.sources } : {},
4468
- ...options.toolCatalog !== void 0 ? { toolCatalog: options.toolCatalog } : {},
4469
- // Read-only still lifts question sets into their block, so the outcome shows.
4470
- ...handlers.onAnswer !== void 0 || options.readOnly === true && options.onAnswer !== null ? {
4471
- elicitation: {
4472
- picked: (toolCallId, questionId) => picks.get(pickKey(toolCallId, questionId)),
4473
- pick,
4474
- canAnswer: handlers.onAnswer !== void 0,
4475
- canSkip: handlers.onSkip !== void 0,
4476
- answer,
4477
- skip,
4478
- submitting: (toolCallId) => settling.get(toolCallId) ?? null,
4479
- errorOf: (toolCallId) => settleErrors.get(toolCallId)?.message ?? null,
4480
- errorCodeOf: (toolCallId) => settleErrors.get(toolCallId)?.code ?? null
4481
- }
4482
- } : {},
4483
- ...handlers.onApprove !== void 0 || handlers.onReject !== void 0 ? {
4484
- approval: {
4485
- canApprove: handlers.onApprove !== void 0,
4486
- canReject: handlers.onReject !== void 0,
4487
- approve,
4488
- reject,
4489
- submitting: (toolCallId) => settling.get(toolCallId) ?? null,
4490
- errorOf: (toolCallId) => settleErrors.get(toolCallId)?.message ?? null,
4491
- errorCodeOf: (toolCallId) => settleErrors.get(toolCallId)?.code ?? null
4492
- }
4493
- } : {}
4494
- }),
4516
+ blocks: keepUiIdentity(
4517
+ buildTranscriptBlocks(message, {
4518
+ isReasoningOpen: (key, isStreamingRun) => openReasoning.get(key) ?? isStreamingRun,
4519
+ toggleReasoning: (key, open) => stableToggle(key)(open),
4520
+ ...options.sources !== void 0 ? { sources: options.sources } : {},
4521
+ ...options.toolCatalog !== void 0 ? { toolCatalog: options.toolCatalog } : {},
4522
+ // Read-only still lifts question sets into their block, so the outcome shows.
4523
+ ...handlers.onAnswer !== void 0 || options.readOnly === true && options.onAnswer !== null ? {
4524
+ elicitation: {
4525
+ picked: (toolCallId, questionId) => picks.get(pickKey(toolCallId, questionId)),
4526
+ pick,
4527
+ canAnswer: handlers.onAnswer !== void 0,
4528
+ canSkip: handlers.onSkip !== void 0,
4529
+ answer,
4530
+ skip,
4531
+ submitting: (toolCallId) => settling.get(toolCallId) ?? null,
4532
+ errorOf: (toolCallId) => settleErrors.get(toolCallId)?.message ?? null,
4533
+ errorCodeOf: (toolCallId) => settleErrors.get(toolCallId)?.code ?? null
4534
+ }
4535
+ } : {},
4536
+ ...handlers.onApprove !== void 0 || handlers.onReject !== void 0 ? {
4537
+ approval: {
4538
+ canApprove: handlers.onApprove !== void 0,
4539
+ canReject: handlers.onReject !== void 0,
4540
+ approve,
4541
+ reject,
4542
+ submitting: (toolCallId) => settling.get(toolCallId) ?? null,
4543
+ errorOf: (toolCallId) => settleErrors.get(toolCallId)?.message ?? null,
4544
+ errorCodeOf: (toolCallId) => settleErrors.get(toolCallId)?.code ?? null
4545
+ }
4546
+ } : {}
4547
+ })
4548
+ ),
4495
4549
  text,
4496
4550
  usage: usage ? describeUsage(usage) : null,
4497
4551
  timestamp: describeTimestamp((options.getCreatedAt ?? createdAtFromMetadata)(message)),
@@ -4526,6 +4580,7 @@ function useTranscriptItems({
4526
4580
  }
4527
4581
  });
4528
4582
  }
4583
+ uiBlocks.current = keptUi;
4529
4584
  return items;
4530
4585
  }
4531
4586
  function settleHandlers(options, backend) {
@@ -5719,9 +5774,24 @@ function outcomeId(value) {
5719
5774
  }
5720
5775
 
5721
5776
  // src/approvals/use-action-proposals.ts
5777
+ var UNSUPPORTED_STATUSES = /* @__PURE__ */ new Set([404, 405, 501]);
5778
+ var UNKNOWN_THREAD = /* @__PURE__ */ new Set(["Thread not found", "Unknown thread"]);
5779
+ var MAX_BACKOFF_MS = 3e4;
5780
+ var unsupportedBackends = /* @__PURE__ */ new WeakSet();
5781
+ function statusOf(cause) {
5782
+ const status = cause?.status;
5783
+ return typeof status === "number" ? status : void 0;
5784
+ }
5785
+ function isProposalsUnsupported(cause) {
5786
+ const status = statusOf(cause);
5787
+ if (status === void 0 || !UNSUPPORTED_STATUSES.has(status)) return false;
5788
+ return !(status === 404 && cause instanceof Error && UNKNOWN_THREAD.has(cause.message));
5789
+ }
5722
5790
  function useActionProposals(backend, threadId, setMessages, refreshKey, pollMs = 1e3) {
5723
5791
  const [items, setItems] = (0, import_react22.useState)([]);
5724
5792
  const [error, setError] = (0, import_react22.useState)(null);
5793
+ const [unsupported, setUnsupported] = (0, import_react22.useState)(() => unsupportedBackends.has(backend));
5794
+ const [failures, setFailures] = (0, import_react22.useState)(0);
5725
5795
  const latest = (0, import_react22.useRef)({ threadId, setMessages });
5726
5796
  latest.current = { threadId, setMessages };
5727
5797
  const mounted = (0, import_react22.useRef)(true);
@@ -5734,6 +5804,7 @@ function useActionProposals(backend, threadId, setMessages, refreshKey, pollMs =
5734
5804
  }, []);
5735
5805
  const refresh = (0, import_react22.useCallback)(async () => {
5736
5806
  if (threadId === void 0 || backend.listActionProposals === void 0) return;
5807
+ if (unsupportedBackends.has(backend)) return;
5737
5808
  const pending = requests.current.get(threadId);
5738
5809
  if (pending) return pending;
5739
5810
  const request = (async () => {
@@ -5750,12 +5821,23 @@ function useActionProposals(backend, threadId, setMessages, refreshKey, pollMs =
5750
5821
  if (!mounted.current || latest.current.threadId !== threadId) return;
5751
5822
  setItems(proposals);
5752
5823
  setError(null);
5824
+ setFailures(0);
5753
5825
  latest.current.setMessages(
5754
5826
  (current) => reconcileProposalMessages(current, proposals, facts)
5755
5827
  );
5756
5828
  } catch (cause) {
5757
- if (mounted.current && latest.current.threadId === threadId)
5829
+ if (isProposalsUnsupported(cause)) {
5830
+ unsupportedBackends.add(backend);
5831
+ if (mounted.current) {
5832
+ setUnsupported(true);
5833
+ setError(null);
5834
+ }
5835
+ return;
5836
+ }
5837
+ if (mounted.current && latest.current.threadId === threadId) {
5758
5838
  setError(cause instanceof Error ? cause : new Error(String(cause)));
5839
+ setFailures((count) => count + 1);
5840
+ }
5759
5841
  }
5760
5842
  })();
5761
5843
  requests.current.set(threadId, request);
@@ -5768,18 +5850,24 @@ function useActionProposals(backend, threadId, setMessages, refreshKey, pollMs =
5768
5850
  (0, import_react22.useEffect)(() => {
5769
5851
  setItems([]);
5770
5852
  setError(null);
5853
+ setFailures(0);
5771
5854
  }, [threadId]);
5855
+ (0, import_react22.useEffect)(() => {
5856
+ setUnsupported(unsupportedBackends.has(backend));
5857
+ }, [backend]);
5772
5858
  (0, import_react22.useEffect)(() => {
5773
5859
  void refresh();
5774
5860
  }, [refresh, refreshKey]);
5775
5861
  (0, import_react22.useEffect)(() => {
5776
- if (!items.some(proposalNeedsPolling) || pollMs <= 0) return;
5777
- const timer = setInterval(() => {
5862
+ if (unsupported || !items.some(proposalNeedsPolling) || pollMs <= 0) return;
5863
+ const wait = failures === 0 ? pollMs : Math.min(pollMs * 2 ** failures, MAX_BACKOFF_MS);
5864
+ const timer = setTimeout(() => {
5778
5865
  void refresh();
5779
- }, pollMs);
5780
- return () => clearInterval(timer);
5781
- }, [items, pollMs, refresh]);
5866
+ }, wait);
5867
+ return () => clearTimeout(timer);
5868
+ }, [items, failures, unsupported, pollMs, refresh]);
5782
5869
  (0, import_react22.useEffect)(() => {
5870
+ if (unsupported) return;
5783
5871
  const refetch = () => {
5784
5872
  void refresh();
5785
5873
  };
@@ -5794,8 +5882,8 @@ function useActionProposals(backend, threadId, setMessages, refreshKey, pollMs =
5794
5882
  window.removeEventListener("online", refetch);
5795
5883
  document.removeEventListener("visibilitychange", visible);
5796
5884
  };
5797
- }, [refresh]);
5798
- return { items, error, refresh };
5885
+ }, [refresh, unsupported]);
5886
+ return { items, error, unsupported, refresh };
5799
5887
  }
5800
5888
 
5801
5889
  // src/use-agent-chat.ts