@datalayer/agent-runtimes 1.3.11 → 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/healthz.js +4 -2
- package/lib/api/spacer/healthz.js +4 -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/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/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/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 +33 -2
- package/lib/loop/core/index.js +29 -2
- package/lib/loop/embed/LoopEmbed.js +1 -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 +81 -10
- 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/shell/useEditorPreviews.js +7 -0
- package/lib/loop/presets.d.ts +5 -0
- package/lib/loop/presets.js +7 -1
- package/lib/models/environments.generated.d.ts +7 -1
- package/lib/models/environments.generated.js +1 -1
- package/lib/specs/agents/agents.d.ts +1 -0
- package/lib/specs/agents/agents.js +112 -0
- package/lib/specs/models.js +3 -3
- package/lib/tools/adapters/copilotkit/CopilotKitToolAdapter.js +1 -1
- 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_models.py +33 -8
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Provides the A2UI v0.9 markdown renderer to every `A2uiSurface` beneath
|
|
4
|
+
* it. Without it, `Text` components fall back to the raw markdown string
|
|
5
|
+
* (`## Title` instead of a rendered heading).
|
|
6
|
+
*
|
|
7
|
+
* In the library rather than the examples: the loop's own surface plugin
|
|
8
|
+
* draws surfaces, and needs the same context the examples had.
|
|
9
|
+
*/
|
|
10
|
+
export declare const A2uiMarkdownProvider: React.FC<React.PropsWithChildren>;
|
|
11
|
+
export default A2uiMarkdownProvider;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { MarkdownContext } from '@a2ui/react/v0_9';
|
|
3
|
+
import { renderMarkdown } from '@a2ui/markdown-it';
|
|
4
|
+
/**
|
|
5
|
+
* Provides the A2UI v0.9 markdown renderer to every `A2uiSurface` beneath
|
|
6
|
+
* it. Without it, `Text` components fall back to the raw markdown string
|
|
7
|
+
* (`## Title` instead of a rendered heading).
|
|
8
|
+
*
|
|
9
|
+
* In the library rather than the examples: the loop's own surface plugin
|
|
10
|
+
* draws surfaces, and needs the same context the examples had.
|
|
11
|
+
*/
|
|
12
|
+
export const A2uiMarkdownProvider = ({ children, }) => (_jsx(MarkdownContext.Provider, { value: renderMarkdown, children: children }));
|
|
13
|
+
export default A2uiMarkdownProvider;
|
|
@@ -25,17 +25,15 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
25
25
|
import { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
|
|
26
26
|
import { Box, setupPrimerPortals } from '@datalayer/primer-addons';
|
|
27
27
|
import { Button, Spinner, Text } from '@primer/react';
|
|
28
|
-
import { basicCatalog } from '@a2ui/react/v0_9';
|
|
29
28
|
import { A2UI_RENDER_SCOPE_SX, A2uiSurfaceComposed } from '../components/a2ui';
|
|
30
29
|
import { ThemedProvider } from './utils/themedProvider';
|
|
31
30
|
import { A2uiMarkdownProvider } from './utils/a2uiMarkdownProvider';
|
|
32
31
|
import { useA2uiProcessor } from './utils/a2ui';
|
|
32
|
+
import { createA2uiSurfacePlugin, validateA2uiSubmission, } from '../loop/plugins/a2ui-surface';
|
|
33
33
|
import { useExampleAgentRuntime } from './hooks/useExampleAgentRuntime';
|
|
34
34
|
import { uniqueAgentId } from './utils/agentId';
|
|
35
|
-
import { useSpecRenderToolResult, specRendererClassName, } from './hooks/useSpecRenderToolResult';
|
|
36
35
|
import { LoopEmbed } from '../loop';
|
|
37
36
|
import { AgentA2uiPlugin } from '../loop/plugins/agent-a2ui';
|
|
38
|
-
import { createChatExtrasPlugin } from '../loop/plugins/chat-extras';
|
|
39
37
|
setupPrimerPortals();
|
|
40
38
|
const AGENT_NAME = 'a2ui-agent';
|
|
41
39
|
const AGENTSPEC_ID = 'example-a2ui-agent';
|
|
@@ -57,156 +55,6 @@ const SUGGESTIONS = [
|
|
|
57
55
|
message: 'Make a product configurator for a laptop with CPU, RAM and add-ons.',
|
|
58
56
|
},
|
|
59
57
|
];
|
|
60
|
-
/**
|
|
61
|
-
* Whether a submitted form value should count as "not provided" for the
|
|
62
|
-
* purpose of required-field validation. Handles the value shapes the A2UI
|
|
63
|
-
* basic catalog emits: strings (text/email), arrays (choice), booleans
|
|
64
|
-
* (checkbox).
|
|
65
|
-
*/
|
|
66
|
-
function isEmptyValue(value) {
|
|
67
|
-
if (value === null || value === undefined) {
|
|
68
|
-
return true;
|
|
69
|
-
}
|
|
70
|
-
if (typeof value === 'string') {
|
|
71
|
-
return value.trim() === '';
|
|
72
|
-
}
|
|
73
|
-
if (Array.isArray(value)) {
|
|
74
|
-
return value.length === 0;
|
|
75
|
-
}
|
|
76
|
-
if (typeof value === 'boolean') {
|
|
77
|
-
return value === false;
|
|
78
|
-
}
|
|
79
|
-
return false;
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Basic email format check (matches the intent of the backend `email` field
|
|
83
|
-
* type). Intentionally permissive: requires `local@domain.tld` with no spaces.
|
|
84
|
-
*/
|
|
85
|
-
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
86
|
-
/**
|
|
87
|
-
* Validate a single submitted value against its backend-provided rule. Returns
|
|
88
|
-
* a human-readable error message, or `null` when the value is acceptable.
|
|
89
|
-
*/
|
|
90
|
-
function validateFieldValue(rule, value) {
|
|
91
|
-
const empty = isEmptyValue(value);
|
|
92
|
-
if (empty) {
|
|
93
|
-
// Optional fields left blank are fine; only required ones fail here.
|
|
94
|
-
return rule.required ? `${rule.label} is required.` : null;
|
|
95
|
-
}
|
|
96
|
-
if (rule.format === 'email' &&
|
|
97
|
-
typeof value === 'string' &&
|
|
98
|
-
!EMAIL_PATTERN.test(value.trim())) {
|
|
99
|
-
return `${rule.label} must be a valid email address.`;
|
|
100
|
-
}
|
|
101
|
-
if (rule.pattern && typeof value === 'string') {
|
|
102
|
-
let matches;
|
|
103
|
-
try {
|
|
104
|
-
matches = new RegExp(rule.pattern).test(value.trim());
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
matches = true; // Ignore malformed patterns rather than blocking.
|
|
108
|
-
}
|
|
109
|
-
if (!matches) {
|
|
110
|
-
return `${rule.label} is not in the expected format.`;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
if (rule.minLength !== undefined &&
|
|
114
|
-
typeof value === 'string' &&
|
|
115
|
-
value.trim().length < rule.minLength) {
|
|
116
|
-
return `${rule.label} must be at least ${rule.minLength} characters.`;
|
|
117
|
-
}
|
|
118
|
-
if (rule.type === 'slider' && typeof value === 'number') {
|
|
119
|
-
if (rule.min !== undefined && value < rule.min) {
|
|
120
|
-
return `${rule.label} must be at least ${rule.min}.`;
|
|
121
|
-
}
|
|
122
|
-
if (rule.max !== undefined && value > rule.max) {
|
|
123
|
-
return `${rule.label} must be at most ${rule.max}.`;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Normalize a backend tool result (object or JSON string) into A2UI messages,
|
|
130
|
-
* rewriting the catalog id to the frontend catalog so the renderer accepts it.
|
|
131
|
-
*/
|
|
132
|
-
function extractToolResult(result) {
|
|
133
|
-
if (!result) {
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
let obj = result;
|
|
137
|
-
if (typeof result === 'string') {
|
|
138
|
-
try {
|
|
139
|
-
obj = JSON.parse(result);
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
if (typeof obj !== 'object' ||
|
|
146
|
-
obj === null ||
|
|
147
|
-
!Array.isArray(obj.messages)) {
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
const parsed = obj;
|
|
151
|
-
const messages = (parsed.messages ?? []).map(message => {
|
|
152
|
-
const payload = message;
|
|
153
|
-
if (payload.createSurface &&
|
|
154
|
-
payload.createSurface.catalogId !== basicCatalog.id) {
|
|
155
|
-
return {
|
|
156
|
-
...payload,
|
|
157
|
-
createSurface: {
|
|
158
|
-
...payload.createSurface,
|
|
159
|
-
catalogId: basicCatalog.id,
|
|
160
|
-
},
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
return message;
|
|
164
|
-
});
|
|
165
|
-
return { ...parsed, messages };
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Renders a fully interactive A2UI surface *inline* in the chat transcript.
|
|
169
|
-
*
|
|
170
|
-
* Uses its own `useA2uiProcessor` instance (independent from the canvas
|
|
171
|
-
* processor) so the same generated surface can be filled in and submitted from
|
|
172
|
-
* both places. Messages are processed once on mount.
|
|
173
|
-
*/
|
|
174
|
-
const InlineA2uiSurface = ({ messages, onAction, validationError }) => {
|
|
175
|
-
const { surfaces, processMessages, resetSurfaces, themeStyle } = useA2uiProcessor(onAction);
|
|
176
|
-
const processedRef = useRef(false);
|
|
177
|
-
useEffect(() => {
|
|
178
|
-
if (processedRef.current) {
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
processedRef.current = true;
|
|
182
|
-
resetSurfaces();
|
|
183
|
-
processMessages(messages);
|
|
184
|
-
}, [messages, processMessages, resetSurfaces]);
|
|
185
|
-
if (surfaces.length === 0) {
|
|
186
|
-
return null;
|
|
187
|
-
}
|
|
188
|
-
return (_jsxs(Box, { style: themeStyle, sx: {
|
|
189
|
-
...A2UI_RENDER_SCOPE_SX,
|
|
190
|
-
display: 'flex',
|
|
191
|
-
flexDirection: 'column',
|
|
192
|
-
gap: 2,
|
|
193
|
-
}, children: [validationError && (_jsx(Box, { role: "alert", sx: {
|
|
194
|
-
px: 3,
|
|
195
|
-
py: 2,
|
|
196
|
-
borderRadius: 2,
|
|
197
|
-
bg: 'danger.subtle',
|
|
198
|
-
border: '1px solid',
|
|
199
|
-
borderColor: 'danger.muted',
|
|
200
|
-
color: 'danger.fg',
|
|
201
|
-
fontSize: 1,
|
|
202
|
-
}, children: validationError })), surfaces.map(surface => (_jsx(Box, { sx: {
|
|
203
|
-
border: '1px solid',
|
|
204
|
-
borderColor: 'border.default',
|
|
205
|
-
borderRadius: 2,
|
|
206
|
-
p: 3,
|
|
207
|
-
bg: 'canvas.default',
|
|
208
|
-
}, children: _jsx(A2uiSurfaceComposed, { surface: surface }) }, surface.id)))] }));
|
|
209
|
-
};
|
|
210
58
|
const A2UiAgentExample = () => {
|
|
211
59
|
const agentName = useMemo(() => uniqueAgentId(AGENT_NAME), []);
|
|
212
60
|
const { agentId, error: chatError, status, isReady, baseUrl, } = useExampleAgentRuntime({
|
|
@@ -221,8 +69,6 @@ const A2UiAgentExample = () => {
|
|
|
221
69
|
const isCreatingChat = !isReady && status !== 'error';
|
|
222
70
|
const [surfaceTitle, setSurfaceTitle] = useState(null);
|
|
223
71
|
const [submissions, setSubmissions] = useState([]);
|
|
224
|
-
const [pendingPrompt, setPendingPrompt] = useState(undefined);
|
|
225
|
-
void pendingPrompt;
|
|
226
72
|
const [validationError, setValidationError] = useState(null);
|
|
227
73
|
const submissionSeq = useRef(0);
|
|
228
74
|
// Validation rules per surface id, populated from each tool result so the
|
|
@@ -241,9 +87,7 @@ const A2UiAgentExample = () => {
|
|
|
241
87
|
// Block submission when any field fails validation (required, email
|
|
242
88
|
// format, pattern, length, slider range).
|
|
243
89
|
const rules = fieldRulesBySurfaceRef.current[action.surfaceId] ?? [];
|
|
244
|
-
const errors = rules
|
|
245
|
-
.map(rule => validateFieldValue(rule, values[rule.id]))
|
|
246
|
-
.filter((message) => message !== null);
|
|
90
|
+
const errors = validateA2uiSubmission(rules, values);
|
|
247
91
|
if (errors.length > 0) {
|
|
248
92
|
setValidationError({
|
|
249
93
|
surfaceId: action.surfaceId,
|
|
@@ -252,23 +96,19 @@ const A2UiAgentExample = () => {
|
|
|
252
96
|
return;
|
|
253
97
|
}
|
|
254
98
|
setValidationError(null);
|
|
99
|
+
// The canvas copy: recorded here, since the transcript's own copy is
|
|
100
|
+
// the one that goes back to the agent.
|
|
255
101
|
submissionSeq.current += 1;
|
|
256
|
-
|
|
257
|
-
setSubmissions(prev => [
|
|
258
|
-
{
|
|
259
|
-
id: seq,
|
|
260
|
-
values,
|
|
261
|
-
},
|
|
262
|
-
...prev,
|
|
263
|
-
]);
|
|
264
|
-
// Feed the submission back to the agent so it replies with a confirmation.
|
|
265
|
-
const title = surfaceTitleRef.current ?? 'the form';
|
|
266
|
-
const json = JSON.stringify(values, null, 2);
|
|
267
|
-
setPendingPrompt(`I just submitted "${title}" (submission #${seq}). Here are the values:\n\n\`\`\`json\n${json}\n\`\`\`\n\nPlease confirm you received them and briefly summarize what happens next.`);
|
|
102
|
+
setSubmissions(prev => [{ id: submissionSeq.current, values }, ...prev]);
|
|
268
103
|
}
|
|
269
104
|
}, []);
|
|
270
105
|
const { surfaces, processMessages, resetSurfaces, themeStyle } = useA2uiProcessor(handleAction);
|
|
271
|
-
|
|
106
|
+
/*
|
|
107
|
+
* The transcript's surfaces are the loop's own — the preset's surface
|
|
108
|
+
* plugin draws, checks and submits them. This example asks for an instance
|
|
109
|
+
* of its own so the canvas beside the chat can mirror what was drawn, and
|
|
110
|
+
* the "Submitted values" panel can record what went back to the agent.
|
|
111
|
+
*/
|
|
272
112
|
const showSurfaceRef = useRef(() => { });
|
|
273
113
|
useEffect(() => {
|
|
274
114
|
showSurfaceRef.current = (messages) => {
|
|
@@ -276,59 +116,27 @@ const A2UiAgentExample = () => {
|
|
|
276
116
|
processMessages(messages);
|
|
277
117
|
};
|
|
278
118
|
}, [resetSurfaces, processMessages]);
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
context.toolCallId &&
|
|
289
|
-
!processedToolCallIds.current.has(context.toolCallId)) {
|
|
290
|
-
processedToolCallIds.current.add(context.toolCallId);
|
|
291
|
-
showSurfaceRef.current(parsed.messages);
|
|
292
|
-
setSurfaceTitle(parsed.title ?? 'A2UI surface');
|
|
293
|
-
if (parsed.surfaceId) {
|
|
294
|
-
fieldRulesBySurfaceRef.current[parsed.surfaceId] =
|
|
295
|
-
parsed.fieldRules ?? [];
|
|
119
|
+
const chatPlugins = useMemo(() => [
|
|
120
|
+
AgentA2uiPlugin,
|
|
121
|
+
createA2uiSurfacePlugin({
|
|
122
|
+
onRendered: rendered => {
|
|
123
|
+
showSurfaceRef.current(rendered.messages);
|
|
124
|
+
setSurfaceTitle(rendered.title);
|
|
125
|
+
if (rendered.surfaceId) {
|
|
126
|
+
fieldRulesBySurfaceRef.current[rendered.surfaceId] =
|
|
127
|
+
rendered.result.fieldRules ?? [];
|
|
296
128
|
}
|
|
297
129
|
setValidationError(null);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
validationError.surfaceId === parsed.surfaceId
|
|
309
|
-
? validationError.message
|
|
310
|
-
: null })] })) : (_jsxs(Box, { sx: {
|
|
311
|
-
border: '1px solid',
|
|
312
|
-
borderColor: 'border.default',
|
|
313
|
-
borderRadius: 2,
|
|
314
|
-
px: 3,
|
|
315
|
-
py: 2,
|
|
316
|
-
bg: 'canvas.default',
|
|
317
|
-
display: 'flex',
|
|
318
|
-
alignItems: 'center',
|
|
319
|
-
gap: 2,
|
|
320
|
-
}, children: [_jsx(Spinner, { size: "small" }), _jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "Rendering A2UI surface..." })] })) }));
|
|
321
|
-
},
|
|
322
|
-
}), [handleAction, validationError]);
|
|
323
|
-
const renderToolResult = useSpecRenderToolResult(AGENTSPEC_ID, renderers);
|
|
324
|
-
// The chat column is the shared loop on the A2UI capacity plugin; the
|
|
325
|
-
// example's own surface renderer reaches the transcript through the
|
|
326
|
-
// chat-extras channel, so tool results still draw as A2UI surfaces.
|
|
327
|
-
const { plugin: extrasPlugin, setExtras } = useMemo(() => createChatExtrasPlugin(), []);
|
|
328
|
-
const chatPlugins = useMemo(() => [AgentA2uiPlugin, extrasPlugin], [extrasPlugin]);
|
|
329
|
-
useEffect(() => {
|
|
330
|
-
setExtras({ renderToolResult });
|
|
331
|
-
}, [renderToolResult, setExtras]);
|
|
130
|
+
},
|
|
131
|
+
onSubmitted: submitted => {
|
|
132
|
+
submissionSeq.current += 1;
|
|
133
|
+
setSubmissions(prev => [
|
|
134
|
+
{ id: submissionSeq.current, values: submitted.values },
|
|
135
|
+
...prev,
|
|
136
|
+
]);
|
|
137
|
+
},
|
|
138
|
+
}),
|
|
139
|
+
], []);
|
|
332
140
|
const clearCanvas = useCallback(() => {
|
|
333
141
|
resetSurfaces();
|
|
334
142
|
setSurfaceTitle(null);
|
|
@@ -446,6 +254,11 @@ const A2UiAgentExample = () => {
|
|
|
446
254
|
justifyContent: 'center',
|
|
447
255
|
height: '100%',
|
|
448
256
|
gap: 3,
|
|
449
|
-
}, children: [_jsx(Spinner, { size: "large" }), _jsx(Text, { sx: { color: 'fg.muted' }, children: "Creating A2UI agent..." })] })) : chatError || !agentId ? (_jsx(Box, { sx: { p: 3 }, children: _jsxs(Text, { sx: { color: 'danger.fg' }, children: ["Failed to initialize chat:", ' ', chatError || 'No agent id returned'] }) })) : (_jsx(LoopEmbed, { serverUrl: baseUrl, target: "local",
|
|
257
|
+
}, children: [_jsx(Spinner, { size: "large" }), _jsx(Text, { sx: { color: 'fg.muted' }, children: "Creating A2UI agent..." })] })) : chatError || !agentId ? (_jsx(Box, { sx: { p: 3 }, children: _jsxs(Text, { sx: { color: 'danger.fg' }, children: ["Failed to initialize chat:", ' ', chatError || 'No agent id returned'] }) })) : (_jsx(LoopEmbed, { serverUrl: baseUrl, target: "local",
|
|
258
|
+
// What lets the target take: without the choice the agents
|
|
259
|
+
// plugin pins the sandbox to the page, whatever `target` says,
|
|
260
|
+
// and the chat then turns in-page, where the server's tools
|
|
261
|
+
// do not exist.
|
|
262
|
+
showAgentVariants: true, agentId: agentId, defaultEditor: "none", showHeader: true, plugins: chatPlugins })) })] }) }) }));
|
|
450
263
|
};
|
|
451
264
|
export default A2UiAgentExample;
|
|
@@ -765,7 +765,17 @@ const A2UiJupyterOutputExample = () => {
|
|
|
765
765
|
borderRadius: 2,
|
|
766
766
|
overflow: 'hidden',
|
|
767
767
|
bg: 'canvas.default',
|
|
768
|
-
}, children: agentReady && agentId ? (_jsx(LoopEmbed, { serverUrl: serverUrl, target: "local",
|
|
768
|
+
}, children: agentReady && agentId ? (_jsx(LoopEmbed, { serverUrl: serverUrl, target: "local",
|
|
769
|
+
// No `showAgentVariants` on purpose: without the choice the
|
|
770
|
+
// agents plugin pins the sandbox to the page, and this chat is
|
|
771
|
+
// meant to turn in the page. The server agent above speaks
|
|
772
|
+
// vercel-ai and only runs the code; it has no AG-UI route, so a
|
|
773
|
+
// chat addressing it on the server got "404 Not Found". The
|
|
774
|
+
// demo tool below is a frontend tool, and the in-page loop
|
|
775
|
+
// calls it.
|
|
776
|
+
agentId: agentId, defaultEditor: "none", showHeader: true,
|
|
777
|
+
// A reader asks for one demonstration after another: `+` starts over.
|
|
778
|
+
chatHeaderButtons: true, plugins: chatPlugins })) : (_jsxs(Box, { sx: {
|
|
769
779
|
height: '100%',
|
|
770
780
|
display: 'flex',
|
|
771
781
|
alignItems: 'center',
|
|
@@ -3,8 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The scene lives in `./scenes/A2UiViewerScene` and arrives as an A2UI scene
|
|
5
5
|
* plugin — one workspace view, ordered ahead of the chat. The rest is the
|
|
6
|
-
* standard chat plugins
|
|
7
|
-
*
|
|
6
|
+
* standard chat plugins.
|
|
7
|
+
*
|
|
8
|
+
* The workspace runs on whatever the header's target says, like the examples
|
|
9
|
+
* with an agent of their own. It used to be pinned to the browser — there is
|
|
10
|
+
* no agent behind the scene, so the page was the cheapest place — which left
|
|
11
|
+
* the header's *Local* selected and ignored: the sandbox stayed in the page.
|
|
12
|
+
* On Local the loop asks the local agent-runtimes server for its shell agent
|
|
13
|
+
* and the Jupyter server that agent starts beside itself, which is what
|
|
14
|
+
* choosing Local means everywhere else here.
|
|
8
15
|
*
|
|
9
16
|
* @module examples/A2UiViewerExample
|
|
10
17
|
*/
|
|
@@ -8,8 +8,15 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
8
8
|
*
|
|
9
9
|
* The scene lives in `./scenes/A2UiViewerScene` and arrives as an A2UI scene
|
|
10
10
|
* plugin — one workspace view, ordered ahead of the chat. The rest is the
|
|
11
|
-
* standard chat plugins
|
|
12
|
-
*
|
|
11
|
+
* standard chat plugins.
|
|
12
|
+
*
|
|
13
|
+
* The workspace runs on whatever the header's target says, like the examples
|
|
14
|
+
* with an agent of their own. It used to be pinned to the browser — there is
|
|
15
|
+
* no agent behind the scene, so the page was the cheapest place — which left
|
|
16
|
+
* the header's *Local* selected and ignored: the sandbox stayed in the page.
|
|
17
|
+
* On Local the loop asks the local agent-runtimes server for its shell agent
|
|
18
|
+
* and the Jupyter server that agent starts beside itself, which is what
|
|
19
|
+
* choosing Local means everywhere else here.
|
|
13
20
|
*
|
|
14
21
|
* @module examples/A2UiViewerExample
|
|
15
22
|
*/
|
|
@@ -17,6 +24,8 @@ import { useMemo } from 'react';
|
|
|
17
24
|
import { Box, setupPrimerPortals } from '@datalayer/primer-addons';
|
|
18
25
|
import { ThemedProvider } from './utils/themedProvider';
|
|
19
26
|
import { LoopEmbed } from '../loop';
|
|
27
|
+
import { useRuntimeTargetStore } from './utils/runtimeTargetStore';
|
|
28
|
+
import { useExampleAgentRuntimesUrl } from './utils/useExampleAgentRuntimesUrl';
|
|
20
29
|
import { defineA2uiScenePlugin } from '../loop/plugins/a2ui-scene';
|
|
21
30
|
setupPrimerPortals();
|
|
22
31
|
const ScenePlugin = defineA2uiScenePlugin({
|
|
@@ -25,8 +34,29 @@ const ScenePlugin = defineA2uiScenePlugin({
|
|
|
25
34
|
description: 'Paste A2UI protocol messages and watch the surface they describe.',
|
|
26
35
|
load: () => import('./scenes/A2UiViewerScene'),
|
|
27
36
|
});
|
|
37
|
+
const AGENTSPEC_ID = 'example-a2ui-viewer';
|
|
28
38
|
const A2UiViewerExample = () => {
|
|
39
|
+
/*
|
|
40
|
+
* The agent's surfaces, and what pressing their Submit does, are the
|
|
41
|
+
* loop's own: its spec binds `render_a2ui_surface` to the `a2ui-surface`
|
|
42
|
+
* renderer, and the preset's surface plugin draws, checks and submits.
|
|
43
|
+
* Nothing here but the scene.
|
|
44
|
+
*/
|
|
29
45
|
const plugins = useMemo(() => [ScenePlugin], []);
|
|
30
|
-
|
|
46
|
+
// The header's choice; the shell remounts this example when it changes.
|
|
47
|
+
const target = useRuntimeTargetStore(state => state.target);
|
|
48
|
+
const serverUrl = useExampleAgentRuntimesUrl();
|
|
49
|
+
return (_jsx(ThemedProvider, { children: _jsx(Box, { sx: { height: '100vh', minHeight: 0 }, children: _jsx(LoopEmbed, { target: target,
|
|
50
|
+
// An in-page agent has no server to ask; the others need to know
|
|
51
|
+
// where theirs is.
|
|
52
|
+
serverUrl: target === 'browser' ? undefined : serverUrl, agentId: AGENTSPEC_ID,
|
|
53
|
+
// What a server target creates that agent from. Without it the
|
|
54
|
+
// chat on Local posted to an agent nobody had created — a 404.
|
|
55
|
+
localAgentSpec: AGENTSPEC_ID,
|
|
56
|
+
// What lets the header's target take: without the choice the
|
|
57
|
+
// sandbox plugin pins itself to the page, whatever `target` says.
|
|
58
|
+
showAgentVariants: true, defaultEditor: "none", showHeader: true,
|
|
59
|
+
// A reader asks for one surface after another: `+` starts over.
|
|
60
|
+
chatHeaderButtons: true, plugins: plugins }) }) }));
|
|
31
61
|
};
|
|
32
62
|
export default A2UiViewerExample;
|
package/lib/examples/main.js
CHANGED
|
@@ -259,6 +259,23 @@ const ensureLocalJupyterToken = () => {
|
|
|
259
259
|
setJupyterServerToken(DEFAULT_LOCAL_JUPYTER_SERVER_TOKEN);
|
|
260
260
|
}
|
|
261
261
|
};
|
|
262
|
+
/**
|
|
263
|
+
* Whether somebody signed in to this browser and is still signed in.
|
|
264
|
+
*
|
|
265
|
+
* The shell's own persisted sign-in, or the token core stored at sign-in —
|
|
266
|
+
* either one is a person, and a configured key must not stand in for them.
|
|
267
|
+
*/
|
|
268
|
+
const hasPersistedSignIn = () => {
|
|
269
|
+
try {
|
|
270
|
+
if (useSimpleAuthStore.getState().token) {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
return Boolean(window.localStorage.getItem(DATALAYER_IAM_TOKEN_KEY));
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
262
279
|
// Load configurations from DOM
|
|
263
280
|
const loadConfigurations = () => {
|
|
264
281
|
// Load Datalayer configuration
|
|
@@ -277,36 +294,22 @@ const loadConfigurations = () => {
|
|
|
277
294
|
if (datalayerConfig.iamUrl) {
|
|
278
295
|
datalayerConfig.runtimesUrl = resolveRuntimesUrl(datalayerConfig.runtimesUrl);
|
|
279
296
|
coreStore.getState().setConfiguration(datalayerConfig);
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
settings: {},
|
|
297
|
-
unsubscribedFromOutbounds: false,
|
|
298
|
-
onboarding: {
|
|
299
|
-
clients: {
|
|
300
|
-
Platform: 0,
|
|
301
|
-
JupyterLab: 0,
|
|
302
|
-
CLI: 0,
|
|
303
|
-
VSCode: 0,
|
|
304
|
-
},
|
|
305
|
-
position: 'top',
|
|
306
|
-
tours: {},
|
|
307
|
-
},
|
|
308
|
-
events: [],
|
|
309
|
-
}, datalayerConfig.token);
|
|
297
|
+
/*
|
|
298
|
+
A configured token — `VITE_DATALAYER_API_KEY` — is for a browser
|
|
299
|
+
nobody has signed in to. Somebody's own sign-in wins over it.
|
|
300
|
+
|
|
301
|
+
This used to sign the configured token in as a made-up user,
|
|
302
|
+
`example-id`, on every load. Core claims the browser's session for
|
|
303
|
+
whoever signs in and forgets it when a *different* person appears —
|
|
304
|
+
and a made-up user is a different person from whoever really signed
|
|
305
|
+
in, so every refresh wiped the real session: token, user and the
|
|
306
|
+
persisted sign-in of the shell. Now the token is only set, and core
|
|
307
|
+
asks IAM who it belongs to, which claims the session under the real
|
|
308
|
+
identity or logs the token out if IAM refuses it.
|
|
309
|
+
*/
|
|
310
|
+
if (datalayerConfig.token && !hasPersistedSignIn()) {
|
|
311
|
+
iamStore.setState({ token: datalayerConfig.token });
|
|
312
|
+
void iamStore.getState().refreshUserByToken(datalayerConfig.token);
|
|
310
313
|
}
|
|
311
314
|
}
|
|
312
315
|
}
|
|
@@ -476,12 +479,11 @@ const AgentRuntimesIAMCallback = () => {
|
|
|
476
479
|
const providerAccessToken = provider
|
|
477
480
|
? params.get(`${provider}_access_token`)
|
|
478
481
|
: null;
|
|
482
|
+
// Only the providers whose token the browser is handed. A LinkedIn or
|
|
483
|
+
// Bluesky token never leaves IAM (IBrowserTokenProviderName), so there is
|
|
484
|
+
// nothing of theirs to keep here.
|
|
479
485
|
if (providerAccessToken &&
|
|
480
|
-
(provider === 'github' ||
|
|
481
|
-
provider === 'google' ||
|
|
482
|
-
provider === 'linkedin' ||
|
|
483
|
-
provider === 'okta' ||
|
|
484
|
-
provider === 'bluesky')) {
|
|
486
|
+
(provider === 'github' || provider === 'google' || provider === 'okta')) {
|
|
485
487
|
iamStore
|
|
486
488
|
.getState()
|
|
487
489
|
.setIAMProviderAccessToken(provider, providerAccessToken);
|
|
@@ -1,7 +1 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* Provides the A2UI v0.9 markdown renderer to all child `A2uiSurface`
|
|
4
|
-
* instances. Without it, `Text` components fall back to the raw markdown
|
|
5
|
-
* string (e.g. `## Title` instead of a rendered `<h2>`).
|
|
6
|
-
*/
|
|
7
|
-
export declare const A2uiMarkdownProvider: React.FC<React.PropsWithChildren>;
|
|
1
|
+
export { A2uiMarkdownProvider } from '../../components/a2ui';
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Provides the A2UI v0.9 markdown renderer to all child `A2uiSurface`
|
|
6
|
-
* instances. Without it, `Text` components fall back to the raw markdown
|
|
7
|
-
* string (e.g. `## Title` instead of a rendered `<h2>`).
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
8
4
|
*/
|
|
9
|
-
|
|
5
|
+
// Moved into the library, beside the surface components: the loop's own
|
|
6
|
+
// surface plugin needs the same context the examples had.
|
|
7
|
+
export { A2uiMarkdownProvider } from '../../components/a2ui';
|
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
|
*
|