@datalayer/agent-runtimes 1.3.10 → 1.3.13
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/README.md +1 -2
- package/lib/AgentNode.js +3 -3
- package/lib/api/runtimes/environments.d.ts +22 -1
- package/lib/api/runtimes/environments.js +31 -0
- package/lib/api/runtimes/healthz.js +4 -2
- package/lib/api/spacer/healthz.js +4 -2
- package/lib/chat/ChatFloating.d.ts +8 -1
- package/lib/chat/ChatFloating.js +5 -2
- package/lib/chat/base/ChatBase.js +39 -25
- package/lib/chat/base/modelChoice.d.ts +62 -0
- package/lib/chat/base/modelChoice.js +56 -0
- package/lib/chat/display/ButtonGlow.d.ts +9 -0
- package/lib/chat/display/ButtonGlow.js +60 -0
- package/lib/chat/display/EmptyState.js +4 -1
- package/lib/chat/header/ChatHeaderBase.js +4 -56
- package/lib/chat/header/ChatViewModeToggle.d.ts +29 -0
- package/lib/chat/header/ChatViewModeToggle.js +55 -0
- package/lib/chat/index.d.ts +1 -0
- package/lib/chat/index.js +1 -0
- package/lib/chat/messages/ChatMessageList.js +13 -5
- package/lib/chat/messages/FormSubmissionMessage.d.ts +17 -0
- package/lib/chat/messages/FormSubmissionMessage.js +49 -0
- package/lib/chat/messages/formSubmission.d.ts +41 -0
- package/lib/chat/messages/formSubmission.js +90 -0
- package/lib/chat/prompt/menus/ModelSelector.js +11 -4
- package/lib/chat/styles/streamdownStyles.js +40 -0
- package/lib/client/mixins/RuntimesMixin.js +1 -1
- package/lib/client/mixins/SpacerMixin.js +3 -3
- package/lib/components/a2ui/A2uiMarkdownProvider.d.ts +11 -0
- package/lib/components/a2ui/A2uiMarkdownProvider.js +13 -0
- package/lib/components/a2ui/index.d.ts +1 -0
- package/lib/components/a2ui/index.js +1 -0
- package/lib/components/code-sandboxes/CodeSandboxLauncher.d.ts +8 -0
- package/lib/components/code-sandboxes/CodeSandboxLauncher.js +54 -5
- package/lib/components/code-sandboxes/codeSandboxEnvironments.d.ts +12 -1
- package/lib/components/code-sandboxes/codeSandboxEnvironments.js +38 -1
- package/lib/components/icons/ArtifactIcon.js +2 -1
- package/lib/examples/A2UiAgentExample.js +36 -223
- package/lib/examples/A2UiJupyterOutputExample.js +11 -1
- package/lib/examples/A2UiViewerExample.d.ts +9 -2
- package/lib/examples/A2UiViewerExample.js +33 -3
- package/lib/examples/DecksAgent.d.ts +1 -1
- package/lib/examples/DecksAgent.js +1 -1
- package/lib/examples/main.js +37 -35
- package/lib/examples/utils/a2uiMarkdownProvider.d.ts +1 -7
- package/lib/examples/utils/a2uiMarkdownProvider.js +6 -8
- package/lib/loop/core/index.d.ts +44 -4
- package/lib/loop/core/index.js +29 -2
- package/lib/loop/embed/LoopEmbed.js +7 -0
- package/lib/loop/index.d.ts +1 -1
- package/lib/loop/index.js +4 -1
- package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +22 -0
- package/lib/loop/plugins/a2ui-surface/InlineSurface.js +64 -0
- package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.d.ts +38 -0
- package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.js +64 -0
- package/lib/loop/plugins/a2ui-surface/index.d.ts +34 -0
- package/lib/loop/plugins/a2ui-surface/index.js +91 -0
- package/lib/loop/plugins/a2ui-surface/toolResult.d.ts +52 -0
- package/lib/loop/plugins/a2ui-surface/toolResult.js +106 -0
- package/lib/loop/plugins/chat/ChatView.js +95 -15
- package/lib/loop/plugins/chat/index.d.ts +14 -0
- package/lib/loop/plugins/chat/index.js +1 -0
- package/lib/loop/plugins/document/index.js +4 -0
- package/lib/loop/plugins/notebook/index.js +4 -0
- package/lib/loop/plugins/page-layout/index.d.ts +36 -0
- package/lib/loop/plugins/page-layout/index.js +87 -7
- package/lib/loop/plugins/shell/useEditorPreviews.js +7 -0
- package/lib/loop/presets.d.ts +19 -0
- package/lib/loop/presets.js +9 -1
- package/lib/models/Deck.d.ts +14 -0
- package/lib/models/Deck.js +5 -0
- package/lib/models/Environment.d.ts +117 -0
- package/lib/models/ItemType.d.ts +1 -1
- package/lib/models/Runtime.d.ts +6 -0
- package/lib/models/SpaceItem.d.ts +2 -1
- package/lib/models/environments.generated.d.ts +35 -1
- package/lib/models/environments.generated.js +1 -1
- package/lib/models/index.d.ts +1 -0
- package/lib/models/index.js +1 -0
- package/lib/protocols/BrowserAgentAdapter.d.ts +9 -1
- package/lib/protocols/BrowserAgentAdapter.js +11 -4
- package/lib/runtimes/actions.d.ts +5 -0
- package/lib/runtimes/actions.js +19 -0
- package/lib/specs/agents/agents.d.ts +3 -1
- package/lib/specs/agents/agents.js +426 -130
- package/lib/specs/frontendTools.d.ts +1 -0
- package/lib/specs/frontendTools.js +31 -0
- package/lib/specs/mcpServers.js +13 -13
- package/lib/specs/models.js +7 -7
- package/lib/specs/outputs.js +9 -0
- package/lib/specs/skills.js +7 -7
- package/lib/specs/teams/teams.js +1 -1
- package/lib/tools/adapters/copilotkit/CopilotKitToolAdapter.js +1 -1
- package/lib/types/outputs.d.ts +2 -0
- package/package.json +10 -4
- package/patches/@primer+react+37.31.0.patch +390 -0
- package/patches/eslint-plugin-react+7.37.5.patch +65 -0
- package/scripts/apply-patches.sh +15 -1
- package/scripts/codegen/compose.py +4 -4
- package/scripts/codegen/generate_agents.py +1 -2
- package/scripts/codegen/generate_frontend_tools.py +1 -1
- package/scripts/codegen/generate_mcp_servers.py +2 -2
- package/scripts/codegen/generate_models.py +33 -8
- package/scripts/codegen/generate_outputs.py +2 -0
- package/scripts/codegen/generate_skills.py +2 -2
- package/scripts/codegen/__pycache__/compose.cpython-313.pyc +0 -0
- package/scripts/codegen/__pycache__/versioning.cpython-313.pyc +0 -0
package/lib/loop/core/index.d.ts
CHANGED
|
@@ -183,12 +183,25 @@ export type LoopWorkspaceContext = {
|
|
|
183
183
|
* publishes, a mounted view subscribes, and neither imports the other.
|
|
184
184
|
*/
|
|
185
185
|
export type PromptChannel = {
|
|
186
|
-
/**
|
|
186
|
+
/**
|
|
187
|
+
* Publish a prompt. Returns whether it will be answered: `false` only when
|
|
188
|
+
* nothing is listening and nothing is coming — a view that has not mounted
|
|
189
|
+
* yet holds the message instead (see {@link createPromptChannel}).
|
|
190
|
+
*/
|
|
187
191
|
submit: (message: string) => boolean;
|
|
188
192
|
/** Listen for prompts. Returns an unsubscribe. */
|
|
189
193
|
subscribe: (listener: (message: string) => void) => () => void;
|
|
190
194
|
};
|
|
191
|
-
/**
|
|
195
|
+
/**
|
|
196
|
+
* Create a prompt channel. The shell owns one per workspace.
|
|
197
|
+
*
|
|
198
|
+
* The shell's composer is on screen before the view that answers it: the
|
|
199
|
+
* editor and its sandbox are megabytes and take a moment, and the prompt is
|
|
200
|
+
* ready to type into immediately. A message sent in that moment used to be
|
|
201
|
+
* dropped — it appeared in the conversation and was never answered, and the
|
|
202
|
+
* only way to find out was to send it again — so an unheard message is held
|
|
203
|
+
* and handed to the first view that subscribes, in the order it was sent.
|
|
204
|
+
*/
|
|
192
205
|
export declare function createPromptChannel(): PromptChannel;
|
|
193
206
|
/**
|
|
194
207
|
* What the active view tells the shell about itself.
|
|
@@ -205,6 +218,15 @@ export type ViewControls = {
|
|
|
205
218
|
stop?: () => void;
|
|
206
219
|
/** Start the view's work over — the chat resets its conversation. */
|
|
207
220
|
newChat?: () => void;
|
|
221
|
+
/**
|
|
222
|
+
* Say something as the reader: the chat sends it as the next turn.
|
|
223
|
+
*
|
|
224
|
+
* For whatever speaks on the reader's behalf — a form pressed on a
|
|
225
|
+
* rendered surface, whose values the agent should answer. The chat had
|
|
226
|
+
* this control from the start and kept it to itself; a host with a
|
|
227
|
+
* message had no way in.
|
|
228
|
+
*/
|
|
229
|
+
send?: (message: string) => void;
|
|
208
230
|
};
|
|
209
231
|
/** Props every view receives. */
|
|
210
232
|
export type LoopViewProps = {
|
|
@@ -548,6 +570,15 @@ export type LoopChatExtrasValue = {
|
|
|
548
570
|
* on the chip's text, which is how the chat tells openers apart already.
|
|
549
571
|
*/
|
|
550
572
|
hiddenOpeners?: string[];
|
|
573
|
+
/**
|
|
574
|
+
* Every opener off, however many there are.
|
|
575
|
+
*
|
|
576
|
+
* `hiddenOpeners` takes a page's exceptions by name; this is the page that
|
|
577
|
+
* wants none of them — a landing page whose composer has moved under a
|
|
578
|
+
* conversation that has started, where a row of openers over it would be
|
|
579
|
+
* offering a first question to somebody who has already asked one.
|
|
580
|
+
*/
|
|
581
|
+
hideOpeners?: boolean;
|
|
551
582
|
/**
|
|
552
583
|
* Told what the agent has used, each time the chat learns it.
|
|
553
584
|
*
|
|
@@ -824,14 +855,23 @@ export type ChatLayoutContribution = {
|
|
|
824
855
|
/**
|
|
825
856
|
* How this layout wants the composer built.
|
|
826
857
|
*
|
|
827
|
-
* `floating-top`
|
|
858
|
+
* `floating-top` and `floating-bottom` are a floating card anchored to
|
|
859
|
+
* that edge of the layout rather than
|
|
828
860
|
* the bottom — a command line over the document, sized by the card.
|
|
829
861
|
* `docked-top` is that same command line standing in the flow instead: the
|
|
830
862
|
* layout gives it a mount point and the composer fills it, so its width is
|
|
831
863
|
* the layout's to decide and the page below starts under it rather than
|
|
832
864
|
* behind it.
|
|
833
865
|
*/
|
|
834
|
-
prompt?: 'docked' | 'floating' | 'floating-top' | 'docked-top';
|
|
866
|
+
prompt?: 'docked' | 'floating' | 'floating-top' | 'floating-bottom' | 'docked-top';
|
|
867
|
+
/**
|
|
868
|
+
* The same, live, for a layout whose stance changes while it is on
|
|
869
|
+
* screen — the page layout moves the composer between a floating card
|
|
870
|
+
* and its side panel as the person picks a display mode. Read over
|
|
871
|
+
* `prompt` by the chat view whenever it is given; `prompt` is then only
|
|
872
|
+
* the stance the layout starts in.
|
|
873
|
+
*/
|
|
874
|
+
promptStance?: ReadonlySignal<'docked' | 'floating' | 'floating-top' | 'floating-bottom' | 'docked-top'>;
|
|
835
875
|
};
|
|
836
876
|
export declare const LoopChatLayout: import("@datalayer/reactor").ContributionPoint<ChatLayoutContribution>;
|
|
837
877
|
/**
|
package/lib/loop/core/index.js
CHANGED
|
@@ -71,13 +71,27 @@ export const IDLE_SANDBOX_TARGET_SIGNAL = {
|
|
|
71
71
|
export function loopSurfaceId(sessionId) {
|
|
72
72
|
return `loop-${sessionId || 'default'}`;
|
|
73
73
|
}
|
|
74
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* Create a prompt channel. The shell owns one per workspace.
|
|
76
|
+
*
|
|
77
|
+
* The shell's composer is on screen before the view that answers it: the
|
|
78
|
+
* editor and its sandbox are megabytes and take a moment, and the prompt is
|
|
79
|
+
* ready to type into immediately. A message sent in that moment used to be
|
|
80
|
+
* dropped — it appeared in the conversation and was never answered, and the
|
|
81
|
+
* only way to find out was to send it again — so an unheard message is held
|
|
82
|
+
* and handed to the first view that subscribes, in the order it was sent.
|
|
83
|
+
*/
|
|
75
84
|
export function createPromptChannel() {
|
|
76
85
|
const listeners = new Set();
|
|
86
|
+
/** Said before anything was listening, waiting for the first view. */
|
|
87
|
+
const waiting = [];
|
|
77
88
|
return {
|
|
78
89
|
submit(message) {
|
|
79
90
|
if (listeners.size === 0) {
|
|
80
|
-
|
|
91
|
+
waiting.push(message);
|
|
92
|
+
// Heard, as far as the sender is concerned: it is going to be
|
|
93
|
+
// answered, so the composer clears and no failure is reported.
|
|
94
|
+
return true;
|
|
81
95
|
}
|
|
82
96
|
for (const listener of [...listeners]) {
|
|
83
97
|
listener(message);
|
|
@@ -86,6 +100,19 @@ export function createPromptChannel() {
|
|
|
86
100
|
},
|
|
87
101
|
subscribe(listener) {
|
|
88
102
|
listeners.add(listener);
|
|
103
|
+
if (waiting.length > 0) {
|
|
104
|
+
const held = waiting.splice(0, waiting.length);
|
|
105
|
+
/*
|
|
106
|
+
* Next tick, not now: `subscribe` is called from an effect as the
|
|
107
|
+
* view mounts, and a listener that started answering inside it would
|
|
108
|
+
* be setting state on a component in the middle of mounting.
|
|
109
|
+
*/
|
|
110
|
+
queueMicrotask(() => {
|
|
111
|
+
for (const message of held) {
|
|
112
|
+
listener(message);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
89
116
|
return () => {
|
|
90
117
|
listeners.delete(listener);
|
|
91
118
|
};
|
|
@@ -66,6 +66,7 @@ export function LoopEmbed({ agentId = '', showHeader = false, chatHeaderActions,
|
|
|
66
66
|
preset.defaultEditor,
|
|
67
67
|
preset.showViewSelector,
|
|
68
68
|
preset.hideChatHeader,
|
|
69
|
+
preset.chatHeaderButtons,
|
|
69
70
|
preset.promptPlacement,
|
|
70
71
|
preset.autoFocusPrompt,
|
|
71
72
|
preset.fullScreenTopOffset,
|
|
@@ -77,6 +78,12 @@ export function LoopEmbed({ agentId = '', showHeader = false, chatHeaderActions,
|
|
|
77
78
|
preset.pageLayoutTurnPanel,
|
|
78
79
|
preset.pageLayoutTurnPanelFooter,
|
|
79
80
|
preset.pageLayoutPrompt,
|
|
81
|
+
preset.pageLayoutPromptAnchor,
|
|
82
|
+
// By its fields: a host that writes the size inline hands over a new
|
|
83
|
+
// object every render, and the reactor must not be rebuilt for that.
|
|
84
|
+
preset.pageLayoutSize?.format,
|
|
85
|
+
preset.pageLayoutSize?.width,
|
|
86
|
+
preset.pageLayoutSize?.height,
|
|
80
87
|
preset.localAgentSpec,
|
|
81
88
|
preset.floatingPrompt,
|
|
82
89
|
preset.editorSelector,
|
package/lib/loop/index.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export { ChatHeaderPlugin, CHAT_HEADER_PLUGIN_NAME, } from './plugins/chat-heade
|
|
|
16
16
|
export { InputPromptPlugin, INPUT_PROMPT_PLUGIN_NAME, type InputPromptPluginConfig, } from './plugins/input-prompt';
|
|
17
17
|
export { createChatExtrasPlugin, CHAT_EXTRAS_PLUGIN_NAME, type ChatExtrasHandle, } from './plugins/chat-extras';
|
|
18
18
|
export { LoopChatExtras, LoopChatLayout, LoopChatTurn, LoopOpeningDocument, LoopOpeningNotebook, LoopPromptPanel, type ChatLayoutContribution, type ChatLayoutParts, type ChatTurnContribution, type ChatTurnSnapshot, type ChatTurnStatus, type LoopChatExtrasValue, type OpeningDocumentContribution, type OpeningNotebookContribution, type PromptPanelContribution, } from './core';
|
|
19
|
-
export { LoopPageLayoutPlugin, LOOP_PAGE_LAYOUT_PLUGIN_NAME, openConversationPanel, pageLayoutSheet, pageLayoutPanelOpen, type LoopPageLayoutConfig, } from './plugins/page-layout';
|
|
19
|
+
export { LoopPageLayoutPlugin, LOOP_PAGE_LAYOUT_PLUGIN_NAME, openConversationPanel, pageLayoutSheet, pageLayoutPanelOpen, setPageLayoutChatMode, type LoopPageLayoutConfig, } from './plugins/page-layout';
|
|
20
20
|
export { useOptionalTeamSelection } from './plugins/agents/useTeamSelection';
|
|
21
21
|
export type { TeamMember, TeamSelection } from './plugins/agents/team';
|
|
22
22
|
export { ChatViewPlugin, CHAT_VIEW_PLUGIN_NAME } from './plugins/chat-view';
|
package/lib/loop/index.js
CHANGED
|
@@ -26,7 +26,10 @@ export { createChatExtrasPlugin, CHAT_EXTRAS_PLUGIN_NAME, } from './plugins/chat
|
|
|
26
26
|
// read back: the chat extras, the opening notebook, and who the team is
|
|
27
27
|
// talking to. A page composing panels around a `LoopEmbed` needs all three.
|
|
28
28
|
export { LoopChatExtras, LoopChatLayout, LoopChatTurn, LoopOpeningDocument, LoopOpeningNotebook, LoopPromptPanel, } from './core';
|
|
29
|
-
export { LoopPageLayoutPlugin, LOOP_PAGE_LAYOUT_PLUGIN_NAME, openConversationPanel, pageLayoutSheet, pageLayoutPanelOpen,
|
|
29
|
+
export { LoopPageLayoutPlugin, LOOP_PAGE_LAYOUT_PLUGIN_NAME, openConversationPanel, pageLayoutSheet, pageLayoutPanelOpen,
|
|
30
|
+
// How a host moves the composer: `sidebar` stands it in the conversation
|
|
31
|
+
// panel, under what was said, and opens the panel to show it.
|
|
32
|
+
setPageLayoutChatMode, } from './plugins/page-layout';
|
|
30
33
|
export { useOptionalTeamSelection } from './plugins/agents/useTeamSelection';
|
|
31
34
|
export { ChatViewPlugin, CHAT_VIEW_PLUGIN_NAME } from './plugins/chat-view';
|
|
32
35
|
export { NotebookViewPlugin, NOTEBOOK_VIEW_PLUGIN_NAME, } from './plugins/notebook-view';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A generated A2UI surface, drawn inline where the agent's answer lands.
|
|
3
|
+
*
|
|
4
|
+
* Its own processor, so the surface can be filled in and submitted here
|
|
5
|
+
* whatever else is showing it. The basic catalogue draws from inherited
|
|
6
|
+
* `--a2ui-*` custom properties, so the surface follows the workspace's theme
|
|
7
|
+
* by inheriting rather than by being told — the same rule the Surface view
|
|
8
|
+
* follows, and what keeps a generated form from being the one light card in
|
|
9
|
+
* a dark workspace.
|
|
10
|
+
*
|
|
11
|
+
* @module loop/plugins/a2ui-surface/InlineSurface
|
|
12
|
+
*/
|
|
13
|
+
import type { JSX } from 'react';
|
|
14
|
+
import { type A2uiClientAction, type A2uiMessage } from '@a2ui/web_core/v0_9';
|
|
15
|
+
/** The catalogue a surface is rewritten to before it is drawn. */
|
|
16
|
+
export declare const SURFACE_CATALOG_ID: string;
|
|
17
|
+
export declare function InlineSurface({ messages, onAction, validationError, }: {
|
|
18
|
+
messages: A2uiMessage[];
|
|
19
|
+
onAction: (action: A2uiClientAction) => void;
|
|
20
|
+
validationError?: string | null;
|
|
21
|
+
}): JSX.Element | null;
|
|
22
|
+
export default InlineSurface;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { Box } from '@datalayer/primer-addons';
|
|
4
|
+
import { basicCatalog, } from '@a2ui/react/v0_9';
|
|
5
|
+
import { MessageProcessor, } from '@a2ui/web_core/v0_9';
|
|
6
|
+
import { A2UI_RENDER_SCOPE_SX, A2uiMarkdownProvider, A2uiSurfaceComposed, } from '../../../components/a2ui';
|
|
7
|
+
/** The catalogue a surface is rewritten to before it is drawn. */
|
|
8
|
+
export const SURFACE_CATALOG_ID = basicCatalog.id;
|
|
9
|
+
const INHERIT_THEME = {
|
|
10
|
+
['--a2ui-color-surface']: 'var(--bgColor-muted)',
|
|
11
|
+
['--a2ui-color-on-surface']: 'var(--fgColor-default)',
|
|
12
|
+
['--a2ui-color-primary']: 'var(--fgColor-accent)',
|
|
13
|
+
['--a2ui-color-outline']: 'var(--borderColor-default)',
|
|
14
|
+
};
|
|
15
|
+
export function InlineSurface({ messages, onAction, validationError, }) {
|
|
16
|
+
// Reached through a ref: the processor is built once, and the handler it
|
|
17
|
+
// was built with must not go stale when the host's does not.
|
|
18
|
+
const onActionRef = useRef(onAction);
|
|
19
|
+
onActionRef.current = onAction;
|
|
20
|
+
const processor = useMemo(() => new MessageProcessor([basicCatalog], action => onActionRef.current(action)), []);
|
|
21
|
+
const [surfaces, setSurfaces] = useState([]);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const created = processor.onSurfaceCreated(surface => setSurfaces(previous => [...previous, surface]));
|
|
24
|
+
const deleted = processor.onSurfaceDeleted(id => setSurfaces(previous => previous.filter(surface => surface.id !== id)));
|
|
25
|
+
return () => {
|
|
26
|
+
created.unsubscribe();
|
|
27
|
+
deleted.unsubscribe();
|
|
28
|
+
};
|
|
29
|
+
}, [processor]);
|
|
30
|
+
// Once: the messages are one tool result, processed when it arrives.
|
|
31
|
+
const processedRef = useRef(false);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (processedRef.current) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
processedRef.current = true;
|
|
37
|
+
processor.processMessages(messages);
|
|
38
|
+
}, [messages, processor]);
|
|
39
|
+
if (surfaces.length === 0) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return (_jsx(A2uiMarkdownProvider, { children: _jsxs(Box, { style: INHERIT_THEME, sx: {
|
|
43
|
+
...A2UI_RENDER_SCOPE_SX,
|
|
44
|
+
display: 'flex',
|
|
45
|
+
flexDirection: 'column',
|
|
46
|
+
gap: 2,
|
|
47
|
+
}, children: [validationError ? (_jsx(Box, { role: "alert", sx: {
|
|
48
|
+
px: 3,
|
|
49
|
+
py: 2,
|
|
50
|
+
borderRadius: 2,
|
|
51
|
+
bg: 'danger.subtle',
|
|
52
|
+
border: '1px solid',
|
|
53
|
+
borderColor: 'danger.muted',
|
|
54
|
+
color: 'danger.fg',
|
|
55
|
+
fontSize: 1,
|
|
56
|
+
}, children: validationError })) : null, surfaces.map(surface => (_jsx(Box, { sx: {
|
|
57
|
+
border: '1px solid',
|
|
58
|
+
borderColor: 'border.default',
|
|
59
|
+
borderRadius: 2,
|
|
60
|
+
p: 3,
|
|
61
|
+
bg: 'canvas.default',
|
|
62
|
+
}, children: _jsx(A2uiSurfaceComposed, { surface: surface }) }, surface.id)))] }) }));
|
|
63
|
+
}
|
|
64
|
+
export default InlineSurface;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One `render_a2ui_surface` result in the transcript: the surface, and what
|
|
3
|
+
* pressing its Submit does.
|
|
4
|
+
*
|
|
5
|
+
* The values are checked against the rules the tool sent with the surface —
|
|
6
|
+
* a missing required field is said under the form — and, when they pass,
|
|
7
|
+
* handed back to the agent as the reader's next turn, so the confirmation
|
|
8
|
+
* arrives in the conversation where the form was asked for. A host that
|
|
9
|
+
* wants to know is told too (`onRendered`, `onSubmitted`): the A2UI Agent
|
|
10
|
+
* example mirrors the surface on a canvas beside the chat.
|
|
11
|
+
*
|
|
12
|
+
* @module loop/plugins/a2ui-surface/SurfaceToolResult
|
|
13
|
+
*/
|
|
14
|
+
import type { JSX } from 'react';
|
|
15
|
+
import type { A2uiMessage } from '@a2ui/web_core/v0_9';
|
|
16
|
+
import type { ToolCallRenderContext } from '../../../types/chat';
|
|
17
|
+
import { type A2uiToolResult } from './toolResult';
|
|
18
|
+
export type SurfaceRendered = {
|
|
19
|
+
toolCallId: string;
|
|
20
|
+
surfaceId?: string;
|
|
21
|
+
title: string;
|
|
22
|
+
messages: A2uiMessage[];
|
|
23
|
+
result: A2uiToolResult;
|
|
24
|
+
};
|
|
25
|
+
export type SurfaceSubmitted = {
|
|
26
|
+
surfaceId: string;
|
|
27
|
+
title: string;
|
|
28
|
+
values: Record<string, unknown>;
|
|
29
|
+
};
|
|
30
|
+
export type SurfaceToolResultProps = {
|
|
31
|
+
context: ToolCallRenderContext;
|
|
32
|
+
/** Say something as the reader; `false` when the chat cannot take it. */
|
|
33
|
+
send: (message: string) => boolean;
|
|
34
|
+
onRendered?: (rendered: SurfaceRendered) => void;
|
|
35
|
+
onSubmitted?: (submitted: SurfaceSubmitted) => void;
|
|
36
|
+
};
|
|
37
|
+
export declare function SurfaceToolResult({ context, send, onRendered, onSubmitted, }: SurfaceToolResultProps): JSX.Element;
|
|
38
|
+
export default SurfaceToolResult;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { Spinner, Text } from '@primer/react';
|
|
4
|
+
import { Box } from '@datalayer/primer-addons';
|
|
5
|
+
import { encodeFormSubmission } from '../../../chat/messages/formSubmission';
|
|
6
|
+
import { InlineSurface, SURFACE_CATALOG_ID } from './InlineSurface';
|
|
7
|
+
import { readA2uiToolResult, validateA2uiSubmission, } from './toolResult';
|
|
8
|
+
export function SurfaceToolResult({ context, send, onRendered, onSubmitted, }) {
|
|
9
|
+
const parsed = useMemo(() => context.status === 'complete'
|
|
10
|
+
? readA2uiToolResult(context.result, SURFACE_CATALOG_ID)
|
|
11
|
+
: null, [context.status, context.result]);
|
|
12
|
+
const title = parsed?.title ?? 'the form';
|
|
13
|
+
const [validationError, setValidationError] = useState(null);
|
|
14
|
+
// Told once per result, when it is there to be told about.
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (parsed?.messages && parsed.messages.length > 0) {
|
|
17
|
+
onRendered?.({
|
|
18
|
+
toolCallId: context.toolCallId,
|
|
19
|
+
surfaceId: parsed.surfaceId,
|
|
20
|
+
title,
|
|
21
|
+
messages: parsed.messages,
|
|
22
|
+
result: parsed,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
// The result is what changes; the callback's identity is the host's.
|
|
26
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
27
|
+
}, [parsed, context.toolCallId]);
|
|
28
|
+
const handleAction = useCallback((action) => {
|
|
29
|
+
if (action.name !== 'submit_a2ui_form') {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const values = (action.context ?? {});
|
|
33
|
+
const errors = validateA2uiSubmission(parsed?.fieldRules ?? [], values);
|
|
34
|
+
if (errors.length > 0) {
|
|
35
|
+
setValidationError(errors.join(' '));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
setValidationError(null);
|
|
39
|
+
onSubmitted?.({ surfaceId: action.surfaceId, title, values });
|
|
40
|
+
const sent = send(encodeFormSubmission({
|
|
41
|
+
title,
|
|
42
|
+
surfaceId: action.surfaceId,
|
|
43
|
+
values,
|
|
44
|
+
}));
|
|
45
|
+
if (!sent) {
|
|
46
|
+
setValidationError('The chat is not ready to take the submission yet.');
|
|
47
|
+
}
|
|
48
|
+
}, [parsed, title, send, onSubmitted]);
|
|
49
|
+
if (!parsed?.messages || parsed.messages.length === 0) {
|
|
50
|
+
return (_jsxs(Box, { sx: {
|
|
51
|
+
border: '1px solid',
|
|
52
|
+
borderColor: 'border.default',
|
|
53
|
+
borderRadius: 2,
|
|
54
|
+
px: 3,
|
|
55
|
+
py: 2,
|
|
56
|
+
bg: 'canvas.default',
|
|
57
|
+
display: 'flex',
|
|
58
|
+
alignItems: 'center',
|
|
59
|
+
gap: 2,
|
|
60
|
+
}, children: [_jsx(Spinner, { size: "small" }), _jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "Rendering A2UI surface..." })] }));
|
|
61
|
+
}
|
|
62
|
+
return (_jsxs(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 2 }, children: [_jsx(Text, { sx: { fontWeight: 'bold' }, children: title }), _jsx(InlineSurface, { messages: parsed.messages, onAction: handleAction, validationError: validationError })] }));
|
|
63
|
+
}
|
|
64
|
+
export default SurfaceToolResult;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ReactorPlugin } from '@datalayer/reactor';
|
|
2
|
+
import { type SurfaceRendered, type SurfaceSubmitted } from './SurfaceToolResult';
|
|
3
|
+
export declare const A2UI_SURFACE_PLUGIN_NAME = "@datalayer/loop-plugin-a2ui-surface";
|
|
4
|
+
/** The renderer name a spec binds a tool to. */
|
|
5
|
+
export declare const A2UI_SURFACE_RENDERER = "a2ui-surface";
|
|
6
|
+
/**
|
|
7
|
+
* Every tool some spec binds to this renderer.
|
|
8
|
+
*
|
|
9
|
+
* Read from the specs rather than from the workspace's current agent: a
|
|
10
|
+
* tool result arrives named by its tool, and a name bound in any spec is a
|
|
11
|
+
* surface wherever it turns up.
|
|
12
|
+
*/
|
|
13
|
+
export declare function surfaceToolNames(): Set<string>;
|
|
14
|
+
export type A2uiSurfacePluginOptions = {
|
|
15
|
+
/** Told when a surface has been drawn from a tool result. */
|
|
16
|
+
onRendered?: (rendered: SurfaceRendered) => void;
|
|
17
|
+
/** Told when a submission passed its checks and went to the agent. */
|
|
18
|
+
onSubmitted?: (submitted: SurfaceSubmitted) => void;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* An instance of the plugin, with a host's callbacks.
|
|
22
|
+
*
|
|
23
|
+
* The chat reaches a workspace's `send` only through slot props, so the
|
|
24
|
+
* plugin keeps a silent component in the chat's title-bar slot — it draws
|
|
25
|
+
* nothing and holds the `send` of the workspace it is in. The slot is asked
|
|
26
|
+
* whether anyone contributed before it is drawn, so this costs the header
|
|
27
|
+
* no space.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createA2uiSurfacePlugin(options?: A2uiSurfacePluginOptions): ReactorPlugin<Record<string, never>, unknown, unknown>;
|
|
30
|
+
/** The instance the preset mounts: no host callbacks. */
|
|
31
|
+
export declare const A2uiSurfacePlugin: ReactorPlugin<Record<string, never>, unknown, unknown>;
|
|
32
|
+
export type { SurfaceRendered, SurfaceSubmitted } from './SurfaceToolResult';
|
|
33
|
+
export { readA2uiToolResult, validateA2uiField, validateA2uiSubmission, type A2uiFieldRule, type A2uiToolResult, } from './toolResult';
|
|
34
|
+
export default A2uiSurfacePlugin;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import { contribution, definePlugin, signal } from '@datalayer/reactor';
|
|
4
|
+
import { AGENTSPECS } from '../../../specs/agents';
|
|
5
|
+
import { LoopChatExtras, LoopSlots, } from '../../core';
|
|
6
|
+
import { SurfaceToolResult, } from './SurfaceToolResult';
|
|
7
|
+
export const A2UI_SURFACE_PLUGIN_NAME = '@datalayer/loop-plugin-a2ui-surface';
|
|
8
|
+
/** The renderer name a spec binds a tool to. */
|
|
9
|
+
export const A2UI_SURFACE_RENDERER = 'a2ui-surface';
|
|
10
|
+
/**
|
|
11
|
+
* Every tool some spec binds to this renderer.
|
|
12
|
+
*
|
|
13
|
+
* Read from the specs rather than from the workspace's current agent: a
|
|
14
|
+
* tool result arrives named by its tool, and a name bound in any spec is a
|
|
15
|
+
* surface wherever it turns up.
|
|
16
|
+
*/
|
|
17
|
+
export function surfaceToolNames() {
|
|
18
|
+
const names = new Set();
|
|
19
|
+
for (const spec of Object.values(AGENTSPECS)) {
|
|
20
|
+
for (const binding of spec.frontendRenderTools ?? []) {
|
|
21
|
+
if (binding.renderer === A2UI_SURFACE_RENDERER) {
|
|
22
|
+
names.add(binding.tool);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return names;
|
|
27
|
+
}
|
|
28
|
+
let counter = 0;
|
|
29
|
+
/**
|
|
30
|
+
* An instance of the plugin, with a host's callbacks.
|
|
31
|
+
*
|
|
32
|
+
* The chat reaches a workspace's `send` only through slot props, so the
|
|
33
|
+
* plugin keeps a silent component in the chat's title-bar slot — it draws
|
|
34
|
+
* nothing and holds the `send` of the workspace it is in. The slot is asked
|
|
35
|
+
* whether anyone contributed before it is drawn, so this costs the header
|
|
36
|
+
* no space.
|
|
37
|
+
*/
|
|
38
|
+
export function createA2uiSurfacePlugin(options = {}) {
|
|
39
|
+
const id = `a2ui-surface-${(counter += 1)}`;
|
|
40
|
+
let currentSend;
|
|
41
|
+
const send = (message) => {
|
|
42
|
+
if (!currentSend) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
currentSend(message);
|
|
46
|
+
return true;
|
|
47
|
+
};
|
|
48
|
+
function Capture({ workspace, }) {
|
|
49
|
+
const workspaceSend = workspace?.viewControls.send;
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
currentSend = workspaceSend;
|
|
52
|
+
return () => {
|
|
53
|
+
if (currentSend === workspaceSend) {
|
|
54
|
+
currentSend = undefined;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}, [workspaceSend]);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const tools = surfaceToolNames();
|
|
61
|
+
// A failed call is the chat's to draw: its default card says what went
|
|
62
|
+
// wrong, and a spinner that never ends would say nothing at all. So is
|
|
63
|
+
// every tool that is not a surface: this renderer is the chat's only one,
|
|
64
|
+
// and returning nothing for a tool it does not own hid every other tool
|
|
65
|
+
// row — the notebook reads, the cell runs — from the transcript.
|
|
66
|
+
const renderToolResult = (context) => tools.has(context.toolName) && context.status !== 'error' ? (_jsx(SurfaceToolResult, { context: context, send: send, onRendered: options.onRendered, onSubmitted: options.onSubmitted }, context.toolCallId)) : (context.defaultUI);
|
|
67
|
+
const plugin = definePlugin({
|
|
68
|
+
name: counter === 1
|
|
69
|
+
? A2UI_SURFACE_PLUGIN_NAME
|
|
70
|
+
: `${A2UI_SURFACE_PLUGIN_NAME}-${id}`,
|
|
71
|
+
displayName: 'A2UI surfaces',
|
|
72
|
+
description: 'Generated surfaces drawn in the transcript, and submitted back to the agent.',
|
|
73
|
+
octicon: 'browser',
|
|
74
|
+
emoji: '\u{1FA9F}',
|
|
75
|
+
contributes: [
|
|
76
|
+
contribution(LoopChatExtras, { id, extras: signal({ renderToolResult }) }, { id }),
|
|
77
|
+
],
|
|
78
|
+
build() {
|
|
79
|
+
return {
|
|
80
|
+
components: [
|
|
81
|
+
{ slot: LoopSlots.chatHeader, id, Component: Capture },
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
return plugin;
|
|
87
|
+
}
|
|
88
|
+
/** The instance the preset mounts: no host callbacks. */
|
|
89
|
+
export const A2uiSurfacePlugin = createA2uiSurfacePlugin();
|
|
90
|
+
export { readA2uiToolResult, validateA2uiField, validateA2uiSubmission, } from './toolResult';
|
|
91
|
+
export default A2uiSurfacePlugin;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the `render_a2ui_surface` tool answers, read and checked.
|
|
3
|
+
*
|
|
4
|
+
* Pure: no React, no catalogue instance. What is here is what a test wants
|
|
5
|
+
* to reach without a chat around it — the shape of a result, the rules a
|
|
6
|
+
* surface ships for its own fields, and the verdict on a submission.
|
|
7
|
+
*
|
|
8
|
+
* @module loop/plugins/a2ui-surface/toolResult
|
|
9
|
+
*/
|
|
10
|
+
import type { A2uiMessage } from '@a2ui/web_core/v0_9';
|
|
11
|
+
export type A2uiRequiredField = {
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Per-field validation descriptor emitted by the backend tool. The rules
|
|
17
|
+
* gate a submission: required, email format, pattern, length, slider range.
|
|
18
|
+
*/
|
|
19
|
+
export type A2uiFieldRule = {
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
type?: string;
|
|
23
|
+
required?: boolean;
|
|
24
|
+
format?: 'email';
|
|
25
|
+
pattern?: string;
|
|
26
|
+
minLength?: number;
|
|
27
|
+
min?: number;
|
|
28
|
+
max?: number;
|
|
29
|
+
};
|
|
30
|
+
export type A2uiToolResult = {
|
|
31
|
+
surfaceId?: string;
|
|
32
|
+
title?: string;
|
|
33
|
+
messages?: A2uiMessage[];
|
|
34
|
+
requiredFields?: A2uiRequiredField[];
|
|
35
|
+
fieldRules?: A2uiFieldRule[];
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Read a tool result — an object, or the JSON of one — as A2UI messages,
|
|
39
|
+
* with the catalogue id rewritten to `catalogId` so the frontend catalogue
|
|
40
|
+
* accepts what a backend named by its own id.
|
|
41
|
+
*/
|
|
42
|
+
export declare function readA2uiToolResult(result: unknown, catalogId: string): A2uiToolResult | null;
|
|
43
|
+
/**
|
|
44
|
+
* Whether a submitted value counts as "not provided". Handles the shapes the
|
|
45
|
+
* basic catalogue emits: strings (text/email), arrays (choice), booleans
|
|
46
|
+
* (checkbox).
|
|
47
|
+
*/
|
|
48
|
+
export declare function isEmptyA2uiValue(value: unknown): boolean;
|
|
49
|
+
/** One rule's complaint about one value, or `null` when it is acceptable. */
|
|
50
|
+
export declare function validateA2uiField(rule: A2uiFieldRule, value: unknown): string | null;
|
|
51
|
+
/** Every rule's complaint about a submission; empty when it passes. */
|
|
52
|
+
export declare function validateA2uiSubmission(rules: A2uiFieldRule[], values: Record<string, unknown>): string[];
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Read a tool result — an object, or the JSON of one — as A2UI messages,
|
|
7
|
+
* with the catalogue id rewritten to `catalogId` so the frontend catalogue
|
|
8
|
+
* accepts what a backend named by its own id.
|
|
9
|
+
*/
|
|
10
|
+
export function readA2uiToolResult(result, catalogId) {
|
|
11
|
+
if (!result) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
let obj = result;
|
|
15
|
+
if (typeof result === 'string') {
|
|
16
|
+
try {
|
|
17
|
+
obj = JSON.parse(result);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (typeof obj !== 'object' ||
|
|
24
|
+
obj === null ||
|
|
25
|
+
!Array.isArray(obj.messages)) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const parsed = obj;
|
|
29
|
+
const messages = (parsed.messages ?? []).map(message => {
|
|
30
|
+
const payload = message;
|
|
31
|
+
if (payload.createSurface &&
|
|
32
|
+
payload.createSurface.catalogId !== catalogId) {
|
|
33
|
+
return {
|
|
34
|
+
...payload,
|
|
35
|
+
createSurface: { ...payload.createSurface, catalogId },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return message;
|
|
39
|
+
});
|
|
40
|
+
return { ...parsed, messages };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Whether a submitted value counts as "not provided". Handles the shapes the
|
|
44
|
+
* basic catalogue emits: strings (text/email), arrays (choice), booleans
|
|
45
|
+
* (checkbox).
|
|
46
|
+
*/
|
|
47
|
+
export function isEmptyA2uiValue(value) {
|
|
48
|
+
if (value === null || value === undefined) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
if (typeof value === 'string') {
|
|
52
|
+
return value.trim() === '';
|
|
53
|
+
}
|
|
54
|
+
if (Array.isArray(value)) {
|
|
55
|
+
return value.length === 0;
|
|
56
|
+
}
|
|
57
|
+
if (typeof value === 'boolean') {
|
|
58
|
+
return value === false;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
/** Permissive on purpose: `local@domain.tld` with no spaces. */
|
|
63
|
+
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
64
|
+
/** One rule's complaint about one value, or `null` when it is acceptable. */
|
|
65
|
+
export function validateA2uiField(rule, value) {
|
|
66
|
+
if (isEmptyA2uiValue(value)) {
|
|
67
|
+
return rule.required ? `${rule.label} is required.` : null;
|
|
68
|
+
}
|
|
69
|
+
if (rule.format === 'email' &&
|
|
70
|
+
typeof value === 'string' &&
|
|
71
|
+
!EMAIL_PATTERN.test(value.trim())) {
|
|
72
|
+
return `${rule.label} must be a valid email address.`;
|
|
73
|
+
}
|
|
74
|
+
if (rule.pattern && typeof value === 'string') {
|
|
75
|
+
let matches;
|
|
76
|
+
try {
|
|
77
|
+
matches = new RegExp(rule.pattern).test(value.trim());
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
matches = true; // A malformed pattern blocks nothing.
|
|
81
|
+
}
|
|
82
|
+
if (!matches) {
|
|
83
|
+
return `${rule.label} is not in the expected format.`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (rule.minLength !== undefined &&
|
|
87
|
+
typeof value === 'string' &&
|
|
88
|
+
value.trim().length < rule.minLength) {
|
|
89
|
+
return `${rule.label} must be at least ${rule.minLength} characters.`;
|
|
90
|
+
}
|
|
91
|
+
if (rule.type === 'slider' && typeof value === 'number') {
|
|
92
|
+
if (rule.min !== undefined && value < rule.min) {
|
|
93
|
+
return `${rule.label} must be at least ${rule.min}.`;
|
|
94
|
+
}
|
|
95
|
+
if (rule.max !== undefined && value > rule.max) {
|
|
96
|
+
return `${rule.label} must be at most ${rule.max}.`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
/** Every rule's complaint about a submission; empty when it passes. */
|
|
102
|
+
export function validateA2uiSubmission(rules, values) {
|
|
103
|
+
return rules
|
|
104
|
+
.map(rule => validateA2uiField(rule, values[rule.id]))
|
|
105
|
+
.filter((message) => message !== null);
|
|
106
|
+
}
|