@datalayer/agent-runtimes 1.3.113 → 1.4.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 (83) hide show
  1. package/dist-embed/chunks/{ChatView-D41Nx5rC.js → ChatView-BpKZPdcp.js} +4 -4
  2. package/dist-embed/datalayer-app-main.js +150 -150
  3. package/lib/apps/apps/appspec.d.ts +13 -0
  4. package/lib/apps/apps/appspec.js +125 -0
  5. package/lib/apps/apps/checks.d.ts +1 -1
  6. package/lib/apps/apps/checks.js +2 -2
  7. package/lib/apps/apps/customComponents.js +1 -1
  8. package/lib/apps/core/canvasBlocks.d.ts +3 -3
  9. package/lib/apps/core/canvasParts.d.ts +6 -4
  10. package/lib/apps/embed/AppEmbed.js +3 -7
  11. package/lib/apps/embed/embedSession.d.ts +8 -4
  12. package/lib/apps/embed/embedSession.js +16 -9
  13. package/lib/apps/plugins/a2ui-surface/AnswerShows.d.ts +21 -0
  14. package/lib/apps/plugins/a2ui-surface/AnswerShows.js +29 -0
  15. package/lib/apps/plugins/a2ui-surface/SurfaceToolResult.js +4 -7
  16. package/lib/apps/plugins/a2ui-surface/index.d.ts +2 -1
  17. package/lib/apps/plugins/a2ui-surface/index.js +10 -1
  18. package/lib/apps/plugins/a2ui-surface/toolResult.d.ts +20 -11
  19. package/lib/apps/plugins/a2ui-surface/toolResult.js +27 -11
  20. package/lib/apps/plugins/app-components/index.d.ts +1 -1
  21. package/lib/apps/plugins/app-components/index.js +2 -2
  22. package/lib/apps/plugins/app-elements/AppElements.d.ts +3 -3
  23. package/lib/apps/plugins/app-elements/AppElements.js +5 -20
  24. package/lib/apps/plugins/app-elements/QuestionView.d.ts +28 -0
  25. package/lib/apps/plugins/app-elements/QuestionView.js +57 -0
  26. package/lib/apps/plugins/app-elements/index.d.ts +7 -4
  27. package/lib/apps/plugins/app-elements/index.js +14 -5
  28. package/lib/apps/plugins/canvas-blocks/index.d.ts +13 -13
  29. package/lib/apps/plugins/canvas-blocks/index.js +24 -24
  30. package/lib/apps/plugins/canvas-parts/index.d.ts +7 -1
  31. package/lib/apps/plugins/canvas-parts/index.js +36 -0
  32. package/lib/apps/plugins/chat/ChatView.js +7 -0
  33. package/lib/chat/base/ChatBase.js +234 -43
  34. package/lib/chat/base/loopElement.d.ts +16 -17
  35. package/lib/chat/base/loopElement.js +11 -27
  36. package/lib/chat/base/loopStep.d.ts +15 -15
  37. package/lib/chat/base/loopStep.js +10 -13
  38. package/lib/chat/base/loopWindow.d.ts +15 -20
  39. package/lib/chat/base/loopWindow.js +26 -27
  40. package/lib/chat/base/uiOps.d.ts +96 -0
  41. package/lib/chat/base/uiOps.js +251 -0
  42. package/lib/chat/messages/ChatMessageList.d.ts +8 -1
  43. package/lib/chat/messages/ChatMessageList.js +4 -4
  44. package/lib/components/a2ui/datalayer/Chart.d.ts +1 -1
  45. package/lib/components/a2ui/datalayer/schema.js +1 -1
  46. package/lib/components/teams/AnswerSurfaces.d.ts +3 -2
  47. package/lib/components/teams/AnswerSurfaces.js +7 -7
  48. package/lib/components/teams/useA2ATeam.d.ts +1 -1
  49. package/lib/config/AgentConfiguration.d.ts +1 -1
  50. package/lib/protocols/AGUIAdapter.d.ts +7 -0
  51. package/lib/protocols/AGUIAdapter.js +43 -3
  52. package/lib/protocols/agentic-commerce/index.d.ts +5 -9
  53. package/lib/protocols/agentic-commerce/index.js +6 -10
  54. package/lib/protocols/commerce.d.ts +8 -9
  55. package/lib/protocols/commerce.js +29 -0
  56. package/lib/protocols/ucp/index.d.ts +5 -10
  57. package/lib/protocols/ucp/index.js +6 -11
  58. package/lib/specs/actions.d.ts +8 -0
  59. package/lib/specs/actions.js +56 -0
  60. package/lib/specs/agents/agents.js +338 -169
  61. package/lib/specs/apps.d.ts +1 -0
  62. package/lib/specs/apps.js +272 -0
  63. package/lib/specs/appspecSchema.js +125 -0
  64. package/lib/specs/cogs.js +6 -3
  65. package/lib/specs/index.d.ts +2 -1
  66. package/lib/specs/index.js +2 -1
  67. package/lib/specs/protocols.d.ts +24 -0
  68. package/lib/specs/protocols.js +197 -0
  69. package/lib/specs/ui.d.ts +21 -0
  70. package/lib/specs/{uiPlugins.js → ui.js} +14 -14
  71. package/lib/types/agentspecs.d.ts +72 -5
  72. package/lib/types/messages.d.ts +7 -0
  73. package/lib/types/protocol.d.ts +8 -0
  74. package/package.json +2 -2
  75. package/scripts/codegen/generate_agents.py +43 -8
  76. package/scripts/codegen/generate_apps.py +38 -1
  77. package/scripts/codegen/generate_protocols.py +207 -0
  78. package/scripts/codegen/generate_sandbox_agents.py +1 -1
  79. package/scripts/codegen/{generate_ui_plugins.py → generate_ui.py} +40 -40
  80. package/scripts/gen_workers.py +2 -2
  81. package/lib/chat/base/loopMessage.d.ts +0 -37
  82. package/lib/chat/base/loopMessage.js +0 -37
  83. package/lib/specs/uiPlugins.d.ts +0 -21
