@datalayer/agent-runtimes 1.3.94 → 1.3.96

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 (44) hide show
  1. package/dist-embed/chunks/AgentInspector-DaMCROPs.js +1 -0
  2. package/dist-embed/chunks/{AgentInspectorDialog-LBbHMnDa.js → AgentInspectorDialog-zGMiFR5z.js} +1 -1
  3. package/dist-embed/chunks/ChatView-DE-jo1oJ.js +288 -0
  4. package/dist-embed/datalayer-app-main.js +110 -110
  5. package/dist-embed/datalayer-app.js +1 -1
  6. package/lib/apps/__tests__/fixtures/sceneCheckCases.json +352 -0
  7. package/lib/apps/apps/computer.d.ts +25 -1
  8. package/lib/apps/apps/computer.js +45 -2
  9. package/lib/apps/apps/sceneChecks.d.ts +92 -0
  10. package/lib/apps/apps/sceneChecks.js +649 -0
  11. package/lib/apps/apps/sceneShape.d.ts +8 -0
  12. package/lib/apps/apps/sceneShape.js +191 -0
  13. package/lib/apps/apps/sceneYaml.d.ts +64 -0
  14. package/lib/apps/apps/sceneYaml.js +253 -0
  15. package/lib/apps/core/canvasParts.d.ts +28 -0
  16. package/lib/apps/core/canvasParts.js +14 -0
  17. package/lib/apps/core/index.d.ts +1 -0
  18. package/lib/apps/core/index.js +2 -0
  19. package/lib/apps/embed/loader.js +10 -0
  20. package/lib/apps/plugins/agents/SandboxSelector.js +13 -2
  21. package/lib/apps/plugins/agents/host.d.ts +2 -0
  22. package/lib/apps/plugins/agents/host.js +16 -0
  23. package/lib/apps/plugins/agents/plugin.js +10 -2
  24. package/lib/apps/plugins/agents/switchable.d.ts +12 -0
  25. package/lib/apps/plugins/agents/switchable.js +23 -0
  26. package/lib/apps/plugins/app-computer/AppComputer.js +18 -2
  27. package/lib/apps/plugins/canvas-parts/index.d.ts +44 -0
  28. package/lib/apps/plugins/canvas-parts/index.js +209 -0
  29. package/lib/components/teams/useA2ATeam.js +1 -1
  30. package/lib/examples/HomeExample.js +1 -1
  31. package/lib/examples/ScenesExample.d.ts +1 -1
  32. package/lib/examples/example-selector.js +1 -1
  33. package/lib/hooks/useAgUi.js +4 -1
  34. package/lib/protocols/AGUIAdapter.js +5 -1
  35. package/lib/protocols/agUiRefusal.d.ts +47 -0
  36. package/lib/protocols/agUiRefusal.js +87 -0
  37. package/lib/specs/apps.js +32 -24
  38. package/lib/specs/scenes.js +147 -142
  39. package/lib/specs/teams/teams.js +13 -13
  40. package/package.json +3 -1
  41. package/scripts/dist-embed.mjs +116 -0
  42. package/scripts/record-scene-checks.py +600 -0
  43. package/dist-embed/chunks/AgentInspector-DfPciQ79.js +0 -1
  44. package/dist-embed/chunks/ChatView-BzFbN_mp.js +0 -288
@@ -55,6 +55,18 @@ export type SandboxTargetSpec = {
55
55
  };
56
56
  /** The four, in the order the control shows them: nearest first. */
57
57
  export declare const SANDBOX_TARGETS: readonly SandboxTarget[];
58
+ /**
59
+ * Why a target is shown but not offered here — said on it — or nothing when
60
+ * it is offered.
61
+ *
62
+ * *Local* is a local agent with the Jupyter server it starts beside itself,
63
+ * and a web page has no machine of the reader's to start them on. Decided
64
+ * 2026-10-10 (Eric): not offered on the web for now, and left in the switch,
65
+ * held, so that a reader sees what exists and why it is not here. Inside
66
+ * JupyterLab it is offered as before — the same line the code-sandbox picker
67
+ * draws for a Jupyter Server's kernels (`codeSandboxProviders`).
68
+ */
69
+ export declare function heldTargetReason(target: SandboxTarget): string | undefined;
58
70
  export declare const TARGET_SPECS: Record<SandboxTarget, SandboxTargetSpec>;
