@datalayer/agent-runtimes 1.3.29 → 1.3.30
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/AgentNode.js +14 -33
- package/lib/chat/base/ChatBase.js +41 -32
- package/lib/chat/base/modelChoice.d.ts +13 -0
- package/lib/chat/base/modelChoice.js +13 -0
- package/lib/loop/apps/AppRenderer.d.ts +33 -1
- package/lib/loop/apps/AppRenderer.js +96 -29
- package/lib/loop/apps/agent.d.ts +15 -0
- package/lib/loop/apps/agent.js +42 -0
- package/lib/loop/apps/index.d.ts +1 -0
- package/lib/loop/apps/index.js +1 -0
- package/lib/loop/core/index.d.ts +8 -0
- package/lib/loop/embed/AppEmbed.d.ts +42 -0
- package/lib/loop/embed/AppEmbed.js +205 -0
- package/lib/loop/embed/datalayer-app.d.ts +1 -0
- package/lib/loop/embed/datalayer-app.js +16 -0
- package/lib/loop/embed/element.d.ts +57 -0
- package/lib/loop/embed/element.js +405 -0
- package/lib/loop/embed/embedConfig.d.ts +120 -0
- package/lib/loop/embed/embedConfig.js +148 -0
- package/lib/loop/embed/embedTheme.d.ts +24 -0
- package/lib/loop/embed/embedTheme.js +83 -0
- package/lib/loop/index.d.ts +6 -2
- package/lib/loop/index.js +8 -2
- package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +21 -3
- package/lib/loop/plugins/a2ui-surface/InlineSurface.js +34 -6
- package/lib/loop/plugins/agent-capacity/index.d.ts +4 -0
- package/lib/loop/plugins/agent-capacity/index.js +3 -1
- package/lib/loop/plugins/agents/switchable.d.ts +7 -0
- package/lib/loop/plugins/agents/switchable.js +91 -88
- package/lib/loop/plugins/app-page/AppPage.d.ts +16 -0
- package/lib/loop/plugins/app-page/AppPage.js +53 -0
- package/lib/loop/plugins/app-page/appPage.d.ts +180 -0
- package/lib/loop/plugins/app-page/appPage.js +443 -0
- package/lib/loop/plugins/app-page/index.d.ts +8 -0
- package/lib/loop/plugins/app-page/index.js +34 -0
- package/lib/loop/plugins/chat/ChatView.js +47 -25
- package/lib/loop/plugins/page-layout/SplitLayout.d.ts +31 -0
- package/lib/loop/plugins/page-layout/SplitLayout.js +129 -0
- package/lib/loop/plugins/page-layout/index.d.ts +13 -0
- package/lib/loop/plugins/page-layout/index.js +10 -1
- package/lib/loop/presets.d.ts +12 -0
- package/lib/loop/presets.js +5 -3
- package/lib/specs/actions.js +8 -0
- package/lib/specs/agents/agents.js +4 -0
- package/lib/specs/apps.d.ts +10 -1
- package/lib/specs/apps.js +1792 -178
- package/lib/specs/appspecSchema.js +1 -1
- package/lib/types/agentspecs.d.ts +11 -0
- package/lib/types/config.d.ts +12 -0
- package/package.json +2 -1
- package/scripts/codegen/generate_agents.py +50 -2
- package/scripts/codegen/generate_apps.py +36 -6
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/** The id of the editor surface an application's page is drawn in. */
|
|
6
|
+
export const APP_PAGE_SURFACE = 'app-page';
|
|
7
|
+
/** The id of the A2UI surface itself, inside it. */
|
|
8
|
+
export const APP_PAGE_SURFACE_ID = 'app';
|
|
9
|
+
const APP = {
|
|
10
|
+
path: '/app',
|
|
11
|
+
meaning: "The application's name.",
|
|
12
|
+
words: 'Its name',
|
|
13
|
+
};
|
|
14
|
+
const STATUS = {
|
|
15
|
+
path: '/status',
|
|
16
|
+
meaning: 'Where its answer stands, in a word: Ready, Thinking, Answering, Answered or Stopped.',
|
|
17
|
+
words: 'Where its answer stands',
|
|
18
|
+
};
|
|
19
|
+
const DRAFT = {
|
|
20
|
+
path: '/draft',
|
|
21
|
+
meaning: 'A message written on the page, which a Send button sends.',
|
|
22
|
+
words: 'A message to send',
|
|
23
|
+
};
|
|
24
|
+
const SEND = {
|
|
25
|
+
name: 'send',
|
|
26
|
+
meaning: 'Send a message to the application: the context’s message, or what is written at /draft.',
|
|
27
|
+
words: 'Send the message',
|
|
28
|
+
context: ['message'],
|
|
29
|
+
asks: { message: 'The message it sends' },
|
|
30
|
+
};
|
|
31
|
+
const STOP = {
|
|
32
|
+
name: 'stop',
|
|
33
|
+
meaning: 'Stop the answer being written.',
|
|
34
|
+
words: 'Stop the answer',
|
|
35
|
+
context: [],
|
|
36
|
+
asks: {},
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* What each kind publishes to its page and takes from it, by path — the
|
|
40
|
+
* least that is real today, read from the conversation it runs as.
|
|
41
|
+
*/
|
|
42
|
+
export const APP_KIND_PATHS = {
|
|
43
|
+
chat: {
|
|
44
|
+
publishes: [
|
|
45
|
+
APP,
|
|
46
|
+
{
|
|
47
|
+
path: '/question',
|
|
48
|
+
meaning: 'The question last asked.',
|
|
49
|
+
words: 'The question last asked',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
path: '/answer',
|
|
53
|
+
meaning: 'Its answer to it, as it is written.',
|
|
54
|
+
words: 'Its answer',
|
|
55
|
+
},
|
|
56
|
+
STATUS,
|
|
57
|
+
],
|
|
58
|
+
accepts: [DRAFT],
|
|
59
|
+
actions: [
|
|
60
|
+
SEND,
|
|
61
|
+
STOP,
|
|
62
|
+
{
|
|
63
|
+
name: 'new',
|
|
64
|
+
meaning: 'Start the conversation over.',
|
|
65
|
+
words: 'Start over',
|
|
66
|
+
context: [],
|
|
67
|
+
asks: {},
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
inputs: true,
|
|
71
|
+
},
|
|
72
|
+
widget: {
|
|
73
|
+
publishes: [
|
|
74
|
+
APP,
|
|
75
|
+
{
|
|
76
|
+
path: '/output',
|
|
77
|
+
meaning: 'What it gave for the inputs last run, as it is written.',
|
|
78
|
+
words: 'What it gave',
|
|
79
|
+
},
|
|
80
|
+
STATUS,
|
|
81
|
+
],
|
|
82
|
+
accepts: [],
|
|
83
|
+
actions: [
|
|
84
|
+
{
|
|
85
|
+
name: 'run',
|
|
86
|
+
meaning: 'Run it on its inputs: every value written under /inputs is sent, with the context’s message when there is one.',
|
|
87
|
+
words: 'Run it on its inputs',
|
|
88
|
+
context: ['message'],
|
|
89
|
+
asks: { message: 'What it says before the inputs' },
|
|
90
|
+
},
|
|
91
|
+
STOP,
|
|
92
|
+
],
|
|
93
|
+
inputs: true,
|
|
94
|
+
},
|
|
95
|
+
worker: {
|
|
96
|
+
publishes: [
|
|
97
|
+
APP,
|
|
98
|
+
{
|
|
99
|
+
path: '/goal',
|
|
100
|
+
meaning: 'What it works toward.',
|
|
101
|
+
words: 'What it works toward',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
path: '/activity',
|
|
105
|
+
meaning: 'What it is doing right now, in a short line, or nothing.',
|
|
106
|
+
words: 'What it is doing now',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
path: '/report',
|
|
110
|
+
meaning: 'What it last said, as it is written.',
|
|
111
|
+
words: 'What it last said',
|
|
112
|
+
},
|
|
113
|
+
STATUS,
|
|
114
|
+
],
|
|
115
|
+
accepts: [DRAFT],
|
|
116
|
+
actions: [SEND, STOP],
|
|
117
|
+
inputs: false,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
/** Whether a kind runs as its conversation and has its paths here. */
|
|
121
|
+
export const isAppPageKind = (kind) => Object.prototype.hasOwnProperty.call(APP_KIND_PATHS, kind);
|
|
122
|
+
/** The path a setting, an input of the page, is kept at. */
|
|
123
|
+
export const inputPath = (id) => `/inputs/${id}`;
|
|
124
|
+
/**
|
|
125
|
+
* What an application publishes and takes, with its own settings: the kind's
|
|
126
|
+
* list, and for a kind that has inputs each setting at `/inputs/<id>`, both
|
|
127
|
+
* shown and written. Empty lists for a decision, or a kind not known.
|
|
128
|
+
*/
|
|
129
|
+
export function appKindPaths(app) {
|
|
130
|
+
if (!isAppPageKind(app.kind)) {
|
|
131
|
+
return { publishes: [], accepts: [], actions: [], inputs: false };
|
|
132
|
+
}
|
|
133
|
+
const kind = APP_KIND_PATHS[app.kind];
|
|
134
|
+
if (!kind.inputs) {
|
|
135
|
+
return kind;
|
|
136
|
+
}
|
|
137
|
+
const inputs = app.interface.settings.map(setting => ({
|
|
138
|
+
path: inputPath(setting.id),
|
|
139
|
+
meaning: `The setting “${setting.label || setting.id}”.`,
|
|
140
|
+
words: setting.label || setting.id,
|
|
141
|
+
}));
|
|
142
|
+
return {
|
|
143
|
+
...kind,
|
|
144
|
+
publishes: [...kind.publishes, ...inputs],
|
|
145
|
+
accepts: [...kind.accepts, ...inputs],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/** The components its surface has, or none. */
|
|
149
|
+
const surfaceComponents = (app) => app.interface.surface?.components ?? [];
|
|
150
|
+
/**
|
|
151
|
+
* Whether the workspace draws the application's page: a chat, a widget or a
|
|
152
|
+
* worker whose layout has one (`page`, `split`). A `chat` layout is the
|
|
153
|
+
* conversation alone: a surface composed for it is not drawn, which
|
|
154
|
+
* `surfaceUnshown` says in a sentence.
|
|
155
|
+
*/
|
|
156
|
+
export function hasAppPage(app) {
|
|
157
|
+
if (!app.agent || !isAppPageKind(app.kind)) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
return app.interface.layout !== 'chat';
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Why a composed surface is not drawn, or `null` when it is (or there is
|
|
164
|
+
* none): the application's layout is `chat`, the conversation alone.
|
|
165
|
+
*/
|
|
166
|
+
export function surfaceUnshown(app) {
|
|
167
|
+
if (!app.agent ||
|
|
168
|
+
!isAppPageKind(app.kind) ||
|
|
169
|
+
app.interface.layout !== 'chat' ||
|
|
170
|
+
surfaceComponents(app).length === 0) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
return 'Its page is composed but its layout is chat, the conversation alone: choose page or split to show it.';
|
|
174
|
+
}
|
|
175
|
+
const STATUS_WORDS = {
|
|
176
|
+
idle: 'Ready',
|
|
177
|
+
thinking: 'Thinking',
|
|
178
|
+
streaming: 'Answering',
|
|
179
|
+
done: 'Answered',
|
|
180
|
+
error: 'Stopped',
|
|
181
|
+
};
|
|
182
|
+
/** A setting's first value, as its input holds it. */
|
|
183
|
+
export function initialInput(setting) {
|
|
184
|
+
switch (setting.type) {
|
|
185
|
+
case 'toggle':
|
|
186
|
+
return Boolean(setting.default ?? false);
|
|
187
|
+
case 'select':
|
|
188
|
+
// A ChoicePicker holds a list of the values chosen.
|
|
189
|
+
return setting.default === undefined ? [] : [String(setting.default)];
|
|
190
|
+
case 'slider':
|
|
191
|
+
return Number(setting.default ?? setting.min ?? 0);
|
|
192
|
+
case 'number':
|
|
193
|
+
return setting.max === undefined
|
|
194
|
+
? String(setting.default ?? '')
|
|
195
|
+
: Number(setting.default ?? setting.min ?? 0);
|
|
196
|
+
default:
|
|
197
|
+
return String(setting.default ?? '');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/** What the conversation publishes to the page, by path, now. */
|
|
201
|
+
export function appPageData(app, turn) {
|
|
202
|
+
const data = {
|
|
203
|
+
'/app': app.name,
|
|
204
|
+
'/status': STATUS_WORDS[turn.status] ?? '',
|
|
205
|
+
};
|
|
206
|
+
switch (app.kind) {
|
|
207
|
+
case 'chat':
|
|
208
|
+
data['/question'] = turn.user ?? '';
|
|
209
|
+
data['/answer'] = turn.assistant ?? '';
|
|
210
|
+
break;
|
|
211
|
+
case 'widget':
|
|
212
|
+
data['/output'] = turn.assistant ?? '';
|
|
213
|
+
break;
|
|
214
|
+
case 'worker':
|
|
215
|
+
data['/goal'] = app.goal;
|
|
216
|
+
data['/activity'] = turn.activity ?? '';
|
|
217
|
+
data['/report'] = turn.assistant ?? '';
|
|
218
|
+
break;
|
|
219
|
+
default:
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
return data;
|
|
223
|
+
}
|
|
224
|
+
/** The page's data model when it is drawn: what is published, the inputs, the draft. */
|
|
225
|
+
export function appPageInitialData(app) {
|
|
226
|
+
const tree = {};
|
|
227
|
+
for (const [path, value] of Object.entries(appPageData(app, { id: 0, status: 'idle' }))) {
|
|
228
|
+
tree[path.slice(1)] = value;
|
|
229
|
+
}
|
|
230
|
+
const paths = appKindPaths(app);
|
|
231
|
+
if (paths.inputs) {
|
|
232
|
+
tree.inputs = Object.fromEntries(app.interface.settings.map(setting => [
|
|
233
|
+
setting.id,
|
|
234
|
+
initialInput(setting),
|
|
235
|
+
]));
|
|
236
|
+
}
|
|
237
|
+
if (paths.accepts.some(entry => entry.path === DRAFT.path)) {
|
|
238
|
+
tree.draft = '';
|
|
239
|
+
}
|
|
240
|
+
return tree;
|
|
241
|
+
}
|
|
242
|
+
/** One input in words: `Plan: Team`; nothing for an empty one. */
|
|
243
|
+
function inputWords(label, value) {
|
|
244
|
+
const shown = Array.isArray(value)
|
|
245
|
+
? value.map(String).join(', ')
|
|
246
|
+
: typeof value === 'boolean'
|
|
247
|
+
? value
|
|
248
|
+
? 'yes'
|
|
249
|
+
: 'no'
|
|
250
|
+
: value === undefined || value === null
|
|
251
|
+
? ''
|
|
252
|
+
: String(value);
|
|
253
|
+
return shown.trim() ? `${label}: ${shown}` : '';
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* The inputs written on the page, in words, one per line: each setting by
|
|
257
|
+
* its label, then any other value a block wrote under `/inputs` by its id.
|
|
258
|
+
*/
|
|
259
|
+
export function inputsInWords(app, inputs) {
|
|
260
|
+
const values = inputs && typeof inputs === 'object'
|
|
261
|
+
? inputs
|
|
262
|
+
: {};
|
|
263
|
+
const lines = [];
|
|
264
|
+
const seen = new Set();
|
|
265
|
+
for (const setting of app.interface.settings) {
|
|
266
|
+
seen.add(setting.id);
|
|
267
|
+
lines.push(inputWords(setting.label || setting.id, values[setting.id]));
|
|
268
|
+
}
|
|
269
|
+
for (const [id, value] of Object.entries(values)) {
|
|
270
|
+
if (!seen.has(id)) {
|
|
271
|
+
lines.push(inputWords(id, value));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return lines.filter(Boolean).join('\n');
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* What a button pressed on the page does.
|
|
278
|
+
*
|
|
279
|
+
* `read` gives what is in the surface's data model at a path; `lastInputs`
|
|
280
|
+
* is the inputs in words as last sent, so that a chat's message carries its
|
|
281
|
+
* settings only when they changed.
|
|
282
|
+
*/
|
|
283
|
+
export function appPageAction(app, event, read, lastInputs = '') {
|
|
284
|
+
const paths = appKindPaths(app);
|
|
285
|
+
if (!paths.actions.some(action => action.name === event.name)) {
|
|
286
|
+
const offered = paths.actions.map(action => action.name).join(', ');
|
|
287
|
+
return {
|
|
288
|
+
refused: offered
|
|
289
|
+
? `“${event.name}” is not something ${app.name} does: it does ${offered}.`
|
|
290
|
+
: `${app.name} handles no button.`,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
const given = event.context?.message;
|
|
294
|
+
const message = typeof given === 'string' ? given.trim() : '';
|
|
295
|
+
const inputs = paths.inputs ? inputsInWords(app, read('/inputs')) : '';
|
|
296
|
+
switch (event.name) {
|
|
297
|
+
case 'stop':
|
|
298
|
+
return { stop: true };
|
|
299
|
+
case 'new':
|
|
300
|
+
return { newChat: true };
|
|
301
|
+
case 'run': {
|
|
302
|
+
const text = [message, inputs].filter(Boolean).join('\n');
|
|
303
|
+
return text
|
|
304
|
+
? { send: text, inputs, clear: [] }
|
|
305
|
+
: { refused: `${app.name} has no inputs to run on: set one first.` };
|
|
306
|
+
}
|
|
307
|
+
default: {
|
|
308
|
+
const draft = read(DRAFT.path);
|
|
309
|
+
const written = message || (typeof draft === 'string' ? draft.trim() : '');
|
|
310
|
+
if (!written) {
|
|
311
|
+
return { refused: 'Nothing to send: write a message first.' };
|
|
312
|
+
}
|
|
313
|
+
const changed = inputs && inputs !== lastInputs;
|
|
314
|
+
return {
|
|
315
|
+
send: changed ? `${written}\n\n${inputs}` : written,
|
|
316
|
+
inputs,
|
|
317
|
+
clear: message ? [] : [DRAFT.path],
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const text = (id, path, variant) => ({
|
|
323
|
+
id,
|
|
324
|
+
component: 'Text',
|
|
325
|
+
text: { path },
|
|
326
|
+
...(variant ? { variant } : {}),
|
|
327
|
+
});
|
|
328
|
+
const button = (id, label, name, primary = false) => [
|
|
329
|
+
{
|
|
330
|
+
id,
|
|
331
|
+
component: 'Button',
|
|
332
|
+
child: `${id}-label`,
|
|
333
|
+
...(primary ? { variant: 'primary' } : {}),
|
|
334
|
+
action: { event: { name } },
|
|
335
|
+
},
|
|
336
|
+
{ id: `${id}-label`, component: 'Text', text: label },
|
|
337
|
+
];
|
|
338
|
+
/** A setting as the input of the catalog that holds its value. */
|
|
339
|
+
function settingInput(setting) {
|
|
340
|
+
const base = {
|
|
341
|
+
id: `input-${setting.id}`,
|
|
342
|
+
label: setting.label || setting.id,
|
|
343
|
+
value: { path: inputPath(setting.id) },
|
|
344
|
+
};
|
|
345
|
+
switch (setting.type) {
|
|
346
|
+
case 'toggle':
|
|
347
|
+
return { ...base, component: 'CheckBox' };
|
|
348
|
+
case 'select':
|
|
349
|
+
return {
|
|
350
|
+
...base,
|
|
351
|
+
component: 'ChoicePicker',
|
|
352
|
+
options: setting.options.map(option => ({
|
|
353
|
+
label: option,
|
|
354
|
+
value: option,
|
|
355
|
+
})),
|
|
356
|
+
};
|
|
357
|
+
case 'slider':
|
|
358
|
+
return {
|
|
359
|
+
...base,
|
|
360
|
+
component: 'Slider',
|
|
361
|
+
min: setting.min ?? 0,
|
|
362
|
+
max: setting.max ?? 100,
|
|
363
|
+
};
|
|
364
|
+
case 'number':
|
|
365
|
+
return setting.max === undefined
|
|
366
|
+
? { ...base, component: 'TextField', variant: 'number' }
|
|
367
|
+
: {
|
|
368
|
+
...base,
|
|
369
|
+
component: 'Slider',
|
|
370
|
+
min: setting.min ?? 0,
|
|
371
|
+
max: setting.max,
|
|
372
|
+
};
|
|
373
|
+
default:
|
|
374
|
+
return { ...base, component: 'TextField' };
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* The page drawn when the application has a page layout and no surface
|
|
379
|
+
* composed: what its kind publishes, its settings as inputs, and the
|
|
380
|
+
* buttons its kind handles that the conversation does not already give.
|
|
381
|
+
*/
|
|
382
|
+
export function defaultAppSurface(app) {
|
|
383
|
+
const inputs = appKindPaths(app).inputs
|
|
384
|
+
? app.interface.settings.map(settingInput)
|
|
385
|
+
: [];
|
|
386
|
+
const body = [];
|
|
387
|
+
switch (app.kind) {
|
|
388
|
+
case 'chat':
|
|
389
|
+
body.push(text('question', '/question', 'h3'), text('answer', '/answer'), text('status', '/status', 'caption'), ...inputs);
|
|
390
|
+
break;
|
|
391
|
+
case 'widget':
|
|
392
|
+
body.push(...inputs, { id: 'actions', component: 'Row', children: ['run', 'stop'] }, ...button('run', 'Run', 'run', true), ...button('stop', 'Stop', 'stop'), text('status', '/status', 'caption'), text('output', '/output'));
|
|
393
|
+
break;
|
|
394
|
+
case 'worker':
|
|
395
|
+
body.push(text('goal', '/goal', 'h3'), text('status', '/status', 'caption'), text('activity', '/activity', 'caption'), text('report', '/report'));
|
|
396
|
+
break;
|
|
397
|
+
default:
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
// The root's children: what is not inside a Row or a Button.
|
|
401
|
+
const inner = new Set(body.flatMap(component => [
|
|
402
|
+
...(Array.isArray(component.children) ? component.children : []),
|
|
403
|
+
component.child,
|
|
404
|
+
].filter((id) => typeof id === 'string')));
|
|
405
|
+
return [
|
|
406
|
+
{
|
|
407
|
+
id: 'root',
|
|
408
|
+
component: 'Column',
|
|
409
|
+
children: body
|
|
410
|
+
.map(component => component.id)
|
|
411
|
+
.filter(id => !inner.has(id)),
|
|
412
|
+
},
|
|
413
|
+
...body,
|
|
414
|
+
];
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* The A2UI messages that draw the application's page: its surface composed,
|
|
418
|
+
* or the default page of its kind, fed its first data.
|
|
419
|
+
*/
|
|
420
|
+
export function appPageMessages(app, catalogId) {
|
|
421
|
+
const composed = surfaceComponents(app);
|
|
422
|
+
return [
|
|
423
|
+
{
|
|
424
|
+
version: 'v0.9',
|
|
425
|
+
createSurface: { surfaceId: APP_PAGE_SURFACE_ID, catalogId },
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
version: 'v0.9',
|
|
429
|
+
updateComponents: {
|
|
430
|
+
surfaceId: APP_PAGE_SURFACE_ID,
|
|
431
|
+
components: composed.length > 0 ? composed : defaultAppSurface(app),
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
version: 'v0.9',
|
|
436
|
+
updateDataModel: {
|
|
437
|
+
surfaceId: APP_PAGE_SURFACE_ID,
|
|
438
|
+
path: '/',
|
|
439
|
+
value: appPageInitialData(app),
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
];
|
|
443
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ReactorPlugin } from '@datalayer/reactor';
|
|
2
|
+
import type { AppSpec } from '../../../types/agentspecs';
|
|
3
|
+
export declare const APP_PAGE_PLUGIN_NAME = "@datalayer/loop-plugin-app-page";
|
|
4
|
+
/** The plugin that draws an application's page beside its conversation. */
|
|
5
|
+
export declare function defineAppPagePlugin(app: AppSpec): ReactorPlugin<Record<string, never>, unknown, unknown>;
|
|
6
|
+
export { AppPage, type AppPageProps } from './AppPage';
|
|
7
|
+
export * from './appPage';
|
|
8
|
+
export default defineAppPagePlugin;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { BrowserIcon } from '@primer/octicons-react';
|
|
3
|
+
import { contribution, definePlugin, } from '@datalayer/reactor';
|
|
4
|
+
import { LoopEditorView } from '../../core';
|
|
5
|
+
import { AppPage } from './AppPage';
|
|
6
|
+
import { APP_PAGE_SURFACE } from './appPage';
|
|
7
|
+
export const APP_PAGE_PLUGIN_NAME = '@datalayer/loop-plugin-app-page';
|
|
8
|
+
/** The plugin that draws an application's page beside its conversation. */
|
|
9
|
+
export function defineAppPagePlugin(app) {
|
|
10
|
+
// One component per plugin: the editor keeps it, and the application with it.
|
|
11
|
+
function Page({ workspace }) {
|
|
12
|
+
return _jsx(AppPage, { app: app, workspace: workspace });
|
|
13
|
+
}
|
|
14
|
+
const plugin = definePlugin({
|
|
15
|
+
name: `${APP_PAGE_PLUGIN_NAME}-${app.id}`,
|
|
16
|
+
displayName: `${app.name}: its page`,
|
|
17
|
+
description: `The page of ${app.name}, drawn beside its conversation.`,
|
|
18
|
+
octicon: 'browser',
|
|
19
|
+
emoji: app.emoji || undefined,
|
|
20
|
+
contributes: [
|
|
21
|
+
contribution(LoopEditorView, {
|
|
22
|
+
surfaceId: APP_PAGE_SURFACE,
|
|
23
|
+
title: app.name,
|
|
24
|
+
icon: BrowserIcon,
|
|
25
|
+
order: -10,
|
|
26
|
+
load: async () => ({ default: Page }),
|
|
27
|
+
}, { id: APP_PAGE_SURFACE, order: -10 }),
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
return plugin;
|
|
31
|
+
}
|
|
32
|
+
export { AppPage } from './AppPage';
|
|
33
|
+
export * from './appPage';
|
|
34
|
+
export default defineAppPagePlugin;
|
|
@@ -8,13 +8,13 @@ import { computed, signal } from '@datalayer/reactor';
|
|
|
8
8
|
const NO_STANCE = signal(undefined);
|
|
9
9
|
import { useContributions, useSignalValue, useGate, useReactorPlatform, ReactorLazy, ReactorSlot, useSlotComponents, } from '@datalayer/reactor/react';
|
|
10
10
|
import { ChatBase } from '../../../chat/base/ChatBase';
|
|
11
|
-
import { readServerCatalogue } from '../../../chat/base/modelChoice';
|
|
11
|
+
import { offeredModels as offeredModelsFor, readServerCatalogue, } from '../../../chat/base/modelChoice';
|
|
12
12
|
import { SUGGESTION_CHIP_WIDTH } from '../../../chat/display/EmptyState';
|
|
13
13
|
import { AnonymousKeyExpired } from '@datalayer/core/lib/components/anonymous/AnonymousKeyExpired';
|
|
14
14
|
import { useAnonymousSessionStore } from '../../../runtimes/browser/anonymousToken';
|
|
15
15
|
import { browserProtocolConfig } from '../../../runtimes/browser';
|
|
16
16
|
import { useBrowserInference } from '../../../hooks/useBrowserInference';
|
|
17
|
-
import {
|
|
17
|
+
import { agentInstructions, inPageAgentRefusal, resolveAgentspec, } from '../../apps/agent';
|
|
18
18
|
import { targetRunsAgentInPage, } from '../agents/switchable';
|
|
19
19
|
import { agentIcon } from '../agents/agentIcons';
|
|
20
20
|
import { specSubagents } from '../agents/specSubagents';
|
|
@@ -655,9 +655,23 @@ export default function ChatView({ workspace }) {
|
|
|
655
655
|
* icon for want of this lookup — it used the document tools and called
|
|
656
656
|
* nobody.
|
|
657
657
|
*/
|
|
658
|
-
const
|
|
659
|
-
const
|
|
660
|
-
|
|
658
|
+
const blueprint = useContributions(LoopAgentBlueprint)[0]?.value;
|
|
659
|
+
const blueprintSpecId = blueprint?.specId;
|
|
660
|
+
/*
|
|
661
|
+
* An agent of the catalogue or a Cog's, as a runtime resolves it: an
|
|
662
|
+
* application on a Cog looked up among the agentspecs alone was turned in
|
|
663
|
+
* the page with no instructions at all (LOOP H-01).
|
|
664
|
+
*/
|
|
665
|
+
const spec = resolveAgentspec(member?.specId ?? agentId) ??
|
|
666
|
+
(blueprintSpecId ? resolveAgentspec(blueprintSpecId) : undefined);
|
|
667
|
+
/*
|
|
668
|
+
* The blueprint's words and model — an application's — on top of its
|
|
669
|
+
* spec's, for the agent it contributes; never for a team's member.
|
|
670
|
+
*/
|
|
671
|
+
const blueprintTurn = member ? undefined : blueprint;
|
|
672
|
+
// Turned in the page, an agent that resolves to nothing is refused rather
|
|
673
|
+
// than run bare.
|
|
674
|
+
const inPageRefusal = inPage && !member ? inPageAgentRefusal(blueprintSpecId, spec) : undefined;
|
|
661
675
|
/*
|
|
662
676
|
* The team's name for a member first, then the spec's own.
|
|
663
677
|
*
|
|
@@ -945,12 +959,12 @@ export default function ChatView({ workspace }) {
|
|
|
945
959
|
* for something that was never theirs.
|
|
946
960
|
*/
|
|
947
961
|
const expiredKeyIsTemporary = !signedInUser;
|
|
948
|
-
const chatDisabled = gateBlocked || keyExpired;
|
|
962
|
+
const chatDisabled = gateBlocked || keyExpired || Boolean(inPageRefusal);
|
|
949
963
|
const disabledReason = keyExpired
|
|
950
964
|
? expiredKeyIsTemporary
|
|
951
965
|
? 'This demo runs on a shared key, and its time is up. Sign in to keep going.'
|
|
952
966
|
: 'Your key has expired. Sign in to keep going.'
|
|
953
|
-
: gateReason;
|
|
967
|
+
: (inPageRefusal ?? gateReason);
|
|
954
968
|
/*
|
|
955
969
|
* The prompt's text, held here rather than inside `InputPrompt`.
|
|
956
970
|
*
|
|
@@ -981,7 +995,7 @@ export default function ChatView({ workspace }) {
|
|
|
981
995
|
* the member being addressed changes.
|
|
982
996
|
*/
|
|
983
997
|
const [pickedModel, setPickedModel] = useState();
|
|
984
|
-
const activeModel = pickedModel ?? spec?.model ?? workspace.model ?? '';
|
|
998
|
+
const activeModel = pickedModel ?? blueprintTurn?.model ?? spec?.model ?? workspace.model ?? '';
|
|
985
999
|
/*
|
|
986
1000
|
* The team, in the shape the footer asks for.
|
|
987
1001
|
*
|
|
@@ -1071,7 +1085,7 @@ export default function ChatView({ workspace }) {
|
|
|
1071
1085
|
const protocol = useMemo(() => inPage
|
|
1072
1086
|
? browserProtocolConfig({
|
|
1073
1087
|
agentId: member?.specId ?? agentId,
|
|
1074
|
-
instructions: spec?.
|
|
1088
|
+
instructions: agentInstructions(spec, blueprintTurn?.instructions),
|
|
1075
1089
|
model: activeModel || undefined,
|
|
1076
1090
|
inference,
|
|
1077
1091
|
// Who this member may hand work to, and what they are told. Both
|
|
@@ -1111,6 +1125,7 @@ export default function ChatView({ workspace }) {
|
|
|
1111
1125
|
agentTools,
|
|
1112
1126
|
team,
|
|
1113
1127
|
spec,
|
|
1128
|
+
blueprintTurn,
|
|
1114
1129
|
activeModel,
|
|
1115
1130
|
agentServerUrl,
|
|
1116
1131
|
]);
|
|
@@ -1128,7 +1143,8 @@ export default function ChatView({ workspace }) {
|
|
|
1128
1143
|
const configQuery = useConfig(!inPage, protocol.type === 'ag-ui' ? protocol.configEndpoint : undefined, undefined, agentId);
|
|
1129
1144
|
const skillsQuery = useSkills(true);
|
|
1130
1145
|
const skillActions = useSkillActions(agentId);
|
|
1131
|
-
|
|
1146
|
+
// `undefined` until the runtime answers: its answer is then the list.
|
|
1147
|
+
const [catalogModels, setCatalogModels] = useState(undefined);
|
|
1132
1148
|
useEffect(() => {
|
|
1133
1149
|
/*
|
|
1134
1150
|
* Only where there is a server to ask.
|
|
@@ -1143,23 +1159,31 @@ export default function ChatView({ workspace }) {
|
|
|
1143
1159
|
return undefined;
|
|
1144
1160
|
}
|
|
1145
1161
|
let cancelled = false;
|
|
1146
|
-
|
|
1147
|
-
|
|
1162
|
+
// The agent's own models (its model and model_additionals), each saying
|
|
1163
|
+
// whether its inference serves it.
|
|
1164
|
+
void fetch(`${agentServerUrl}/api/v1/configure/models?agent_id=${encodeURIComponent(agentId)}`)
|
|
1165
|
+
.then(response => (response.ok ? response.json() : null))
|
|
1148
1166
|
.then(payload => {
|
|
1149
1167
|
if (!cancelled) {
|
|
1150
1168
|
// In the chat's shape: the route says `available`, the menu and
|
|
1151
1169
|
// the opening pick read `isAvailable`, and an unmapped row passes
|
|
1152
1170
|
// for usable. See `readServerCatalogue`.
|
|
1153
|
-
|
|
1171
|
+
// A runtime that did not answer offers nothing: the agentspecs
|
|
1172
|
+
// catalogue is not its answer.
|
|
1173
|
+
setCatalogModels(payload ? readServerCatalogue(payload) : []);
|
|
1154
1174
|
}
|
|
1155
1175
|
})
|
|
1156
1176
|
// No catalogue is not an error worth a banner: the menu simply has
|
|
1157
1177
|
// nothing to offer, and the agent answers on whatever it was given.
|
|
1158
|
-
.catch(() =>
|
|
1178
|
+
.catch(() => {
|
|
1179
|
+
if (!cancelled) {
|
|
1180
|
+
setCatalogModels([]);
|
|
1181
|
+
}
|
|
1182
|
+
});
|
|
1159
1183
|
return () => {
|
|
1160
1184
|
cancelled = true;
|
|
1161
1185
|
};
|
|
1162
|
-
}, [agentServerUrl, inPage]);
|
|
1186
|
+
}, [agentServerUrl, agentId, inPage]);
|
|
1163
1187
|
/*
|
|
1164
1188
|
* What to offer when the server has no catalogue of its own.
|
|
1165
1189
|
*
|
|
@@ -1168,16 +1192,14 @@ export default function ChatView({ workspace }) {
|
|
|
1168
1192
|
* with nothing in it is a control that opens onto an empty list — so the
|
|
1169
1193
|
* agentspecs catalogue stands in, filtered to the models marked available.
|
|
1170
1194
|
*/
|
|
1171
|
-
const offeredModels = useMemo(() => catalogModels
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
provider: model.provider,
|
|
1180
|
-
})), [catalogModels]);
|
|
1195
|
+
const offeredModels = useMemo(() => offeredModelsFor(catalogModels, () => Object.values(AI_MODEL_CATALOGUE)
|
|
1196
|
+
// A typed-judgment model answers typed questions, not a chat.
|
|
1197
|
+
.filter(model => model.available && isChatModel(model))
|
|
1198
|
+
.map(model => ({
|
|
1199
|
+
id: model.id,
|
|
1200
|
+
name: model.name,
|
|
1201
|
+
provider: model.provider,
|
|
1202
|
+
}))), [catalogModels]);
|
|
1181
1203
|
/*
|
|
1182
1204
|
* The tools the agent can call, from both sides.
|
|
1183
1205
|
*
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The split: the conversation and the work side by side, one hairline
|
|
3
|
+
* between them that a person drags (LOOP T-07, an Appspec's `layout: split`).
|
|
4
|
+
*
|
|
5
|
+
* The page layout's other arrangement. Where `page` puts the work on a sheet
|
|
6
|
+
* and the conversation in a panel that opens when wanted, `split` keeps both
|
|
7
|
+
* on screen: the conversation on the left — its transcript with the composer
|
|
8
|
+
* under it — and the work on the right, the way the reference screens show a
|
|
9
|
+
* mail, a document or the agent's computer beside the chat.
|
|
10
|
+
*
|
|
11
|
+
* The hairline is a separator a person can move with the pointer or the
|
|
12
|
+
* keyboard (arrows by a step, Home and End to the bounds); the share it
|
|
13
|
+
* leaves the conversation is kept while the workspace is mounted.
|
|
14
|
+
*
|
|
15
|
+
* @module loop/plugins/page-layout/SplitLayout
|
|
16
|
+
*/
|
|
17
|
+
import type { JSX } from 'react';
|
|
18
|
+
import type { ChatLayoutParts } from '../../core';
|
|
19
|
+
/** The conversation's share of the width when the split opens. */
|
|
20
|
+
export declare const SPLIT_DEFAULT_SHARE = 0.4;
|
|
21
|
+
/** The narrowest and widest the conversation may be made. */
|
|
22
|
+
export declare const SPLIT_MIN_SHARE = 0.2;
|
|
23
|
+
export declare const SPLIT_MAX_SHARE = 0.8;
|
|
24
|
+
/** How far an arrow key moves the hairline. */
|
|
25
|
+
export declare const SPLIT_KEY_STEP = 0.05;
|
|
26
|
+
/** A share, held within the bounds. */
|
|
27
|
+
export declare function clampSplitShare(share: number): number;
|
|
28
|
+
/** Where a key moves the hairline, or `null` for a key it ignores. */
|
|
29
|
+
export declare function splitShareForKey(share: number, key: string): number | null;
|
|
30
|
+
export declare function SplitLayout({ editors, hasEditor, transcript, prompt, chips, picker, transient, }: ChatLayoutParts): JSX.Element;
|
|
31
|
+
export default SplitLayout;
|