@@ -19,6 +19,8 @@ import { useContext } from 'react';
19
19
  import React, { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
20
20
  import { Text, Spinner, IconButton } from '@primer/react';
21
21
  import { ReactorLazy } from '@datalayer/reactor/react';
22
+ import { ANSWER_ACTION, emptyUiModel, } from '@datalayer/reactor/ui';
23
+ import { UiSocket } from '@datalayer/reactor/ui/ws';
22
24
  import { SkeletonText } from '@primer/react/experimental';
23
25
  import { SidebarExpandIcon } from '@primer/octicons-react';
24
26
  import { notebookStore, JupyterReactTheme } from '@datalayer/jupyter-react';
@@ -70,10 +72,10 @@ const ephemeralNotebookRefused = lazyViewRefused('notebook');
70
72
  import { useNotebookTools } from '../../tools/adapters/agent-runtimes/notebookHooks';
71
73
  import { useAgentInspectorTracer } from '../../components/inspector/agentSpans';
72
74
  import { chatSpanRecorder, } from '../../components/inspector/chatSpans';
73
- import { loopMessageChange, speakerOf, withLoopMessage } from './loopMessage';
74
- import { loopStepOf, withLoopStep } from './loopStep';
75
- import { applyLoopElement, loopElementChange } from './loopElement';
76
- import { loopWindowMessage, tellLoopWindow } from './loopWindow';
75
+ import { isStepDisplay, withLoopStep } from './loopStep';
76
+ import { applyLoopElement } from './loopElement';
77
+ import { tellLoopWindow } from './loopWindow';
78
+ import { displayPressOf, placeable, routeUiOp, withPress, withUiOp, } from './uiOps';
77
79
  import { useChatWords } from '../ChatLanguage';
78
80
  // Tracks pending prompts already auto-sent for a given conversation scope.
79
81
  // This prevents layout-driven unmount/remount cycles from re-sending prompts.
@@ -1196,6 +1198,10 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
1196
1198
  const [historyLoaded, setHistoryLoaded] = useState(!historyScopeId);
1197
1199
  const [historyRefreshTick, setHistoryRefreshTick] = useState(0);
1198
1200
  const historyRetryAttemptsRef = useRef(new Map());
1201
+ // The steps of an application's code shown so far, as displays (patched
1202
+ // as they stream), and the elements it showed (REACTOR_UI RU-3).
1203
+ const uiStepsRef = useRef(emptyUiModel());
1204
+ const uiElementsRef = useRef(new Set());
1199
1205
  // Adapter-ready flag — flipped to true once the protocol adapter is initialised
1200
1206
  const [adapterReady, setAdapterReady] = useState(false);
1201
1207
  // Guard so each distinct pending prompt is sent at most once. Stores the
@@ -1578,6 +1584,171 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
1578
1584
  threadGivenRef.current?.onStarted?.(id);
1579
1585
  }
1580
1586
  }, []);