59
71
  /** Whether an agent runs on this target — the question the chat asks. */
60
72
  export declare function targetHasAgent(target: SandboxTarget): boolean;
@@ -22,6 +22,7 @@
22
22
  * @module apps/plugins/agents/switchable
23
23
  */
24
24
  import { computed, signal, } from '@datalayer/reactor';
25
+ import { insideJupyterLab } from './host';
25
26
  import { browserSource, createBrowserSandboxService, } from './browserService';
26
27
  import { createServerSandboxService, } from './service';
27
28
  /**
@@ -40,6 +41,23 @@ export const SANDBOX_TARGETS = [
40
41
  'jupyter',
41
42
  'datalayer',
42
43
  ];
44
+ /**
45
+ * Why a target is shown but not offered here — said on it — or nothing when
46
+ * it is offered.
47
+ *
48
+ * *Local* is a local agent with the Jupyter server it starts beside itself,
49
+ * and a web page has no machine of the reader's to start them on. Decided
50
+ * 2026-10-10 (Eric): not offered on the web for now, and left in the switch,
51
+ * held, so that a reader sees what exists and why it is not here. Inside
52
+ * JupyterLab it is offered as before — the same line the code-sandbox picker
53
+ * draws for a Jupyter Server's kernels (`codeSandboxProviders`).
54
+ */
55
+ export function heldTargetReason(target) {
56
+ if (target === 'local' && !insideJupyterLab()) {
57
+ return 'Not offered on the web for now: a local agent and the Jupyter server it starts run beside JupyterLab, not in a page.';
58
+ }
59
+ return undefined;
60
+ }
43
61
  export const TARGET_SPECS = {
44
62
  browser: {
45
63
  label: 'Browser',
@@ -298,6 +316,11 @@ export function createSwitchableSandboxService({ serverUrl, initialTarget = 'loc
298
316
  if (next === previous) {
299
317
  return;
300
318
  }
319
+ // Held here: refused whoever asks, not only by the switch's button.
320
+ const held = heldTargetReason(next);
321
+ if (held) {
322
+ throw new Error(held);
323
+ }
301
324
  // Disconnect first: two live sandboxes would race to report status, and
302
325
  // the reader would watch the header flicker between them.
303
326
  disconnect?.();
@@ -126,11 +126,27 @@ export function AppComputer({ app, workspace, }) {
126
126
  ? noRuntimeSaid(workspace?.sandbox ?? IDLE_SANDBOX_SNAPSHOT, chatText)
127
127
  : undefined;
128
128
  const agentId = workspace?.agentId || app.id;
129
+ const [state, setState] = useState();
130
+ /*
131
+ * Where its files come from (STUDIO D-22).
132
+ *
133
+ * The runtime says it when the plane keeps a host for what applications
134
+ * serve (`servedFrom`, `DATALAYER_USER_APPS_URL`): a file is then fetched
135
+ * from that origin rather than from the runtimes' host, which is every
136
+ * runtime's and so same-origin with all of them. Read from the computer's
137
+ * own answer rather than configured here, because it is the runtime that
138
+ * knows which names it answers to; unsaid, nothing changes.
139
+ */
140
+ const servedFrom = state?.servedFrom ?? '';
129
141
  const context = useMemo(() => serverUrl === undefined
130
142
  ? undefined
131
- : { serverUrl, agentId, token: token || undefined }, [serverUrl, agentId, token]);
143
+ : {
144
+ serverUrl,
145
+ agentId,
146
+ token: token || undefined,
147
+ userAppsUrl: servedFrom || undefined,
148
+ }, [serverUrl, agentId, token, servedFrom]);
132
149
  const parts = computerParts(app);
133
- const [state, setState] = useState();
134
150
  const [error, setError] = useState('');
135
151
  const [busy, setBusy] = useState(false);
136
152
  // Nothing is set once it is gone: an answer may come back after.
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The agent's toolbox, contributed (LOOP C-20): one plugin per source of
3
+ * parts — the servers of the catalogue a connection reaches, its skills, its
4
+ * models, and the rules, tests and events an agent answers to — each
5
+ * contributing to `loop.canvas.part`. The toolbox is what the enabled
6
+ * plugins contribute, as the palette is (`canvas-blocks`, C-12).
7
+ *
8
+ * @module apps/plugins/canvas-parts
9
+ */
10
+ import { type ReactorPlatform, type ReactorPlugin } from '@datalayer/reactor';
11
+ import { LoopCanvasPart, type CanvasPartContribution } from '../../core/canvasParts';
12
+ type PartsReader = {
13
+ getContributions: (point: typeof LoopCanvasPart) => ReadonlyArray<{
14
+ value: CanvasPartContribution;
15
+ }>;
16
+ };
17
+ /** The short ids an organization turns off, and the plugin each names. */
18
+ export declare const CANVAS_PART_SOURCES: readonly ["connections", "skills", "models", "rules", "tests", "events"];
19
+ export type CanvasPartSource = (typeof CANVAS_PART_SOURCES)[number];
20
+ export declare const canvasPartsPluginName: (source: CanvasPartSource) => string;
21
+ /** A connection to each server of the catalogue: read-only, as its owner, every tool. */
22
+ export declare const CONNECTION_PARTS: CanvasPartContribution[];
23
+ /** Each skill of the catalogue. */
24
+ export declare const SKILL_PARTS: CanvasPartContribution[];
25
+ /** Each chat model of the catalogue. */
26
+ export declare const MODEL_PARTS: CanvasPartContribution[];
27
+ /** The rules a person most often sets, each one of the four behaviours. */
28
+ export declare const RULE_PARTS: CanvasPartContribution[];
29
+ /** A test: a question, and what a good answer does. */
30
+ export declare const TEST_PARTS: CanvasPartContribution[];
31
+ /** The events an application answers (C-21): on a schedule, on an event, once. */
32
+ export declare const EVENT_PARTS: CanvasPartContribution[];
33
+ export declare const CANVAS_PART_PLUGINS: ReactorPlugin<Record<string, never>, unknown, unknown>[];
34
+ /** The toolbox's reactor, its named plugins disabled; a name no plugin has is refused. */
35
+ export declare function canvasPartsReactor(disabled?: readonly string[], plugins?: ReactorPlugin<any, any, any>[]): ReactorPlatform;
36
+ /**
37
+ * The toolbox for an organization's plugins off: the ids it turns off that
38
+ * name a source of parts (`skills`, …) take those parts off; the rest are the
39
+ * palette's.
40
+ */
41
+ export declare function canvasPartsReactorOf(pluginsOff: readonly string[]): ReactorPlatform;
42
+ /** What the toolbox offers: the parts the enabled plugins contribute, in their order. */
43
+ export declare function canvasPartsOf(reactor: PartsReader): CanvasPartContribution[];
44
+ export {};
@@ -0,0 +1,209 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * The agent's toolbox, contributed (LOOP C-20): one plugin per source of
7
+ * parts — the servers of the catalogue a connection reaches, its skills, its
8
+ * models, and the rules, tests and events an agent answers to — each
9
+ * contributing to `loop.canvas.part`. The toolbox is what the enabled
10
+ * plugins contribute, as the palette is (`canvas-blocks`, C-12).
11
+ *
12
+ * @module apps/plugins/canvas-parts
13
+ */
14
+ import { buildReactorFromPlugins, contribution, definePlugin, } from '@datalayer/reactor';
15
+ import { MCP_SERVER_LIBRARY } from '../../../specs/mcpServers';
16
+ import { listChatModels } from '../../../specs/models';
17
+ import { getSkillSpecs } from '../../../specs/skills';
18
+ import { LoopCanvasPart, } from '../../core/canvasParts';
19
+ /** The short ids an organization turns off, and the plugin each names. */
20
+ export const CANVAS_PART_SOURCES = [
21
+ 'connections',
22
+ 'skills',
23
+ 'models',
24
+ 'rules',
25
+ 'tests',
26
+ 'events',
27
+ ];
28
+ export const canvasPartsPluginName = (source) => `@datalayer/loop-plugin-parts-${source}`;
29
+ const partsPlugin = (source, displayName, parts) => definePlugin({
30
+ name: canvasPartsPluginName(source),
31
+ displayName,
32
+ description: `${displayName} in the agent's toolbox on the Canvas.`,
33
+ octicon: 'package',
34
+ contributes: parts.map(part => contribution(LoopCanvasPart, part, { id: part.id })),
35
+ });
36
+ /** A connection to each server of the catalogue: read-only, as its owner, every tool. */
37
+ export const CONNECTION_PARTS = Object.values(MCP_SERVER_LIBRARY).map(server => ({
38
+ id: `connection:${server.id}`,
39
+ kind: 'connection',
40
+ label: server.name,
41
+ says: server.description ?? '',
42
+ emoji: server.emoji,
43
+ example: { server: server.id, access: 'read', as: 'owner', only: [] },
44
+ enabled: server.enabled,
45
+ }));
46
+ /** Each skill of the catalogue. */
47
+ export const SKILL_PARTS = getSkillSpecs().map(skill => ({
48
+ id: `skill:${skill.id}`,
49
+ kind: 'skill',
50
+ label: skill.name,
51
+ says: skill.description,
52
+ emoji: skill.emoji,
53
+ example: skill.id,
54
+ enabled: skill.enabled,
55
+ }));
56
+ /** Each chat model of the catalogue. */
57
+ export const MODEL_PARTS = listChatModels().map(model => ({
58
+ id: `model:${model.id}`,
59
+ kind: 'model',
60
+ label: model.name,
61
+ says: model.description,
62
+ example: model.id,
63
+ // A model's `available` says whether its key is set where the catalogue
64
+ // was read, not whether it may be chosen: every chat model may.
65
+ enabled: true,
66
+ }));
67
+ /** The rules a person most often sets, each one of the four behaviours. */
68
+ export const RULE_PARTS = [
69
+ {
70
+ id: 'rule:ask-before-sending',
71
+ kind: 'rule',
72
+ label: 'Ask before sending',
73
+ says: 'It asks you before it sends anything on your behalf.',
74
+ example: {
75
+ action: 'Send on my behalf',
76
+ applies_to: ['send'],
77
+ behaviour: 'ask_first',
78
+ },
79
+ enabled: true,
80
+ },
81
+ {
82
+ id: 'rule:read-freely',
83
+ kind: 'rule',
84
+ label: 'Read freely',
85
+ says: 'It reads what its connections reach without asking.',
86
+ example: { action: 'Read', applies_to: ['read'], behaviour: 'do_it' },
87
+ enabled: true,
88
+ },
89
+ {
90
+ id: 'rule:never-delete',
91
+ kind: 'rule',
92
+ label: 'Leave deleting to me',
93
+ says: 'It never deletes: it tells you what it would.',
94
+ example: {
95
+ action: 'Delete',
96
+ applies_to: ['delete'],
97
+ behaviour: 'leave_to_me',
98
+ },
99
+ enabled: true,
100
+ },
101
+ {
102
+ id: 'rule:buy-if-asked',
103
+ kind: 'rule',
104
+ label: 'Buy only when asked',
105
+ says: 'It buys only when you ask it to, in so many words.',
106
+ example: { action: 'Buy', applies_to: ['buy'], behaviour: 'if_asked' },
107
+ enabled: true,
108
+ },
109
+ ];
110
+ /** A test: a question, and what a good answer does. */
111
+ export const TEST_PARTS = [
112
+ {
113
+ id: 'test:answers-from-sources',
114
+ kind: 'test',
115
+ label: 'Answers from its sources',
116
+ says: 'A question, and what a good answer does.',
117
+ example: {
118
+ ask: 'What is due this week?',
119
+ expect: 'It lists what is due this week, read from its sources, and says where.',
120
+ },
121
+ enabled: true,
122
+ },
123
+ {
124
+ id: 'test:says-when-it-cannot',
125
+ kind: 'test',
126
+ label: 'Says when it cannot',
127
+ says: 'A question it cannot answer, and it says so.',
128
+ example: {
129
+ ask: 'What will the price be next year?',
130
+ expect: 'It says it cannot know, and does not guess.',
131
+ },
132
+ enabled: true,
133
+ },
134
+ ];
135
+ /** The events an application answers (C-21): on a schedule, on an event, once. */
136
+ export const EVENT_PARTS = [
137
+ {
138
+ id: 'event:schedule',
139
+ kind: 'event',
140
+ label: 'On a schedule',
141
+ says: 'Every weekday at 9:00, or any time a schedule says.',
142
+ example: {
143
+ type: 'schedule',
144
+ cron: '0 9 * * 1-5',
145
+ description: 'Every weekday at 9:00',
146
+ prompt: 'Summarise what came in since yesterday.',
147
+ },
148
+ enabled: true,
149
+ },
150
+ {
151
+ id: 'event:email-received',
152
+ kind: 'event',
153
+ label: 'When an email arrives',
154
+ says: 'A message arriving in the inbox it reads.',
155
+ example: {
156
+ type: 'event',
157
+ event: 'email_received',
158
+ prompt: 'Read the email, and draft a reply for me to approve.',
159
+ },
160
+ enabled: true,
161
+ },
162
+ {
163
+ id: 'event:once',
164
+ kind: 'event',
165
+ label: 'Once, at a moment',
166
+ says: 'One run, at a date and a time.',
167
+ example: {
168
+ type: 'once',
169
+ at: '2026-12-31T09:00:00',
170
+ prompt: 'Close the year: list what is still open.',
171
+ },
172
+ enabled: true,
173
+ },
174
+ ];
175
+ export const CANVAS_PART_PLUGINS = [
176
+ partsPlugin('connections', 'Connections', CONNECTION_PARTS),
177
+ partsPlugin('skills', 'Skills', SKILL_PARTS),
178
+ partsPlugin('models', 'Models', MODEL_PARTS),
179
+ partsPlugin('rules', 'Rules', RULE_PARTS),
180
+ partsPlugin('tests', 'Tests', TEST_PARTS),
181
+ partsPlugin('events', 'Events', EVENT_PARTS),
182
+ ];
183
+ /** The toolbox's reactor, its named plugins disabled; a name no plugin has is refused. */
184
+ export function canvasPartsReactor(disabled = [], plugins = CANVAS_PART_PLUGINS) {
185
+ const reactor = buildReactorFromPlugins(plugins);
186
+ reactor.start();
187
+ for (const name of disabled) {
188
+ if (!reactor.hasPlugin(name)) {
189
+ throw new Error(`No plugin of the toolbox is named "${name}"; they are ${plugins.map(plugin => plugin.name).join(', ') || 'none'}.`);
190
+ }
191
+ reactor.disable(name);
192
+ }
193
+ return reactor;
194
+ }
195
+ /**
196
+ * The toolbox for an organization's plugins off: the ids it turns off that
197
+ * name a source of parts (`skills`, …) take those parts off; the rest are the
198
+ * palette's.
199
+ */
200
+ export function canvasPartsReactorOf(pluginsOff) {
201
+ const sources = new Set(CANVAS_PART_SOURCES);
202
+ return canvasPartsReactor(pluginsOff
203
+ .filter(id => sources.has(id))
204
+ .map(id => canvasPartsPluginName(id)));
205
+ }
206
+ /** What the toolbox offers: the parts the enabled plugins contribute, in their order. */
207
+ export function canvasPartsOf(reactor) {
208
+ return reactor.getContributions(LoopCanvasPart).map(entry => entry.value);
209
+ }
@@ -14,7 +14,7 @@
14
14
  * assessment and Change detection), one `ask_<peer>` tool per peer, with
15
15
  * {@link a2aPeerTool}. A team of two names its peer as `peerApp`; a team of
16
16
  * N names them as `peers` (LOOP A-08). An entry that runs on a runtime
17
- * itself (a scene of one member: Month-end close, Crop monitoring) is asked
17
+ * itself (a scene of one member: Month-end Close, Crop Monitoring) is asked
18
18
  * over A2A from the page, with no agent in the browser: `entryPeer`.
19
19
  *
20
20
  * What each of them does is kept as a persona — the state its character
@@ -4,7 +4,7 @@ import { Heading, Text, TextInput, Label } from '@primer/react';
4
4
  import { SearchIcon } from '@primer/octicons-react';
5
5
  const HomeExample = ({ examples = [], searchQuery = '', onSearchChange, onSelectExample, }) => {
6
6
  const sortedExamples = [...examples].sort((a, b) => a.title.localeCompare(b.title));
7
- return (_jsx(Box, { width: "100%", height: "100%", overflow: "auto", bg: "canvas.default", children: _jsxs(Box, { maxWidth: "1600px", margin: "0 auto", p: 4, display: "flex", flexDirection: "column", gap: 3, children: [_jsxs(Box, { display: "flex", justifyContent: "space-between", alignItems: "center", gap: 3, flexWrap: "wrap", children: [_jsxs(Box, { children: [_jsx(Heading, { as: "h2", sx: { fontSize: 4, mb: 1 }, children: "Agent Runtimes Examples" }), _jsx(Text, { sx: { color: 'fg.muted', fontSize: 1 }, children: "Browse all examples as cards and open any flow in one click." })] }), _jsx(TextInput, { autoFocus: true, value: searchQuery, onChange: event => onSearchChange?.(event.target.value), placeholder: "Search examples", leadingVisual: SearchIcon, sx: { minWidth: ['100%', '320px'] } })] }), sortedExamples.length === 0 ? (_jsx(Box, { border: "1px dashed", borderColor: "border.default", borderRadius: 3, p: 4, textAlign: "center", color: "fg.muted", children: "No examples match your search." })) : (_jsx(Box, { display: "grid", gridTemplateColumns: [
7
+ return (_jsx(Box, { width: "100%", height: "100%", overflow: "auto", bg: "canvas.default", children: _jsxs(Box, { maxWidth: "1600px", margin: "0 auto", p: 4, display: "flex", flexDirection: "column", gap: 3, children: [_jsxs(Box, { display: "flex", justifyContent: "space-between", alignItems: "center", gap: 3, flexWrap: "wrap", children: [_jsxs(Box, { children: [_jsx(Heading, { as: "h2", sx: { fontSize: 4, mb: 1 }, children: "Agent Runtimes Examples" }), _jsx(Text, { sx: { color: 'fg.muted', fontSize: 1 }, children: "Browse all examples as cards and open any flow in one click." })] }), _jsx(TextInput, { autoFocus: true, value: searchQuery, onChange: event => onSearchChange?.(event.target.value), placeholder: "Search examples", leadingVisual: SearchIcon, sx: { minWidth: ['100%', '320px'] } })] }), sortedExamples.length === 0 ? (_jsx(Box, { border: "1px dashed", borderColor: "border.default", borderRadius: "card", p: 4, textAlign: "center", color: "fg.muted", children: "No examples match your search." })) : (_jsx(Box, { display: "grid", gridTemplateColumns: [
8
8
  '1fr',
9
9
  'repeat(2, minmax(0, 1fr))',
10
10
  'repeat(4, minmax(0, 1fr))',
@@ -12,7 +12,7 @@
12
12
  * are served over A2A by `examples/sales-accounting-a2a/serve_scenes.py`,
13
13
  * Accounting on :8767 as before and each other member on a runtime of its
14
14
  * own from :8768 (`sceneRuntimePorts`), or wherever `VITE_A2A_<MEMBER>_URL`
15
- * says. A scene whose entry runs on a runtime itself (Month-end close, Crop
15
+ * says. A scene whose entry runs on a runtime itself (Month-end Close, Crop
16
16
  * monitoring) is asked over A2A from the page, as the person
17
17
  * (`useA2ATeam`'s `entryPeer`). A member that is not reached is drawn all
18
18
  * the same, and the box says what its application needs (`setup`) and how
@@ -112,7 +112,7 @@ export const EXAMPLE_ENTRIES = [
112
112
  // for one sandbox — and a second one that would not agree with the first.
113
113
  ['example', 'loop', 'workspace', 'sandbox', 'owns-sandbox-control']),
114
114
  makeEntry('LoopAppComputerExample', () => import('./LoopAppComputerExample'), 'An application beside its computer: what its agent ran on its sandbox, its files, and Take over and Hand back (LOOP R-23).', ['example', 'loop', 'app', 'computer', 'sandbox', 'owns-sandbox-control']),
115
- makeEntry('ScenesExample', () => import('./ScenesExample'), 'The scenes of the catalogue in tabs — Sales & Accounting, Month-end close, Crop monitoring, Disaster assessment — each its graph and transcript, its cues as suggestions, played against the local servers (LOOP A-15).', ['example', 'loop', 'scenes', 'a2a', 'agentspecs', 'team']),
115
+ makeEntry('ScenesExample', () => import('./ScenesExample'), 'The scenes of the catalogue in tabs — Sales & Accounting, Month-end Close, Crop Monitoring, Disaster Assessment — each its graph and transcript, its cues as suggestions, played against the local servers (LOOP A-15).', ['example', 'loop', 'scenes', 'a2a', 'agentspecs', 'team']),
116
116
  makeEntry('LoopShellExample', () => import('./LoopShellExample'), 'The Loop shell at its most naked: a blank canvas, a floating draggable prompt, and an editor selector in the corner — none, notebook or document.',
117
117
  // `owns-sandbox-control`: the shell is pinned to the browser sandbox, so
118
118
  // the page's selector would offer three targets it cannot move to.
@@ -3,6 +3,7 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
  import { useCallback, useEffect, useState } from 'react';
6
+ import { agUiRefusal, refusalBody } from '../protocols/agUiRefusal';
6
7
  /**
7
8
  * Hook to manage chat state with AG-UI protocol.
8
9
  *
@@ -94,7 +95,9 @@ export function useAgUi(options = {}) {
94
95
  body: JSON.stringify(requestBody),
95
96
  });
96
97
  if (!response.ok) {
97
- throw new Error(`AG-UI request failed: ${response.status} ${response.statusText}`);
98
+ // Why, in the page's own words (F-15): a 404 at an agent's AG-UI
99
+ // address means the agent is not on this runtime.
100
+ throw new Error(agUiRefusal(response.status, response.statusText, await refusalBody(response)));
98
101
  }
99
102
  // Handle streaming response
100
103
  const reader = response.body?.getReader();
@@ -3,6 +3,7 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
  import { generateMessageId, createAssistantMessage } from '../types/messages';
6
+ import { agUiRefusal, refusalBody } from './agUiRefusal';
6
7
  import { BaseProtocolAdapter } from './BaseProtocolAdapter';
7
8
  /**
8
9
  * AG-UI protocol adapter
@@ -214,7 +215,10 @@ export class AGUIAdapter extends BaseProtocolAdapter {
214
215
  signal: controller.signal,
215
216
  });
216
217
  if (!response.ok) {
217
- throw new Error(`AG-UI request failed: ${response.status} ${response.statusText}`);
218
+ // Why, in the page's own words (F-15): a 404 at an agent's AG-UI
219
+ // address means the agent is not on this runtime, which the status
220
+ // alone never said.
221
+ throw new Error(agUiRefusal(response.status, response.statusText, await refusalBody(response)));
218
222
  }
219
223
  // Parse SSE stream
220
224
  await this.parseSSEStream(response);
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Why an AG-UI run was not answered, in the page's own words (LOOP F-15).
3
+ *
4
+ * What a person met before was `AG-UI request failed: 404` — the status and
5
+ * nothing else. A 404 at an agent's AG-UI address has one meaning: the agent
6
+ * is not on the runtime the page is addressing. The commonest way to get
7
+ * there is to open an application whose agent runs its code in the browser:
8
+ * the runtime refuses to start it (`browser_sandbox_refusal`, 422), the page
9
+ * addresses it anyway, and the address answers nothing. So the page says
10
+ * that, and says what to do about it.
11
+ *
12
+ * When the server said why — a refusal of the platform's own, carried as
13
+ * `detail` — that sentence is the better one and is used instead: it is
14
+ * about this agent, where ours is about any.
15
+ *
16
+ * Pure, so the hook, the adapter and the tests read a refusal the same way
17
+ * (the repo's jest/vitest rule: rules live outside components).
18
+ *
19
+ * @module protocols/agUiRefusal
20
+ */
21
+ /** What the page says when an agent's AG-UI address answers nothing. */
22
+ export declare const AG_UI_NOT_ON_RUNTIME: string;
23
+ /** What the page says when the address refused the caller. */
24
+ export declare const AG_UI_NOT_ALLOWED: string;
25
+ /**
26
+ * The `detail` a platform refusal carries, or `''`.
27
+ *
28
+ * A body that is not JSON — an HTML 404 from the page's own origin, which is
29
+ * what a page addressing a runtime it never got answers — carries none.
30
+ */
31
+ export declare function refusalDetail(body: string | undefined): string;
32
+ /**
33
+ * Why the run was not answered, as the page says it.
34
+ *
35
+ * @param status - The status the address answered with.
36
+ * @param statusText - Its text, for a status we have nothing better for.
37
+ * @param body - What came back, when it was read; a refusal's `detail` wins.
38
+ */
39
+ export declare function agUiRefusal(status: number, statusText: string, body?: string): string;
40
+ /**
41
+ * Read a refusal's body without letting the reading itself throw: a body
42
+ * already consumed, or a connection that went away, leaves us with the
43
+ * status alone, which is still better than nothing.
44
+ */
45
+ export declare function refusalBody(response: {
46
+ text: () => Promise<string>;
47
+ }): Promise<string | undefined>;
@@ -0,0 +1,87 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * Why an AG-UI run was not answered, in the page's own words (LOOP F-15).
7
+ *
8
+ * What a person met before was `AG-UI request failed: 404` — the status and
9
+ * nothing else. A 404 at an agent's AG-UI address has one meaning: the agent
10
+ * is not on the runtime the page is addressing. The commonest way to get
11
+ * there is to open an application whose agent runs its code in the browser:
12
+ * the runtime refuses to start it (`browser_sandbox_refusal`, 422), the page
13
+ * addresses it anyway, and the address answers nothing. So the page says
14
+ * that, and says what to do about it.
15
+ *
16
+ * When the server said why — a refusal of the platform's own, carried as
17
+ * `detail` — that sentence is the better one and is used instead: it is
18
+ * about this agent, where ours is about any.
19
+ *
20
+ * Pure, so the hook, the adapter and the tests read a refusal the same way
21
+ * (the repo's jest/vitest rule: rules live outside components).
22
+ *
23
+ * @module protocols/agUiRefusal
24
+ */
25
+ /** What the page says when an agent's AG-UI address answers nothing. */
26
+ export const AG_UI_NOT_ON_RUNTIME = 'This agent is not on the runtime the page is addressing: its AG-UI address answered nothing. ' +
27
+ 'An agent that runs its code in the browser is never started on a runtime — run it in the page, ' +
28
+ 'or give it a sandbox a runtime has (eval, jupyter-server).';
29
+ /** What the page says when the address refused the caller. */
30
+ export const AG_UI_NOT_ALLOWED = 'This agent did not answer the page: the run was refused. ' +
31
+ 'Sign in again, or open the application from its own address.';
32
+ /**
33
+ * The `detail` a platform refusal carries, or `''`.
34
+ *
35
+ * A body that is not JSON — an HTML 404 from the page's own origin, which is
36
+ * what a page addressing a runtime it never got answers — carries none.
37
+ */
38
+ export function refusalDetail(body) {
39
+ if (!body) {
40
+ return '';
41
+ }
42
+ let said;
43
+ try {
44
+ said = JSON.parse(body);
45
+ }
46
+ catch {
47
+ return '';
48
+ }
49
+ if (!said || typeof said !== 'object') {
50
+ return '';
51
+ }
52
+ const detail = said.detail;
53
+ return typeof detail === 'string' ? detail.trim() : '';
54
+ }
55
+ /**
56
+ * Why the run was not answered, as the page says it.
57
+ *
58
+ * @param status - The status the address answered with.
59
+ * @param statusText - Its text, for a status we have nothing better for.
60
+ * @param body - What came back, when it was read; a refusal's `detail` wins.
61
+ */
62
+ export function agUiRefusal(status, statusText, body) {
63
+ const detail = refusalDetail(body);
64
+ if (detail) {
65
+ return detail;
66
+ }
67
+ if (status === 404) {
68
+ return AG_UI_NOT_ON_RUNTIME;
69
+ }
70
+ if (status === 401 || status === 403) {
71
+ return AG_UI_NOT_ALLOWED;
72
+ }
73
+ return `AG-UI request failed: ${status} ${statusText}`.trim();
74
+ }
75
+ /**
76
+ * Read a refusal's body without letting the reading itself throw: a body
77
+ * already consumed, or a connection that went away, leaves us with the
78
+ * status alone, which is still better than nothing.
79
+ */
80
+ export async function refusalBody(response) {
81
+ try {
82
+ return await response.text();
83
+ }
84
+ catch {
85
+ return undefined;
86
+ }
87
+ }