@datalayer/agent-runtimes 1.3.37 → 1.3.39
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/lib/chat/base/ChatBase.js +7 -3
- package/lib/components/a2ui/visibleWhen.d.ts +7 -0
- package/lib/components/a2ui/visibleWhen.js +13 -0
- package/lib/loop/apps/AppRenderer.d.ts +79 -2
- package/lib/loop/apps/AppRenderer.js +87 -36
- package/lib/loop/core/index.d.ts +24 -1
- package/lib/loop/embed/AppEmbed.js +4 -2
- package/lib/loop/embed/LoopEmbed.js +1 -0
- package/lib/loop/index.d.ts +1 -1
- package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +6 -3
- package/lib/loop/plugins/a2ui-surface/InlineSurface.js +5 -2
- package/lib/loop/plugins/app-page/AppPage.d.ts +15 -1
- package/lib/loop/plugins/app-page/AppPage.js +63 -6
- package/lib/loop/plugins/app-page/appPageModel.d.ts +59 -6
- package/lib/loop/plugins/app-page/appPageModel.js +137 -8
- package/lib/loop/plugins/app-page/index.d.ts +36 -0
- package/lib/loop/plugins/app-page/index.js +32 -1
- package/lib/loop/plugins/chat/ChatView.js +47 -28
- package/lib/loop/plugins/chat/index.js +6 -1
- package/lib/loop/plugins/chat/turnState.d.ts +20 -1
- package/lib/loop/plugins/chat/turnState.js +56 -0
- package/lib/loop/presets.d.ts +11 -0
- package/lib/loop/presets.js +16 -1
- package/lib/protocols/AGUIAdapter.d.ts +2 -0
- package/lib/protocols/AGUIAdapter.js +1 -1
- package/lib/types/chat.d.ts +2 -1
- package/lib/types/protocol.d.ts +2 -0
- package/package.json +1 -1
|
@@ -2812,7 +2812,10 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2812
2812
|
// ========================================================================
|
|
2813
2813
|
// handleSend
|
|
2814
2814
|
// ========================================================================
|
|
2815
|
-
const handleSend = useCallback(async (messageOverride
|
|
2815
|
+
const handleSend = useCallback(async (messageOverride,
|
|
2816
|
+
// AG-UI's: what goes with the run besides the conversation — an
|
|
2817
|
+
// application's page says there what it did (LOOP R-04).
|
|
2818
|
+
forwardedProps) => {
|
|
2816
2819
|
const messageContent = (messageOverride ?? input).trim();
|
|
2817
2820
|
if (!messageContent || isLoading)
|
|
2818
2821
|
return;
|
|
@@ -2915,6 +2918,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2915
2918
|
builtinTools: enabledMcpToolNames,
|
|
2916
2919
|
skills: enabledSkillIds,
|
|
2917
2920
|
identities: connectedIdentitiesRef.current,
|
|
2921
|
+
...(forwardedProps ? { forwardedProps } : {}),
|
|
2918
2922
|
});
|
|
2919
2923
|
}
|
|
2920
2924
|
}
|
|
@@ -3099,8 +3103,8 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3099
3103
|
return undefined;
|
|
3100
3104
|
}
|
|
3101
3105
|
onSendReady({
|
|
3102
|
-
send: (message) => {
|
|
3103
|
-
void handleSend(message);
|
|
3106
|
+
send: (message, forwardedProps) => {
|
|
3107
|
+
void handleSend(message, forwardedProps);
|
|
3104
3108
|
},
|
|
3105
3109
|
stop: handleStop,
|
|
3106
3110
|
// The same reset the header's + performs, for a host whose controls
|
|
@@ -17,3 +17,10 @@ export declare function withVisibleWhen(component: ReactComponentImplementation)
|
|
|
17
17
|
* it — with `visible_when` on every component.
|
|
18
18
|
*/
|
|
19
19
|
export declare const datalayerCatalog: Catalog<ReactComponentImplementation, import("@a2ui/web_core/v0_9").FunctionImplementation>;
|
|
20
|
+
/**
|
|
21
|
+
* Datalayer's catalog narrowed to the components named — what the enabled
|
|
22
|
+
* plugins contribute as blocks (LOOP R-01b, `loop.canvas.block`), so that an
|
|
23
|
+
* application's page draws what its Canvas can place and nothing else. Under
|
|
24
|
+
* the same id; a name the catalog does not draw is an error.
|
|
25
|
+
*/
|
|
26
|
+
export declare function catalogOfBlocks(names: readonly string[]): Catalog<ReactComponentImplementation>;
|
|
@@ -55,3 +55,16 @@ export function withVisibleWhen(component) {
|
|
|
55
55
|
* it — with `visible_when` on every component.
|
|
56
56
|
*/
|
|
57
57
|
export const datalayerCatalog = new Catalog(basicCatalog.id, basicCatalog.protocolVersion, [...basicCatalog.components.values(), ...OWN_COMPONENTS].map(withVisibleWhen), [...basicCatalog.functions.values()], basicCatalog.themeSchema, basicCatalog.instructions);
|
|
58
|
+
/**
|
|
59
|
+
* Datalayer's catalog narrowed to the components named — what the enabled
|
|
60
|
+
* plugins contribute as blocks (LOOP R-01b, `loop.canvas.block`), so that an
|
|
61
|
+
* application's page draws what its Canvas can place and nothing else. Under
|
|
62
|
+
* the same id; a name the catalog does not draw is an error.
|
|
63
|
+
*/
|
|
64
|
+
export function catalogOfBlocks(names) {
|
|
65
|
+
const unknown = names.filter(name => !datalayerCatalog.components.has(name));
|
|
66
|
+
if (unknown.length > 0) {
|
|
67
|
+
throw new Error(`No renderer draws ${unknown.join(', ')}: a block is contributed by a plugin whose component the catalog draws.`);
|
|
68
|
+
}
|
|
69
|
+
return new Catalog(datalayerCatalog.id, datalayerCatalog.protocolVersion, names.map(name => datalayerCatalog.components.get(name)), [...datalayerCatalog.functions.values()], datalayerCatalog.themeSchema, datalayerCatalog.instructions);
|
|
70
|
+
}
|
|
@@ -1,6 +1,45 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* An application, rendered: the LOOP workspace configured from an Appspec
|
|
3
|
+
* (LOOP R-01).
|
|
4
|
+
*
|
|
5
|
+
* One renderer for every place an application appears — the Studio's
|
|
6
|
+
* Preview, its hosted page, an embed, the home page — so that what a builder
|
|
7
|
+
* sees while building is what its users get. It is not a second shell: it is
|
|
8
|
+
* `LoopEmbed`, given
|
|
9
|
+
*
|
|
10
|
+
* - an **application plugin** (`defineAppPlugin`): the agent that answers,
|
|
11
|
+
* created with the application in its payload — so the runtime reaches only
|
|
12
|
+
* what the application connects to and enforces its rules before every tool
|
|
13
|
+
* call — and the application's starters as the openers of the empty chat;
|
|
14
|
+
* - its **layout**, as `interface.layout` says (`appLayoutOptions`, LOOP
|
|
15
|
+
* T-07), on the workspace's own `page-layout` plugin: `chat` is the
|
|
16
|
+
* conversation alone, with the A2UI surfaces its answers draw; `page` is
|
|
17
|
+
* the page on a sheet with the composer over it and the conversation in a
|
|
18
|
+
* panel the composer's display modes open; `split` is the conversation and
|
|
19
|
+
* the page side by side, a hairline between them to drag;
|
|
20
|
+
* - its **page**, when it has one (`hasAppPage`: a chat, a widget or a worker
|
|
21
|
+
* with a `page` or `split` layout): its A2UI surface drawn by the
|
|
22
|
+
* `app-page` plugin, in place of the notebook and the document, fed from
|
|
23
|
+
* the conversation and answered through it (`APP_KIND_PATHS`), with the
|
|
24
|
+
* Canvas's block plugins (`CANVAS_BLOCK_PLUGINS`), whose blocks are what
|
|
25
|
+
* the page draws (R-01b);
|
|
26
|
+
* - a **decision**: a workspace without a conversation whose one view is the
|
|
27
|
+
* page its host draws (`page`, R-02) — the Studio's run page, at its
|
|
28
|
+
* address and embedded;
|
|
29
|
+
* - its **frame**, when the host asks (`frame`): the `window-frame` plugin's
|
|
30
|
+
* window, its title the application's face and name.
|
|
31
|
+
*
|
|
32
|
+
* Which plugins a kind needs, and how its workspace is laid out, is one
|
|
33
|
+
* function: `appPreset`, the preset per kind, as `loopPlugins` is for the
|
|
34
|
+
* examples.
|
|
35
|
+
*
|
|
36
|
+
* @module loop/apps/AppRenderer
|
|
37
|
+
*/
|
|
38
|
+
import { type ComponentType } from 'react';
|
|
39
|
+
import type { PluginRef, ReactorPlugin } from '@datalayer/reactor';
|
|
2
40
|
import type { AppSpec } from '../../types/agentspecs';
|
|
3
41
|
import type { ThemeOverrides } from '../../types/chat';
|
|
42
|
+
import { type AppHostPageProps } from '../plugins/app-page';
|
|
4
43
|
import { type LoopEmbedProps } from '../embed/LoopEmbed';
|
|
5
44
|
import type { LoopPresetOptions } from '../presets';
|
|
6
45
|
import type { PresenceState } from '../../chat/presence/presenceStatus';
|
|
@@ -44,6 +83,19 @@ export type AppRendererProps = Omit<LoopEmbedProps, 'agentId'> & {
|
|
|
44
83
|
* the application's face and name. A host's own `frameTitle` wins.
|
|
45
84
|
*/
|
|
46
85
|
frame?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* The page a host draws for a kind the workspace has no page for: a
|
|
88
|
+
* decision's (R-02). The workspace's one view. A component defined once,
|
|
89
|
+
* outside the host's render: a new one would be a new plugin, and restart
|
|
90
|
+
* the workspace.
|
|
91
|
+
*/
|
|
92
|
+
page?: ComponentType<AppHostPageProps>;
|
|
93
|
+
/**
|
|
94
|
+
* The UI plugins its organization has turned off (`plugins_off`, catalogue
|
|
95
|
+
* ids such as 'a2ui'): their blocks are off its page as they are off its
|
|
96
|
+
* Canvas's palette. Compared by content.
|
|
97
|
+
*/
|
|
98
|
+
pluginsOff?: readonly string[];
|
|
47
99
|
};
|
|
48
100
|
/** What `interface.layout` sets on the workspace. */
|
|
49
101
|
export type AppLayoutOptions = Pick<LoopPresetOptions, 'editors' | 'showViewSelector' | 'defaultEditor' | 'pageLayout' | 'pageLayoutArrangement' | 'pageLayoutPrompt' | 'pageLayoutPromptAnchor' | 'pageLayoutTurnPanelFooter'>;
|
|
@@ -62,6 +114,31 @@ export type AppLayoutOptions = Pick<LoopPresetOptions, 'editors' | 'showViewSele
|
|
|
62
114
|
* it; without one (a decision, whose page is the Studio's), the editors stay.
|
|
63
115
|
*/
|
|
64
116
|
export declare function appLayoutOptions(app: Pick<AppSpec, 'kind' | 'interface' | 'agent'>): AppLayoutOptions;
|
|
117
|
+
/** What a kind of application is drawn with: its plugins, and its workspace. */
|
|
118
|
+
export type AppPreset = {
|
|
119
|
+
plugins: PluginRef[];
|
|
120
|
+
workspace: AppLayoutOptions & Pick<LoopPresetOptions, 'conversation'>;
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* The plugins an application's kind needs, and how its workspace is laid
|
|
124
|
+
* out (LOOP R-01): the preset per kind, as `loopPlugins` is for the examples.
|
|
125
|
+
*
|
|
126
|
+
* - a **chat**, a **widget**, a **worker**: its agent (`defineAppPlugin`);
|
|
127
|
+
* its page (`app-page`) when it has one, with the Canvas's block plugins
|
|
128
|
+
* but those of the UI plugins its organization turned off (`pluginsOff`),
|
|
129
|
+
* whose contributions are the blocks it may draw (R-01b); a thumb and a
|
|
130
|
+
* comment on each answer when its record keeps feedback (V-18); laid out
|
|
131
|
+
* as `interface.layout` says;
|
|
132
|
+
* - a **decision**: the page its host draws (`page`), as the one view of a
|
|
133
|
+
* workspace without a conversation (R-02).
|
|
134
|
+
*
|
|
135
|
+
* An application that cannot be drawn is refused with a sentence: a
|
|
136
|
+
* decision without its host's page, an application run by a team.
|
|
137
|
+
*/
|
|
138
|
+
export declare function appPreset(app: AppSpec, options?: {
|
|
139
|
+
page?: ComponentType<AppHostPageProps>;
|
|
140
|
+
pluginsOff?: readonly string[];
|
|
141
|
+
}): AppPreset;
|
|
65
142
|
/**
|
|
66
143
|
* What an application's agent is created with on a Datalayer runtime: under
|
|
67
144
|
* the application's id, over AG-UI, with the application's own document —
|
|
@@ -87,5 +164,5 @@ export declare const DATALAYER_BOOTSTRAP_AGENTSPEC = "example-simple";
|
|
|
87
164
|
* what the page around it set does not reach in.
|
|
88
165
|
*/
|
|
89
166
|
export declare function appThemeOverrides(app: AppSpec): ThemeOverrides | undefined;
|
|
90
|
-
export declare function AppRenderer({ app, plugins, instance, onPresence, frame, ...embed }: AppRendererProps): React.JSX.Element;
|
|
167
|
+
export declare function AppRenderer({ app, plugins, instance, onPresence, frame, page, pluginsOff, ...embed }: AppRendererProps): React.JSX.Element;
|
|
91
168
|
export default AppRenderer;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
/*
|
|
3
3
|
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
4
4
|
* Distributed under the terms of the Modified BSD License.
|
|
@@ -25,18 +25,26 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
25
25
|
* - its **page**, when it has one (`hasAppPage`: a chat, a widget or a worker
|
|
26
26
|
* with a `page` or `split` layout): its A2UI surface drawn by the
|
|
27
27
|
* `app-page` plugin, in place of the notebook and the document, fed from
|
|
28
|
-
* the conversation and answered through it (`APP_KIND_PATHS`)
|
|
29
|
-
*
|
|
30
|
-
*
|
|
28
|
+
* the conversation and answered through it (`APP_KIND_PATHS`), with the
|
|
29
|
+
* Canvas's block plugins (`CANVAS_BLOCK_PLUGINS`), whose blocks are what
|
|
30
|
+
* the page draws (R-01b);
|
|
31
|
+
* - a **decision**: a workspace without a conversation whose one view is the
|
|
32
|
+
* page its host draws (`page`, R-02) — the Studio's run page, at its
|
|
33
|
+
* address and embedded;
|
|
31
34
|
* - its **frame**, when the host asks (`frame`): the `window-frame` plugin's
|
|
32
35
|
* window, its title the application's face and name.
|
|
33
36
|
*
|
|
37
|
+
* Which plugins a kind needs, and how its workspace is laid out, is one
|
|
38
|
+
* function: `appPreset`, the preset per kind, as `loopPlugins` is for the
|
|
39
|
+
* examples.
|
|
40
|
+
*
|
|
34
41
|
* @module loop/apps/AppRenderer
|
|
35
42
|
*/
|
|
36
43
|
import { useMemo } from 'react';
|
|
37
44
|
import { loopAccentStyles } from '@datalayer/primer-addons';
|
|
38
45
|
import { defineAgentCapacityPlugin } from '../plugins/agent-capacity';
|
|
39
|
-
import { APP_PAGE_SURFACE, defineAppPagePlugin, hasAppPage, } from '../plugins/app-page';
|
|
46
|
+
import { APP_PAGE_SURFACE, defineAppHostPagePlugin, defineAppPagePlugin, hasAppPage, } from '../plugins/app-page';
|
|
47
|
+
import { CANVAS_BLOCK_PLUGINS, canvasBlocksPluginName, } from '../plugins/canvas-blocks';
|
|
40
48
|
import { LoopEmbed } from '../embed/LoopEmbed';
|
|
41
49
|
import { dumpAppspec } from './appspec';
|
|
42
50
|
import { defineAppFeedbackPlugin } from './AppFeedback';
|
|
@@ -118,6 +126,51 @@ export function appLayoutOptions(app) {
|
|
|
118
126
|
pageLayoutTurnPanelFooter: 'actions',
|
|
119
127
|
};
|
|
120
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* The plugins an application's kind needs, and how its workspace is laid
|
|
131
|
+
* out (LOOP R-01): the preset per kind, as `loopPlugins` is for the examples.
|
|
132
|
+
*
|
|
133
|
+
* - a **chat**, a **widget**, a **worker**: its agent (`defineAppPlugin`);
|
|
134
|
+
* its page (`app-page`) when it has one, with the Canvas's block plugins
|
|
135
|
+
* but those of the UI plugins its organization turned off (`pluginsOff`),
|
|
136
|
+
* whose contributions are the blocks it may draw (R-01b); a thumb and a
|
|
137
|
+
* comment on each answer when its record keeps feedback (V-18); laid out
|
|
138
|
+
* as `interface.layout` says;
|
|
139
|
+
* - a **decision**: the page its host draws (`page`), as the one view of a
|
|
140
|
+
* workspace without a conversation (R-02).
|
|
141
|
+
*
|
|
142
|
+
* An application that cannot be drawn is refused with a sentence: a
|
|
143
|
+
* decision without its host's page, an application run by a team.
|
|
144
|
+
*/
|
|
145
|
+
export function appPreset(app, options = {}) {
|
|
146
|
+
if (app.kind === 'decision') {
|
|
147
|
+
if (!options.page) {
|
|
148
|
+
throw new Error(`“${app.name}” is a decision, whose page is drawn by where it runs: none was given here.`);
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
plugins: [defineAppHostPagePlugin(app, options.page)],
|
|
152
|
+
workspace: {
|
|
153
|
+
conversation: false,
|
|
154
|
+
editors: false,
|
|
155
|
+
showViewSelector: false,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const withPage = hasAppPage(app);
|
|
160
|
+
// The blocks its Canvas offers: those of the UI plugins not turned off.
|
|
161
|
+
const off = new Set((options.pluginsOff ?? []).map(canvasBlocksPluginName));
|
|
162
|
+
const blocks = CANVAS_BLOCK_PLUGINS.filter(plugin => !off.has(plugin.name));
|
|
163
|
+
return {
|
|
164
|
+
plugins: [
|
|
165
|
+
defineAppPlugin(app),
|
|
166
|
+
...(withPage ? [defineAppPagePlugin(app), ...blocks] : []),
|
|
167
|
+
// A thumb and a comment on each answer, kept in its record (LOOP
|
|
168
|
+
// V-18): only for an application whose record keeps feedback.
|
|
169
|
+
...(keepsFeedback(app) ? [defineAppFeedbackPlugin(app)] : []),
|
|
170
|
+
],
|
|
171
|
+
workspace: appLayoutOptions(app),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
121
174
|
/**
|
|
122
175
|
* What an application's agent is created with on a Datalayer runtime: under
|
|
123
176
|
* the application's id, over AG-UI, with the application's own document —
|
|
@@ -174,52 +227,50 @@ export function appThemeOverrides(app) {
|
|
|
174
227
|
}
|
|
175
228
|
: undefined;
|
|
176
229
|
}
|
|
177
|
-
export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, frame = false, ...embed }) {
|
|
230
|
+
export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, frame = false, page, pluginsOff, ...embed }) {
|
|
178
231
|
/*
|
|
179
|
-
* The
|
|
180
|
-
*
|
|
181
|
-
*
|
|
232
|
+
* The kind's plugins, made once per application. `LoopEmbed` rebuilds its
|
|
233
|
+
* whole reactor when its plugins change, so a new plugin on every render
|
|
234
|
+
* of the host would restart the running workspace. The key is the
|
|
182
235
|
* application as its file holds it: a change to it is a new application.
|
|
183
236
|
*/
|
|
184
237
|
const source = JSON.stringify(dumpAppspec(app));
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const keeps = keepsFeedback(app);
|
|
197
|
-
const feedbackPlugin = useMemo(() => (keeps ? defineAppFeedbackPlugin(app) : null),
|
|
238
|
+
const off = [...(pluginsOff ?? [])].sort().join(',');
|
|
239
|
+
const preset = useMemo(() => {
|
|
240
|
+
try {
|
|
241
|
+
return appPreset(app, { page, pluginsOff });
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
return {
|
|
245
|
+
problem: error instanceof Error ? error.message : String(error),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
},
|
|
198
249
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
199
|
-
[source,
|
|
200
|
-
const allPlugins = useMemo(() =>
|
|
201
|
-
? [
|
|
202
|
-
appPlugin,
|
|
203
|
-
...(pagePlugin ? [pagePlugin] : []),
|
|
204
|
-
...(feedbackPlugin ? [feedbackPlugin] : []),
|
|
205
|
-
...plugins,
|
|
206
|
-
]
|
|
207
|
-
: plugins, [appPlugin, pagePlugin, feedbackPlugin, plugins]);
|
|
250
|
+
[source, page, off]);
|
|
251
|
+
const allPlugins = useMemo(() => ('problem' in preset ? plugins : [...preset.plugins, ...plugins]), [preset, plugins]);
|
|
208
252
|
/*
|
|
209
253
|
* On Datalayer, the application runs on a runtime: allocated with a plain
|
|
210
254
|
* agentspec, its agent created there with the application's spec — so that
|
|
211
255
|
* the runtime registers it and decides every tool call by its rules (LOOP
|
|
212
256
|
* R-03, R-05), as `loop apps run --cloud` does.
|
|
213
257
|
*/
|
|
214
|
-
const
|
|
258
|
+
const conversation = !('problem' in preset) && preset.workspace.conversation !== false;
|
|
259
|
+
const datalayerCreatePayload = useMemo(() => (conversation ? appDatalayerCreatePayload(app, instance) : undefined),
|
|
215
260
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
216
|
-
[
|
|
261
|
+
[
|
|
262
|
+
source,
|
|
263
|
+
conversation,
|
|
264
|
+
instance?.appUid,
|
|
265
|
+
instance?.deploymentUid,
|
|
266
|
+
instance?.version,
|
|
267
|
+
]);
|
|
217
268
|
const accent = app.interface?.accent;
|
|
218
269
|
const themeOverrides = useMemo(() => appThemeOverrides(app),
|
|
219
270
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
220
271
|
[accent]);
|
|
221
|
-
if (
|
|
222
|
-
return (
|
|
272
|
+
if ('problem' in preset) {
|
|
273
|
+
return (_jsx("div", { role: "status", style: { padding: 16 }, children: preset.problem }));
|
|
223
274
|
}
|
|
224
275
|
return (_jsx(LoopEmbed
|
|
225
276
|
// The application's own agent: created under its id, so that its
|
|
@@ -227,7 +278,7 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, f
|
|
|
227
278
|
, {
|
|
228
279
|
// The application's own agent: created under its id, so that its
|
|
229
280
|
// spec is applied to an agent of its own, never to the one it extends.
|
|
230
|
-
agentId: app.id, ...
|
|
281
|
+
agentId: app.id, ...preset.workspace, ...(frame
|
|
231
282
|
? {
|
|
232
283
|
frameTitle: [app.emoji, app.name].filter(Boolean).join(' '),
|
|
233
284
|
}
|
package/lib/loop/core/index.d.ts
CHANGED
|
@@ -227,8 +227,12 @@ export type ViewControls = {
|
|
|
227
227
|
* rendered surface, whose values the agent should answer. The chat had
|
|
228
228
|
* this control from the start and kept it to itself; a host with a
|
|
229
229
|
* message had no way in.
|
|
230
|
+
*
|
|
231
|
+
* `forwardedProps` go with the run as AG-UI's: an application's page says
|
|
232
|
+
* there what it did besides the message (`loop`, LOOP R-04). Answers why
|
|
233
|
+
* the message cannot go, when it cannot.
|
|
230
234
|
*/
|
|
231
|
-
send?: (message: string) => void;
|
|
235
|
+
send?: (message: string, forwardedProps?: Record<string, unknown>) => string | void;
|
|
232
236
|
};
|
|
233
237
|
/** Props every view receives. */
|
|
234
238
|
export type LoopViewProps = {
|
|
@@ -955,10 +959,29 @@ export type ChatTurnSnapshot = {
|
|
|
955
959
|
*/
|
|
956
960
|
activity?: string;
|
|
957
961
|
};
|
|
962
|
+
/**
|
|
963
|
+
* One message of the conversation, as a page shows it (LOOP R-01, C-18): a
|
|
964
|
+
* person's or the assistant's words, or a tool the assistant called — what
|
|
965
|
+
* the Chat component draws, bound to a chat application's `/messages`.
|
|
966
|
+
*/
|
|
967
|
+
export type ConversationEntry = {
|
|
968
|
+
role: 'user' | 'assistant';
|
|
969
|
+
text: string;
|
|
970
|
+
} | {
|
|
971
|
+
role: 'tool';
|
|
972
|
+
name: string;
|
|
973
|
+
args: Record<string, unknown>;
|
|
974
|
+
result?: unknown;
|
|
975
|
+
};
|
|
958
976
|
export type ChatTurnContribution = {
|
|
959
977
|
/** Stable id, for the registry and the graph. */
|
|
960
978
|
id: string;
|
|
961
979
|
/** The turn, live. */
|
|
962
980
|
turn: ReadonlySignal<ChatTurnSnapshot>;
|
|
981
|
+
/**
|
|
982
|
+
* The whole conversation, live: every message and tool call in order, the
|
|
983
|
+
* reply as it streams included; empty again when the chat starts over.
|
|
984
|
+
*/
|
|
985
|
+
conversation: ReadonlySignal<ConversationEntry[]>;
|
|
963
986
|
};
|
|
964
987
|
export declare const LoopChatTurn: import("@datalayer/reactor").ContributionPoint<ChatTurnContribution>;
|
|
@@ -80,7 +80,8 @@ function useServerAgent(app, serverUrl) {
|
|
|
80
80
|
setState({
|
|
81
81
|
protocol: {
|
|
82
82
|
type: 'ag-ui',
|
|
83
|
-
|
|
83
|
+
// Its session API: each thread a session (LOOP R-04).
|
|
84
|
+
endpoint: `${base}/api/v1/apps/agents/${encodeURIComponent(app.id)}/ag-ui/`,
|
|
84
85
|
agentId: app.id,
|
|
85
86
|
},
|
|
86
87
|
});
|
|
@@ -137,7 +138,8 @@ function useDatalayerAgent(app, instance) {
|
|
|
137
138
|
return {
|
|
138
139
|
protocol: {
|
|
139
140
|
type: 'ag-ui',
|
|
140
|
-
|
|
141
|
+
// Its session API: each thread a session (LOOP R-04).
|
|
142
|
+
endpoint: `${runtime.agentBaseUrl}/api/v1/apps/agents/${encodeURIComponent(app.id)}/ag-ui/`,
|
|
141
143
|
agentId: app.id,
|
|
142
144
|
authToken: token,
|
|
143
145
|
},
|
package/lib/loop/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export { ChatPlugin, CHAT_PLUGIN_NAME, type ChatPluginConfig, } from './plugins/
|
|
|
15
15
|
export { ChatHeaderPlugin, CHAT_HEADER_PLUGIN_NAME, } from './plugins/chat-header';
|
|
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
|
-
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';
|
|
18
|
+
export { LoopChatExtras, LoopChatLayout, LoopChatTurn, LoopOpeningDocument, LoopOpeningNotebook, LoopPromptPanel, type ChatLayoutContribution, type ChatLayoutParts, type ChatTurnContribution, type ConversationEntry, type ChatTurnSnapshot, type ChatTurnStatus, type LoopChatExtrasValue, type OpeningDocumentContribution, type OpeningNotebookContribution, type PromptPanelContribution, } from './core';
|
|
19
19
|
export { LoopPageLayoutPlugin, LOOP_PAGE_LAYOUT_PLUGIN_NAME, openConversationPanel, pageLayoutSheet, pageLayoutPanelOpen, setPageLayoutChatMode, SplitLayout, type LoopPageLayoutConfig, } from './plugins/page-layout';
|
|
20
20
|
export { useOptionalTeamSelection } from './plugins/agents/useTeamSelection';
|
|
21
21
|
export type { TeamMember, TeamSelection } from './plugins/agents/team';
|
|
@@ -13,13 +13,14 @@
|
|
|
13
13
|
* an action is handed the surface it came from, so that what a block wrote
|
|
14
14
|
* into its data model can be read when a button is pressed. It draws from
|
|
15
15
|
* Datalayer's catalog: the basic one, any block shown only while its
|
|
16
|
-
* `visible_when` holds
|
|
16
|
+
* `visible_when` holds — or, given one, a catalog narrowed to the blocks
|
|
17
|
+
* the workspace's plugins contribute (`catalogOfBlocks`).
|
|
17
18
|
*
|
|
18
19
|
* @module loop/plugins/a2ui-surface/InlineSurface
|
|
19
20
|
*/
|
|
20
21
|
import type { JSX } from 'react';
|
|
21
22
|
import type { ReactComponentImplementation } from '@a2ui/react/v0_9';
|
|
22
|
-
import { type A2uiClientAction, type A2uiMessage, type SurfaceModel } from '@a2ui/web_core/v0_9';
|
|
23
|
+
import { Catalog, type A2uiClientAction, type A2uiMessage, type SurfaceModel } from '@a2ui/web_core/v0_9';
|
|
23
24
|
/** A surface as the renderer holds it: its data model is `dataModel`. */
|
|
24
25
|
export type InlineSurfaceModel = SurfaceModel<ReactComponentImplementation>;
|
|
25
26
|
type Surface = InlineSurfaceModel;
|
|
@@ -27,8 +28,10 @@ type Surface = InlineSurfaceModel;
|
|
|
27
28
|
export declare const SURFACE_CATALOG_ID: string;
|
|
28
29
|
/** Values to publish into a surface, by path. */
|
|
29
30
|
export type InlineSurfaceData = Record<string, unknown>;
|
|
30
|
-
export declare function InlineSurface({ messages, onAction, validationError, data, }: {
|
|
31
|
+
export declare function InlineSurface({ messages, onAction, validationError, data, catalog, }: {
|
|
31
32
|
messages: A2uiMessage[];
|
|
33
|
+
/** What it draws with, read once: Datalayer's catalog unless given. */
|
|
34
|
+
catalog?: Catalog<ReactComponentImplementation>;
|
|
32
35
|
/** Told of an action, with the surface it came from. */
|
|
33
36
|
onAction: (action: A2uiClientAction, surface?: Surface) => void;
|
|
34
37
|
validationError?: string | null;
|
|
@@ -11,12 +11,15 @@ const INHERIT_THEME = {
|
|
|
11
11
|
['--a2ui-color-primary']: 'var(--fgColor-accent)',
|
|
12
12
|
['--a2ui-color-outline']: 'var(--borderColor-default)',
|
|
13
13
|
};
|
|
14
|
-
export function InlineSurface({ messages, onAction, validationError, data, }) {
|
|
14
|
+
export function InlineSurface({ messages, onAction, validationError, data, catalog = datalayerCatalog, }) {
|
|
15
15
|
// Reached through a ref: the processor is built once, and the handler it
|
|
16
16
|
// was built with must not go stale when the host's does not.
|
|
17
17
|
const onActionRef = useRef(onAction);
|
|
18
18
|
onActionRef.current = onAction;
|
|
19
|
-
const processor = useMemo(() => new MessageProcessor([
|
|
19
|
+
const processor = useMemo(() => new MessageProcessor([catalog], action => onActionRef.current(action, processorRef.current?.model.getSurface(action.surfaceId))),
|
|
20
|
+
// Once: a host that changes what it draws with mounts a new surface.
|
|
21
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
22
|
+
[]);
|
|
20
23
|
const processorRef = useRef(undefined);
|
|
21
24
|
processorRef.current = processor;
|
|
22
25
|
const [surfaces, setSurfaces] = useState([]);
|
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* An application's page, drawn beside its conversation: its A2UI surface on
|
|
3
3
|
* the workspace's own renderer (`InlineSurface`), fed by the chat's current
|
|
4
|
-
* turn, its buttons
|
|
4
|
+
* turn and its conversation, its buttons — and a Chat block's message, a File
|
|
5
|
+
* upload's files — answered through the chat's controls, what the page did
|
|
6
|
+
* going with the chat's run to the application's session (LOOP R-04).
|
|
7
|
+
*
|
|
8
|
+
* It draws the blocks the workspace's plugins contribute to the Canvas's
|
|
9
|
+
* palette (`loop.canvas.block`, R-01b) and nothing else, so that what the
|
|
10
|
+
* Canvas can place and what the page can draw are one list: a page that
|
|
11
|
+
* uses a block no enabled plugin contributes says so, in place of the page.
|
|
5
12
|
*
|
|
6
13
|
* @module loop/plugins/app-page/AppPage
|
|
7
14
|
*/
|
|
8
15
|
import type { JSX } from 'react';
|
|
9
16
|
import type { AppSpec } from '../../../types/agentspecs';
|
|
10
17
|
import { type LoopWorkspaceContext } from '../../core';
|
|
18
|
+
/** The components a page's messages place, each once. */
|
|
19
|
+
export declare function componentsOnPage(messages: readonly unknown[]): string[];
|
|
20
|
+
/**
|
|
21
|
+
* Why a page is not drawn with the blocks contributed, or null: the blocks it
|
|
22
|
+
* places that no enabled plugin contributes.
|
|
23
|
+
*/
|
|
24
|
+
export declare function blocksMissing(placed: readonly string[], contributed: readonly string[]): string | null;
|
|
11
25
|
export type AppPageProps = {
|
|
12
26
|
app: AppSpec;
|
|
13
27
|
workspace: LoopWorkspaceContext;
|
|
@@ -3,16 +3,61 @@ import { useCallback, useMemo, useRef, useState } from 'react';
|
|
|
3
3
|
import { Box } from '@datalayer/primer-addons';
|
|
4
4
|
import { signal } from '@datalayer/reactor';
|
|
5
5
|
import { useContributions, useSignalValue } from '@datalayer/reactor/react';
|
|
6
|
-
import {
|
|
6
|
+
import { catalogOfBlocks } from '../../../components/a2ui';
|
|
7
|
+
import { LoopCanvasBlock, LoopChatTurn, } from '../../core';
|
|
7
8
|
import { InlineSurface, SURFACE_CATALOG_ID, } from '../a2ui-surface/InlineSurface';
|
|
8
9
|
import { appPageAction, appPageData, appPageMessages } from './appPageModel';
|
|
9
10
|
/** No chat in the workspace: the page reads a turn that never starts. */
|
|
10
11
|
const NO_TURN = signal({ id: 0, status: 'idle' });
|
|
12
|
+
const NO_CONVERSATION = signal([]);
|
|
13
|
+
/** The components a page's messages place, each once. */
|
|
14
|
+
export function componentsOnPage(messages) {
|
|
15
|
+
const names = new Set();
|
|
16
|
+
for (const message of messages) {
|
|
17
|
+
const update = message.updateComponents;
|
|
18
|
+
for (const node of update?.components ?? []) {
|
|
19
|
+
names.add(String(node.component));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return [...names];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Why a page is not drawn with the blocks contributed, or null: the blocks it
|
|
26
|
+
* places that no enabled plugin contributes.
|
|
27
|
+
*/
|
|
28
|
+
export function blocksMissing(placed, contributed) {
|
|
29
|
+
const have = new Set(contributed);
|
|
30
|
+
const missing = placed.filter(name => !have.has(name));
|
|
31
|
+
if (missing.length === 0) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return `This page places ${missing.join(', ')}, which no enabled plugin contributes: ${missing.length === 1 ? 'it is' : 'they are'} drawn once the plugin that contributes ${missing.length === 1 ? 'it' : 'them'} is on.`;
|
|
35
|
+
}
|
|
11
36
|
export function AppPage({ app, workspace }) {
|
|
12
37
|
const entries = useContributions(LoopChatTurn);
|
|
13
38
|
const turn = useSignalValue(entries[0]?.value.turn ?? NO_TURN);
|
|
39
|
+
const conversation = useSignalValue(entries[0]?.value.conversation ?? NO_CONVERSATION);
|
|
14
40
|
const messages = useMemo(() => appPageMessages(app, SURFACE_CATALOG_ID), [app]);
|
|
15
|
-
|
|
41
|
+
// The blocks the enabled plugins contribute: what this page may draw.
|
|
42
|
+
const blocks = useContributions(LoopCanvasBlock);
|
|
43
|
+
const contributed = [...new Set(blocks.map(entry => entry.value.id))].sort();
|
|
44
|
+
const drawnWith = contributed.join(',');
|
|
45
|
+
const drawing = useMemo(() => {
|
|
46
|
+
const missing = blocksMissing(componentsOnPage(messages), contributed);
|
|
47
|
+
if (missing) {
|
|
48
|
+
return { problem: missing };
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
return { catalog: catalogOfBlocks(contributed) };
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
return {
|
|
55
|
+
problem: error instanceof Error ? error.message : String(error),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
59
|
+
}, [messages, drawnWith]);
|
|
60
|
+
const data = useMemo(() => appPageData(app, turn, conversation), [app, turn, conversation]);
|
|
16
61
|
const [refusal, setRefusal] = useState(null);
|
|
17
62
|
// The workspace changes as the chat reports itself; the handler reads the latest.
|
|
18
63
|
const workspaceRef = useRef(workspace);
|
|
@@ -34,10 +79,20 @@ export function AppPage({ app, workspace }) {
|
|
|
34
79
|
controls.newChat?.();
|
|
35
80
|
}
|
|
36
81
|
else {
|
|
37
|
-
// The chat's own send,
|
|
38
|
-
//
|
|
82
|
+
// The chat's own send, with what the page did besides — the block's
|
|
83
|
+
// action, its files, the settings — for the application's session
|
|
84
|
+
// (R-04); the prompt channel when the chat has not reported itself
|
|
85
|
+
// yet, which carries words only.
|
|
39
86
|
if (controls.send) {
|
|
40
|
-
controls.send(outcome.send);
|
|
87
|
+
const refused = controls.send(outcome.send, { loop: outcome.loop });
|
|
88
|
+
if (refused) {
|
|
89
|
+
setRefusal(refused);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else if (outcome.loop.files) {
|
|
94
|
+
setRefusal('The conversation is not ready for a file yet: try again.');
|
|
95
|
+
return;
|
|
41
96
|
}
|
|
42
97
|
else {
|
|
43
98
|
workspaceRef.current.prompts.submit(outcome.send);
|
|
@@ -48,6 +103,8 @@ export function AppPage({ app, workspace }) {
|
|
|
48
103
|
}
|
|
49
104
|
}
|
|
50
105
|
}, [app]);
|
|
51
|
-
return (_jsx(Box, { "data-testid": "app-page", sx: { height: '100%', minHeight: 0, overflow: 'auto', p: 3 }, children: _jsx(
|
|
106
|
+
return (_jsx(Box, { "data-testid": "app-page", sx: { height: '100%', minHeight: 0, overflow: 'auto', p: 3 }, children: 'problem' in drawing ? (_jsx(Box, { role: "status", sx: { color: 'fg.muted', fontSize: 1 }, children: drawing.problem })) : (_jsx(InlineSurface
|
|
107
|
+
// A new list of blocks is a new surface, drawn with it.
|
|
108
|
+
, { messages: messages, data: data, onAction: onAction, validationError: refusal, catalog: drawing.catalog }, drawnWith)) }));
|
|
52
109
|
}
|
|
53
110
|
export default AppPage;
|