1587
+ /*
1588
+ * The conversation's ui channel (REACTOR_UI RU-W): one WebSocket per
1589
+ * conversation, its thread the scope, opened once its session is there
1590
+ * (the first event of its first run, or a thread reopened). What its
1591
+ * messages show, its steps, its elements, its authors and its window
1592
+ * messages arrive here as `reactor.ui` ops; the presses of what they
1593
+ * offered go up it. An op about a message the run stream has not brought
1594
+ * yet — they are two connections — waits for it (`placeable`).
1595
+ */
1596
+ const uiSocketRef = useRef(null);
1597
+ const parkedUiOpsRef = useRef([]);
1598
+ const historyStepsRef = useRef(new Set());
1599
+ const displayItemsRef = useRef([]);
1600
+ const uiSocketUrl = protocol?.uiSocket;
1601
+ const uiSocketTokenRef = useRef(protocol?.authToken);
1602
+ uiSocketTokenRef.current = protocol?.authToken;
1603
+ const applyToMessages = useCallback((op) => {
1604
+ const apply = (items) => {
1605
+ try {
1606
+ return withUiOp(items, op);
1607
+ }
1608
+ catch (error) {
1609
+ console.error('[ChatBase] reactor.ui op not applied:', error);
1610
+ return items;
1611
+ }
1612
+ };
1613
+ setDisplayItems(prev => apply(prev));
1614
+ if (useStoreMode) {
1615
+ const store = useChatStore.getState();
1616
+ store.setMessages(apply(store.messages));
1617
+ }
1618
+ }, [useStoreMode]);
1619
+ const applySocketOp = useCallback((op, history) => {
1620
+ // The past is drawn again, never acted on: a window message is not
1621
+ // raised again, and the steps of turns long over are not drawn again.
1622
+ if (history) {
1623
+ if (op.op === 'window')
1624
+ return;
1625
+ if ((op.op === 'show' || op.op === 'ask') && isStepDisplay(op.body)) {
1626
+ historyStepsRef.current.add(op.body.id);
1627
+ return;
1628
+ }
1629
+ if ((op.op === 'patch' || op.op === 'append') &&
1630
+ historyStepsRef.current.has(op.id)) {
1631
+ return;
1632
+ }
1633
+ }
1634
+ let next;
1635
+ try {
1636
+ next = routeUiOp(op, uiStepsRef.current, uiElementsRef.current);
1637
+ }
1638
+ catch (error) {
1639
+ console.error('[ChatBase] reactor.ui op not applied:', error);
1640
+ return;
1641
+ }
1642
+ uiStepsRef.current = next.steps;
1643
+ const routed = next.routed;
1644
+ if (routed.to === 'window') {
1645
+ // For the page it sits in, outside the conversation (LOOP P-25).
1646
+ tellLoopWindow(routed.data);
1647
+ }
1648
+ else if (routed.to === 'step') {
1649
+ // A step of its code, drawn as it runs (LOOP P-16).
1650
+ const step = routed.step;
1651
+ setDisplayItems(prev => withLoopStep(prev, step));
1652
+ }
1653
+ else if (routed.to === 'element') {
1654
+ // In a side panel or on a page of its own (LOOP P-18).
1655
+ uiElementsRef.current.add(routed.change.id);
1656
+ applyLoopElement(routed.change);
1657
+ }
1658
+ else if (!placeable(displayItemsRef.current, routed.op)) {
1659
+ const under = routed.op.op === 'show' || routed.op.op === 'ask'
1660
+ ? routed.op.body.data?.message_id
1661
+ : undefined;
1662
+ if (history && typeof under === 'string') {
1663
+ // Shown in the past under a message that said nothing: the
1664
+ // thread did not draw it, the chat draws it for what it showed.
1665
+ const placeholder = {
1666
+ id: under,
1667
+ role: 'assistant',
1668
+ content: '',
1669
+ createdAt: new Date(),
1670
+ };
1671
+ displayItemsRef.current = [...displayItemsRef.current, placeholder];
1672
+ setDisplayItems(prev => prev.some(item => item.id === under)
1673
+ ? prev
1674
+ : [...prev, placeholder]);
1675
+ if (useStoreMode)
1676
+ useChatStore.getState().addMessage(placeholder);
1677
+ applyToMessages(routed.op);
1678
+ }
1679
+ else {
1680
+ parkedUiOpsRef.current.push(routed.op);
1681
+ }
1682
+ }
1683
+ else {
1684
+ applyToMessages(routed.op);
1685
+ }
1686
+ }, [applyToMessages, useStoreMode]);
1687
+ const applySocketOpRef = useRef(applySocketOp);
1688
+ applySocketOpRef.current = applySocketOp;
1689
+ // What waited for its message, applied once the message is here.
1690
+ useEffect(() => {
1691
+ displayItemsRef.current = displayItems;
1692
+ if (parkedUiOpsRef.current.length === 0)
1693
+ return;
1694
+ const parked = parkedUiOpsRef.current.splice(0);
1695
+ let items = displayItems;
1696
+ for (const op of parked) {
1697
+ if (!placeable(items, op)) {
1698
+ parkedUiOpsRef.current.push(op);
1699
+ continue;
1700
+ }
1701
+ try {
1702
+ items = withUiOp(items, op);
1703
+ }
1704
+ catch {
1705
+ // Said by `applyToMessages`, which refuses it the same way.
1706
+ }
1707
+ applyToMessages(op);
1708
+ }
1709
+ }, [displayItems, applyToMessages]);
1710
+ /** The conversation's ui socket, opened for its thread when it is not yet. */
1711
+ const ensureUiSocket = useCallback(() => {
1712
+ if (!uiSocketUrl)
1713
+ return null;
1714
+ const scope = threadIdRef.current;
1715
+ const open = uiSocketRef.current;
1716
+ if (open && open.scope === scope)
1717
+ return open.socket;
1718
+ // Another conversation: what the last one showed goes with it.
1719
+ open?.socket.close();
1720
+ uiStepsRef.current = emptyUiModel();
1721
+ uiElementsRef.current = new Set();
1722
+ parkedUiOpsRef.current = [];
1723
+ historyStepsRef.current = new Set();
1724
+ const socket = new UiSocket({
1725
+ url: uiSocketUrl,
1726
+ scope,
1727
+ token: () => uiSocketTokenRef.current ?? '',
1728
+ onOp: (op, _seq, history) => applySocketOpRef.current(op, history),
1729
+ onStatus: (status, reason) => {
1730
+ if (status === 'refused' || status === 'gone') {
1731
+ console.error(`[ChatBase] ui socket ${status}: ${reason ?? ''}`);
1732
+ }
1733
+ },
1734
+ });
1735
+ uiSocketRef.current = { socket, scope };
1736
+ socket.connect();
1737
+ return socket;
1738
+ }, [uiSocketUrl]);
1739
+ const ensureUiSocketRef = useRef(ensureUiSocket);
1740
+ ensureUiSocketRef.current = ensureUiSocket;
1741
+ // A thread reopened has its session already: its socket opens now.
1742
+ useEffect(() => {
1743
+ if (thread?.messages.length)
1744
+ ensureUiSocketRef.current();
1745
+ return () => {
1746
+ uiSocketRef.current?.socket.close();
1747
+ uiSocketRef.current = null;
1748
+ };
1749
+ // At mount: the thread it was given.
1750
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1751
+ }, []);
1581
1752
  const messagesContainerRef = useRef(null);
1582
1753
  const messagesEndRef = useRef(null);
1583
1754
  const inputRef = useRef(null);
@@ -2378,6 +2549,8 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2378
2549
  adapterRef.current = adapter;
2379
2550
  setAdapterReady(true);
2380
2551
  unsubscribeRef.current = adapter.subscribe((event) => {
2552
+ // The run's session is there once it answers: its ui socket opens.
2553
+ ensureUiSocketRef.current();
2381
2554
  /*
2382
2555
  * Nothing more from a turn the reader has stopped.
2383
2556
  *
@@ -2707,45 +2880,11 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2707
2880
  }
2708
2881
  }
2709
2882
  break;
2710
- case 'activity': {
2711
- // A step of an application's code, drawn as it runs (LOOP P-16).
2712
- const step = loopStepOf(event.activity);
2713
- if (step) {
2714
- setDisplayItems(prev => withLoopStep(prev, step));
2715
- break;
2716
- }
2717
- // What its code tells the page it sits in, outside the
2718
- // conversation (LOOP P-25): handed to whoever listens.
2719
- const windowMessage = loopWindowMessage(event.activity);
2720
- if (windowMessage) {
2721
- tellLoopWindow(windowMessage.data);
2722
- break;
2723
- }
2724
- // An element its code opened in a side panel or on a page of its
2725
- // own, changed or closed (LOOP P-18): kept for whoever draws them.
2726
- const element = loopElementChange(event.activity);
2727
- if (element) {
2728
- applyLoopElement(element);
2729
- break;
2730
- }
2731
- // An application's code changed a message it sent: its author
2732
- // said, or the message taken away (LOOP P-15).
2733
- const change = loopMessageChange(event.activity);
2734
- if (!change)
2735
- break;
2736
- setDisplayItems(prev => withLoopMessage(prev, change));
2737
- if (useStoreMode) {
2738
- if ('removed' in change) {
2739
- useChatStore.getState().deleteMessage(change.id);
2740
- }
2741
- else {
2742
- useChatStore.getState().updateMessage(change.id, {
2743
- speaker: speakerOf(change.author),
2744
- });
2745
- }
2746
- }
2883
+ case 'activity':
2884
+ // What the server says besides the conversation's text comes on
2885
+ // the ui socket (REACTOR_UI RU-W); the run's other activities —
2886
+ // `loop.session`, `loop.page` — are their readers'.
2747
2887
  break;
2748
- }
2749
2888
  case 'done':
2750
2889
  /*
2751
2890
  * What the turn cost, when nobody else is counting.
@@ -2949,6 +3088,28 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2949
3088
  return;
2950
3089
  if (!adapterRef.current && !onSendMessage)
2951
3090
  return;
3091
+ // A press of what a display offered goes up the ui socket, checked
3092
+ // there; the run that answers it names it only (REACTOR_UI RU-W).
3093
+ const pressed = displayPressOf(forwardedPropsGiven);
3094
+ if (pressed && forwardedPropsGiven) {
3095
+ const socket = uiSocketRef.current?.socket;
3096
+ if (!socket) {
3097
+ setError(new Error('This conversation’s ui channel is not open.'));
3098
+ return;
3099
+ }
3100
+ try {
3101
+ const result = await socket.act(pressed);
3102
+ if (!result.ok) {
3103
+ setError(new Error(result.detail || `The press was refused (${result.status}).`));
3104
+ return;
3105
+ }
3106
+ forwardedPropsGiven = withPress(forwardedPropsGiven, result.value);
3107
+ }
3108
+ catch (failure) {
3109
+ setError(failure instanceof Error ? failure : new Error(String(failure)));
3110
+ return;
3111
+ }
3112
+ }
2952
3113
  stoppedRef.current = false;
2953
3114
  inspectAnswerRef.current = '';
2954
3115
  recorderRef.current?.recorder.turnStarted(messageContent, selectedModel || undefined);
@@ -3221,6 +3382,36 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3221
3382
  protocol?.authToken,
3222
3383
  protocol?.agentId,
3223
3384
  ]);
3385
+ /*
3386
+ * How a message's display presses what it offered (REACTOR_UI RU-4,
3387
+ * RU-W): with the token of the offer, as the run's press — which
3388
+ * `handleSend` sends up the ui socket before the run, the run naming it
3389
+ * only. An answer is the person's words in the conversation; any other
3390
+ * press, the action's name.
3391
+ */
3392
+ const actOnDisplay = useCallback((display) => async (action) => {
3393
+ const offer = display.actions?.find(one => one.name === action.action);
3394
+ if (!offer) {
3395
+ throw new Error(`“${action.action}” was not offered here.`);
3396
+ }
3397
+ const value = action.payload
3398
+ ?.value;
3399
+ const words = action.action === ANSWER_ACTION
3400
+ ? typeof value === 'string'
3401
+ ? value
3402
+ : 'Answered.'
3403
+ : action.action;
3404
+ await handleSend(words, {
3405
+ loop: {
3406
+ action: {
3407
+ name: action.action,
3408
+ display_id: display.id,
3409
+ token: offer.token,
3410
+ payload: action.payload ?? {},
3411
+ },
3412
+ },
3413
+ });
3414
+ }, [handleSend]);
3224
3415
  // ---- handleNewChat ----
3225
3416
  const handleNewChat = useCallback(() => {
3226
3417
  setDisplayItems([]);
@@ -3618,7 +3809,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3618
3809
  rows inside carry their own horizontal padding, which becomes the
3619
3810
  margin once the cap does bind.
3620
3811
  */
3621
- width: "100%", maxWidth: 920, mx: "auto", bg: "canvas.default", children: [_jsx(ChatMessageList, { displayItems: welcomedItems, isLoading: isLoading, isStreaming: isStreaming, showLoadingIndicator: showLoadingIndicator, hideMessagesAfterToolUI: hideMessagesAfterToolUI, avatarConfig: defaultAvatarConfig, padding: padding,
3812
+ width: "100%", maxWidth: 920, mx: "auto", bg: "canvas.default", children: [_jsx(ChatMessageList, { displayItems: welcomedItems, actOn: protocol?.uiSocket ? actOnDisplay : undefined, isLoading: isLoading, isStreaming: isStreaming, showLoadingIndicator: showLoadingIndicator, hideMessagesAfterToolUI: hideMessagesAfterToolUI, avatarConfig: defaultAvatarConfig, padding: padding,
3622
3813
  // Tool calls are drawn by the page's Reactor plugins; a named
3623
3814
  // notebook's cell surfaces stand in for the default row of its tools,
3624
3815
  // so every chat with a headless notebook gets the same transcript.
@@ -3,16 +3,14 @@
3
3
  * panel beside the conversation, or on a page of its own over it. Inline,
4
4
  * an element is a message, and the chat draws it as one (P-04).
5
5
  *
6
- * The session API says it with a `CUSTOM` event named `loop.element` —
7
- * `{id, where, title, shows}`, `shows` the A2UI surface an answer's
8
- * components are drawn from, or `{id, closed: true}` — which the AG-UI
9
- * adapter hands on as an `activity`. The chat does not draw it: it keeps
10
- * what is open here, and whoever draws panels and pages (the `app-elements`
11
- * plugin) listens. One store per page, as there is one chat per
12
- * application on it.
6
+ * The session API shows it as a `reactor.ui` display that says where it
7
+ * shows in its data (`where: "panel" | "page"`, with its `title`) — an
8
+ * answer's display (REACTOR_UI RU-2) — and closes it with a `remove` of its
9
+ * id (RU-3). The chat does not draw it: it keeps what is open here, and
10
+ * whoever draws panels and pages (the `app-elements` plugin) listens. One
11
+ * store per page, as there is one chat per application on it.
13
12
  */
14
- /** The `CUSTOM` event's name. */
15
- export declare const LOOP_ELEMENT = "loop.element";
13
+ import type { Display } from '@datalayer/reactor/ui';
16
14
  /** Where an element opens, besides inline. */
17
15
  export type LoopElementWhere = 'panel' | 'page';
18
16
  /** An element open in a side panel or on a page. */
@@ -20,19 +18,20 @@ export type LoopElement = {
20
18
  id: string;
21
19
  where: LoopElementWhere;
22
20
  title: string;
23
- /** The surface it draws: what `render_a2ui_surface` returns. */
24
- shows: unknown;
21
+ /** The display it draws: what an answer shows (`AnswerShows`). */
22
+ shows: Display;
25
23
  };
26
- /** What an event says: an element opened or changed, or one closed. */
24
+ /** What changed: an element opened or changed, or one closed. */
27
25
  export type LoopElementChange = LoopElement | {
28
26
  id: string;
29
27
  closed: true;
30
28
  };
31
- /** The change an activity says, or `null` when it says none. */
32
- export declare function loopElementChange(activity: {
33
- type: string;
34
- data: unknown;
35
- } | undefined): LoopElementChange | null;
29
+ /**
30
+ * The element a display is, or `null` for one shown inline, under its
31
+ * message: a display is an element when it says it shows in a panel or on
32
+ * a page.
33
+ */
34
+ export declare function elementOfDisplay(display: Display): LoopElement | null;
36
35
  /**
37
36
  * The elements open with a change applied: one opened is added last, one
38
37
  * changed is replaced where it is, one closed is taken away.
@@ -3,36 +3,20 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
  /**
6
- * Where an element of an application's code shows (LOOP P-18): in a side
7
- * panel beside the conversation, or on a page of its own over it. Inline,
8
- * an element is a message, and the chat draws it as one (P-04).
9
- *
10
- * The session API says it with a `CUSTOM` event named `loop.element` —
11
- * `{id, where, title, shows}`, `shows` the A2UI surface an answer's
12
- * components are drawn from, or `{id, closed: true}` — which the AG-UI
13
- * adapter hands on as an `activity`. The chat does not draw it: it keeps
14
- * what is open here, and whoever draws panels and pages (the `app-elements`
15
- * plugin) listens. One store per page, as there is one chat per
16
- * application on it.
6
+ * The element a display is, or `null` for one shown inline, under its
7
+ * message: a display is an element when it says it shows in a panel or on
8
+ * a page.
17
9
  */
18
- /** The `CUSTOM` event's name. */
19
- export const LOOP_ELEMENT = 'loop.element';
20
- /** The change an activity says, or `null` when it says none. */
21
- export function loopElementChange(activity) {
22
- if (!activity || activity.type !== LOOP_ELEMENT)
23
- return null;
24
- const data = activity.data;
25
- if (!data || typeof data.id !== 'string' || !data.id)
26
- return null;
27
- if (data.closed === true)
28
- return { id: data.id, closed: true };
29
- if (data.where !== 'panel' && data.where !== 'page')
10
+ export function elementOfDisplay(display) {
11
+ const where = display.data?.where;
12
+ if (where !== 'panel' && where !== 'page')
30
13
  return null;
14
+ const title = display.data?.title;
31
15
  return {
32
- id: data.id,
33
- where: data.where,
34
- title: typeof data.title === 'string' ? data.title : '',
35
- shows: data.shows ?? null,
16
+ id: display.id,
17
+ where,
18
+ title: typeof title === 'string' ? title : '',
19
+ shows: display,
36
20
  };
37
21
  }
38
22
  /**
@@ -3,18 +3,19 @@
3
3
  * (LOOP P-16): the chain of thought.
4
4
  *
5
5
  * AG-UI's `STEP_STARTED` and `STEP_FINISHED` carry a step's name only; the
6
- * session API says the step whole beside them, with a `CUSTOM` event named
7
- * `loop.step` — `{id, name, kind, parent_id, input, output, error,
8
- * started_at, ended_at}` — which the AG-UI adapter hands on as an
9
- * `activity`. A step is drawn as the chat draws a tool call: one row,
10
- * collapsed, its input and output inside, running until it ends; a nested
11
- * step says the step it is in. A step that streams its output (LOOP P-31)
12
- * is said again as it writes, its output so far, and drawn in place.
6
+ * session API shows the step whole beside them as a `reactor.ui` display of
7
+ * type `application/vnd.datalayer.loop.step+json` (REACTOR_UI RU-3) — its
8
+ * data `{name, kind, parent_id, input, output, error, started_at,
9
+ * ended_at}`, its id the step's — shown as it starts and again as it ends,
10
+ * its output patched in as it streams (LOOP P-31). A step is drawn as the
11
+ * chat draws a tool call: one row, collapsed, its input and output inside,
12
+ * running until it ends; a nested step says the step it is in.
13
13
  */
14
+ import type { Display } from '@datalayer/reactor/ui';
14
15
  import type { DisplayItem, ToolCallMessage } from '../../types/chat';
15
- /** The `CUSTOM` event's name. */
16
- export declare const LOOP_STEP = "loop.step";
17
- /** A step, as the event says it. */
16
+ /** The display type of a step. */
17
+ export declare const STEP_DISPLAY_TYPE = "application/vnd.datalayer.loop.step+json";
18
+ /** A step, as its display says it. */
18
19
  export type LoopStep = {
19
20
  id: string;
20
21
  name: string;
@@ -25,11 +26,10 @@ export type LoopStep = {
25
26
  error: string;
26
27
  ended: boolean;
27
28
  };
28
- /** The step an activity says, or `null` when it says none. */
29
- export declare function loopStepOf(activity: {
30
- type: string;
31
- data: unknown;
32
- } | undefined): LoopStep | null;
29
+ /** Whether a display is a step's. */
30
+ export declare const isStepDisplay: (display: Display) => boolean;
31
+ /** The step a step's display says. Throws for one that is not a step's. */
32
+ export declare function stepOfDisplay(display: Display): LoopStep;
33
33
  /** The id of the row a step is drawn as. */
34
34
  export declare const stepCallId: (id: string) => string;
35
35
  /** A step as the row the chat draws a tool call as. */
@@ -2,21 +2,18 @@
2
2
  * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
- /** The `CUSTOM` event's name. */
6
- export const LOOP_STEP = 'loop.step';
7
- /** The step an activity says, or `null` when it says none. */
8
- export function loopStepOf(activity) {
9
- if (!activity || activity.type !== LOOP_STEP)
10
- return null;
11
- const data = activity.data;
12
- if (!data ||
13
- typeof data.id !== 'string' ||
14
- !data.id ||
15
- typeof data.name !== 'string') {
16
- return null;
5
+ /** The display type of a step. */
6
+ export const STEP_DISPLAY_TYPE = 'application/vnd.datalayer.loop.step+json';
7
+ /** Whether a display is a step's. */
8
+ export const isStepDisplay = (display) => display.type === STEP_DISPLAY_TYPE;
9
+ /** The step a step's display says. Throws for one that is not a step's. */
10
+ export function stepOfDisplay(display) {
11
+ const data = display.data ?? {};
12
+ if (!isStepDisplay(display) || typeof data.name !== 'string') {
13
+ throw new Error(`Display ${display.id} is not a step`);
17
14
  }
18
15
  return {
19
- id: data.id,
16
+ id: display.id,
20
17
  name: data.name,
21
18
  kind: typeof data.kind === 'string' ? data.kind : 'run',
22
19
  parentId: typeof data.parent_id === 'string' && data.parent_id
@@ -2,30 +2,25 @@
2
2
  * Window messages (LOOP P-25): what an application's code tells the page it
3
3
  * sits in, outside the conversation — ``session.send_window_message(data)``.
4
4
  *
5
- * The session API says it with a `CUSTOM` event named `loop.window` —
6
- * `{data}` — which the AG-UI adapter hands on as an `activity`. The chat
7
- * does not draw it: it hands it to whoever listens here — the embed's host
8
- * bridge, which raises it on the element as its `window-message` event.
5
+ * The session says it with a `reactor.ui` `window` op named `message`, its
6
+ * `args` `{data}` (REACTOR_UI RU-3), on the conversation's ui socket (RU-W). The chat does not draw it: it hands
7
+ * it to whoever listens here — the embed's host bridge, which raises it on
8
+ * the element as its `window-message` event. A window op of another name is
9
+ * not one this page acts on: refused, it throws.
9
10
  */
10
- /** The `CUSTOM` event's name. */
11
- export declare const LOOP_WINDOW = "loop.window";
12
- /** What an activity says to the page, or `null` when it is not a window message. */
13
- export declare function loopWindowMessage(activity: {
14
- type: string;
15
- data: unknown;
16
- } | undefined): {
17
- data: unknown;
18
- } | null;
11
+ import { type WindowOp } from '@datalayer/reactor/ui';
12
+ /** The name of the window op that hands the page a message. */
13
+ export declare const WINDOW_MESSAGE = "message";
14
+ /** What a window op hands the page. Throws for a window op of another name. */
15
+ export declare function windowMessageOf(op: WindowOp): unknown;
19
16
  /** Hand a window message to whoever listens. */
20
17
  export declare function tellLoopWindow(data: unknown): void;
21
18
  /** Listen for window messages. Returns the unsubscribe. */
22
19
  export declare function onLoopWindowMessage(listener: (data: unknown) => void): () => void;
23
20
  /**
24
- * What a turn's stream says to the page — the `loop.window` events of the
25
- * turn a window message posted by the page ran — and the sentence its
26
- * `RUN_ERROR` said, if it failed.
21
+ * Why the turn a window message posted by the page ran failed — its
22
+ * `RUN_ERROR`'s sentence — or `null`. What its code tells the page back comes
23
+ * on the conversation's ui socket (REACTOR_UI RU-W), never on this stream: a
24
+ * `reactor.ui` event here is refused.
27
25
  */
28
- export declare function windowTurnOf(stream: string): {
29
- said: unknown[];
30
- error: string | null;
31
- };
26
+ export declare function windowTurnErrorOf(stream: string): string | null;
@@ -6,21 +6,25 @@
6
6
  * Window messages (LOOP P-25): what an application's code tells the page it
7
7
  * sits in, outside the conversation — ``session.send_window_message(data)``.
8
8
  *
9
- * The session API says it with a `CUSTOM` event named `loop.window` —
10
- * `{data}` — which the AG-UI adapter hands on as an `activity`. The chat
11
- * does not draw it: it hands it to whoever listens here — the embed's host
12
- * bridge, which raises it on the element as its `window-message` event.
9
+ * The session says it with a `reactor.ui` `window` op named `message`, its
10
+ * `args` `{data}` (REACTOR_UI RU-3), on the conversation's ui socket (RU-W). The chat does not draw it: it hands
11
+ * it to whoever listens here — the embed's host bridge, which raises it on
12
+ * the element as its `window-message` event. A window op of another name is
13
+ * not one this page acts on: refused, it throws.
13
14
  */
14
- /** The `CUSTOM` event's name. */
15
- export const LOOP_WINDOW = 'loop.window';
16
- /** What an activity says to the page, or `null` when it is not a window message. */
17
- export function loopWindowMessage(activity) {
18
- if (!activity || activity.type !== LOOP_WINDOW)
19
- return null;
20
- const said = activity.data;
21
- if (!said || typeof said !== 'object' || !('data' in said))
22
- return null;
23
- return { data: said.data };
15
+ import { AGUI_NAME } from '@datalayer/reactor/ui';
16
+ /** The name of the window op that hands the page a message. */
17
+ export const WINDOW_MESSAGE = 'message';
18
+ /** What a window op hands the page. Throws for a window op of another name. */
19
+ export function windowMessageOf(op) {
20
+ if (op.body.name !== WINDOW_MESSAGE) {
21
+ throw new Error(`A window op named ${op.body.name} is not one this page acts on`);
22
+ }
23
+ const args = op.body.args;
24
+ if (!args || !('data' in args)) {
25
+ throw new Error('A window message says no data');
26
+ }
27
+ return args.data;
24
28
  }
25
29
  const listeners = new Set();
26
30
  /** Hand a window message to whoever listens. */
@@ -36,12 +40,12 @@ export function onLoopWindowMessage(listener) {
36
40
  };
37
41
  }
38
42
  /**
39
- * What a turn's stream says to the page — the `loop.window` events of the
40
- * turn a window message posted by the page ran — and the sentence its
41
- * `RUN_ERROR` said, if it failed.
43
+ * Why the turn a window message posted by the page ran failed — its
44
+ * `RUN_ERROR`'s sentence — or `null`. What its code tells the page back comes
45
+ * on the conversation's ui socket (REACTOR_UI RU-W), never on this stream: a
46
+ * `reactor.ui` event here is refused.
42
47
  */
43
- export function windowTurnOf(stream) {
44
- const said = [];
48
+ export function windowTurnErrorOf(stream) {
45
49
  let error = null;
46
50
  for (const block of stream.split('\n\n')) {
47
51
  const data = block
@@ -61,14 +65,9 @@ export function windowTurnOf(stream) {
61
65
  if (event.type === 'RUN_ERROR') {
62
66
  error = String(event.message ?? 'The turn failed.');
63
67
  }
64
- else if (event.type === 'CUSTOM') {
65
- const message = loopWindowMessage({
66
- type: String(event.name),
67
- data: event.value,
68
- });
69
- if (message)
70
- said.push(message.data);
68
+ else if (event.type === 'CUSTOM' && event.name === AGUI_NAME) {
69
+ throw new Error('A reactor.ui op came on the run stream: it comes over the ui socket.');
71
70
  }
72
71
  }
73
- return { said, error };
72
+ return error;
74
73
  }