@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
|
@@ -6,12 +6,15 @@
|
|
|
6
6
|
* A chat, a widget or a worker runs as its conversation. Its page is its
|
|
7
7
|
* A2UI surface (`interface.surface`) drawn beside that conversation, fed from
|
|
8
8
|
* it: the question last asked, the answer as it streams, where the turn
|
|
9
|
-
* stands. What a block writes — an input's `value` — stays in the surface's
|
|
9
|
+
* stands, a chat's whole conversation. What a block writes — an input's `value` — stays in the surface's
|
|
10
10
|
* data model at a path the application takes, and what a button does reaches
|
|
11
11
|
* the application through the chat's own channels: a message sent, the
|
|
12
12
|
* answer stopped, the conversation started over. Nothing here is a server
|
|
13
|
-
* API
|
|
14
|
-
*
|
|
13
|
+
* API: what a button did besides its message — the block's action, the files
|
|
14
|
+
* given, the settings — goes with the chat's own run to the application's
|
|
15
|
+
* session (LOOP R-04, `forwardedProps.loop`), which puts a file where the
|
|
16
|
+
* application reads it; a setting also reaches a written application in the
|
|
17
|
+
* words of its message.
|
|
15
18
|
*
|
|
16
19
|
* A decision is not here: its page is drawn by the Studio's run page, with
|
|
17
20
|
* its own paths (`INTERFACE_PATHS`), until it moves onto the workspace (R-02).
|
|
@@ -21,7 +24,7 @@
|
|
|
21
24
|
* @module loop/plugins/app-page/appPageModel
|
|
22
25
|
*/
|
|
23
26
|
import type { AppSettingSpec, AppSpec } from '../../../types/agentspecs';
|
|
24
|
-
import type { ChatTurnSnapshot } from '../../core';
|
|
27
|
+
import type { ChatTurnSnapshot, ConversationEntry } from '../../core';
|
|
25
28
|
/** The id of the editor surface an application's page is drawn in. */
|
|
26
29
|
export declare const APP_PAGE_SURFACE = "app-page";
|
|
27
30
|
/** The id of the A2UI surface itself, inside it. */
|
|
@@ -94,8 +97,11 @@ export declare function hasAppPage(app: Pick<AppSpec, 'kind' | 'interface' | 'ag
|
|
|
94
97
|
export declare function surfaceUnshown(app: Pick<AppSpec, 'kind' | 'interface' | 'agent'>): string | null;
|
|
95
98
|
/** A setting's first value, as its input holds it. */
|
|
96
99
|
export declare function initialInput(setting: AppSettingSpec): unknown;
|
|
97
|
-
/**
|
|
98
|
-
|
|
100
|
+
/**
|
|
101
|
+
* What the conversation publishes to the page, by path, now: from its
|
|
102
|
+
* current turn, and for a chat its whole conversation.
|
|
103
|
+
*/
|
|
104
|
+
export declare function appPageData(app: Pick<AppSpec, 'kind' | 'name' | 'goal'>, turn: ChatTurnSnapshot, conversation: ConversationEntry[]): Record<string, unknown>;
|
|
99
105
|
/** The page's data model when it is drawn: what is published, the inputs, the draft. */
|
|
100
106
|
export declare function appPageInitialData(app: Pick<AppSpec, 'kind' | 'name' | 'goal' | 'interface'>): Record<string, unknown>;
|
|
101
107
|
/**
|
|
@@ -103,6 +109,51 @@ export declare function appPageInitialData(app: Pick<AppSpec, 'kind' | 'name' |
|
|
|
103
109
|
* its label, then any other value a block wrote under `/inputs` by its id.
|
|
104
110
|
*/
|
|
105
111
|
export declare function inputsInWords(app: Pick<AppSpec, 'interface'>, inputs: unknown): string;
|
|
112
|
+
/**
|
|
113
|
+
* The largest file a session takes: what the session API refuses above
|
|
114
|
+
* (`agent_runtimes.loop.apps.sessions.MAX_FILE_BYTES`), said here before
|
|
115
|
+
* the file is sent.
|
|
116
|
+
*/
|
|
117
|
+
export declare const MAX_FILE_BYTES: number;
|
|
118
|
+
/** A file as File upload gives it, and as the session API takes it. */
|
|
119
|
+
export type GivenFile = {
|
|
120
|
+
name: string;
|
|
121
|
+
type: string;
|
|
122
|
+
size: number;
|
|
123
|
+
data_url: string;
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* The files given on the page, as the session API takes them — or why they
|
|
127
|
+
* cannot go: what was given is not files as File upload gives them, or one
|
|
128
|
+
* is larger than a session takes. What becomes of each — put in the
|
|
129
|
+
* application's sandbox, or its text in the message — is the session's to
|
|
130
|
+
* decide (LOOP R-04).
|
|
131
|
+
*/
|
|
132
|
+
export declare function givenFiles(files: unknown): {
|
|
133
|
+
files: GivenFile[];
|
|
134
|
+
} | {
|
|
135
|
+
refused: string;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* The application's settings as the page holds them, as the session API
|
|
139
|
+
* takes them: a choice as the option chosen, a number as a number, a toggle
|
|
140
|
+
* as on or off — each setting the page has a value for.
|
|
141
|
+
*/
|
|
142
|
+
export declare function settingsOf(app: Pick<AppSpec, 'interface'>, inputs: unknown): Record<string, unknown>;
|
|
143
|
+
/**
|
|
144
|
+
* What the page did besides its message, for the session API: the action of
|
|
145
|
+
* the block pressed, the files given with it, the settings it holds. Sent
|
|
146
|
+
* with the chat's own run (AG-UI's `forwardedProps.loop`), so that what it
|
|
147
|
+
* answers streams in the conversation (LOOP R-04).
|
|
148
|
+
*/
|
|
149
|
+
export type AppPageLoop = {
|
|
150
|
+
action: {
|
|
151
|
+
name: string;
|
|
152
|
+
payload: Record<string, unknown>;
|
|
153
|
+
};
|
|
154
|
+
files?: GivenFile[];
|
|
155
|
+
settings?: Record<string, unknown>;
|
|
156
|
+
};
|
|
106
157
|
/** What a button asked, as the surface hands it over. */
|
|
107
158
|
export type AppPageEvent = {
|
|
108
159
|
name: string;
|
|
@@ -112,6 +163,8 @@ export type AppPageEvent = {
|
|
|
112
163
|
export type AppPageOutcome = {
|
|
113
164
|
/** A message for the application, sent as the person's next turn. */
|
|
114
165
|
send: string;
|
|
166
|
+
/** What the page did besides, for the session it is sent in. */
|
|
167
|
+
loop: AppPageLoop;
|
|
115
168
|
/** The inputs in words as sent, to tell a change next time. */
|
|
116
169
|
inputs: string;
|
|
117
170
|
/** Paths to empty once it is sent. */
|
|
@@ -28,6 +28,18 @@ const SEND = {
|
|
|
28
28
|
context: ['message'],
|
|
29
29
|
asks: { message: 'The message it sends' },
|
|
30
30
|
};
|
|
31
|
+
const MESSAGES = {
|
|
32
|
+
path: '/messages',
|
|
33
|
+
meaning: 'The conversation so far, in order: each message {role: user or assistant, text}, each tool called {role: tool, name, args, result}.',
|
|
34
|
+
words: 'The conversation',
|
|
35
|
+
list: true,
|
|
36
|
+
};
|
|
37
|
+
const FILES = {
|
|
38
|
+
path: '/files',
|
|
39
|
+
meaning: 'The files given on the page, each {name, type, size, data_url}: they go to the application’s session with the next run, put in its sandbox when it has a shell, a text file in the message when it has none.',
|
|
40
|
+
words: 'The files given',
|
|
41
|
+
list: true,
|
|
42
|
+
};
|
|
31
43
|
const STOP = {
|
|
32
44
|
name: 'stop',
|
|
33
45
|
meaning: 'Stop the answer being written.',
|
|
@@ -54,6 +66,7 @@ export const APP_KIND_PATHS = {
|
|
|
54
66
|
words: 'Its answer',
|
|
55
67
|
},
|
|
56
68
|
STATUS,
|
|
69
|
+
MESSAGES,
|
|
57
70
|
],
|
|
58
71
|
accepts: [DRAFT],
|
|
59
72
|
actions: [
|
|
@@ -79,15 +92,22 @@ export const APP_KIND_PATHS = {
|
|
|
79
92
|
},
|
|
80
93
|
STATUS,
|
|
81
94
|
],
|
|
82
|
-
accepts: [],
|
|
95
|
+
accepts: [FILES],
|
|
83
96
|
actions: [
|
|
84
97
|
{
|
|
85
98
|
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.',
|
|
99
|
+
meaning: 'Run it on its inputs: every value written under /inputs is sent, and each file at /files, with the context’s message when there is one.',
|
|
87
100
|
words: 'Run it on its inputs',
|
|
88
101
|
context: ['message'],
|
|
89
102
|
asks: { message: 'What it says before the inputs' },
|
|
90
103
|
},
|
|
104
|
+
{
|
|
105
|
+
name: 'upload',
|
|
106
|
+
meaning: 'Run it on the files just given — the context’s files, else those at /files — with its inputs.',
|
|
107
|
+
words: 'Run it on the files given',
|
|
108
|
+
context: ['files'],
|
|
109
|
+
asks: { files: 'The files it runs on' },
|
|
110
|
+
},
|
|
91
111
|
STOP,
|
|
92
112
|
],
|
|
93
113
|
inputs: true,
|
|
@@ -197,8 +217,11 @@ export function initialInput(setting) {
|
|
|
197
217
|
return String(setting.default ?? '');
|
|
198
218
|
}
|
|
199
219
|
}
|
|
200
|
-
/**
|
|
201
|
-
|
|
220
|
+
/**
|
|
221
|
+
* What the conversation publishes to the page, by path, now: from its
|
|
222
|
+
* current turn, and for a chat its whole conversation.
|
|
223
|
+
*/
|
|
224
|
+
export function appPageData(app, turn, conversation) {
|
|
202
225
|
const data = {
|
|
203
226
|
'/app': app.name,
|
|
204
227
|
'/status': STATUS_WORDS[turn.status] ?? '',
|
|
@@ -207,6 +230,7 @@ export function appPageData(app, turn) {
|
|
|
207
230
|
case 'chat':
|
|
208
231
|
data['/question'] = turn.user ?? '';
|
|
209
232
|
data['/answer'] = turn.assistant ?? '';
|
|
233
|
+
data['/messages'] = conversation;
|
|
210
234
|
break;
|
|
211
235
|
case 'widget':
|
|
212
236
|
data['/output'] = turn.assistant ?? '';
|
|
@@ -224,7 +248,7 @@ export function appPageData(app, turn) {
|
|
|
224
248
|
/** The page's data model when it is drawn: what is published, the inputs, the draft. */
|
|
225
249
|
export function appPageInitialData(app) {
|
|
226
250
|
const tree = {};
|
|
227
|
-
for (const [path, value] of Object.entries(appPageData(app, { id: 0, status: 'idle' }))) {
|
|
251
|
+
for (const [path, value] of Object.entries(appPageData(app, { id: 0, status: 'idle' }, []))) {
|
|
228
252
|
tree[path.slice(1)] = value;
|
|
229
253
|
}
|
|
230
254
|
const paths = appKindPaths(app);
|
|
@@ -237,6 +261,9 @@ export function appPageInitialData(app) {
|
|
|
237
261
|
if (paths.accepts.some(entry => entry.path === DRAFT.path)) {
|
|
238
262
|
tree.draft = '';
|
|
239
263
|
}
|
|
264
|
+
if (paths.accepts.some(entry => entry.path === FILES.path)) {
|
|
265
|
+
tree.files = [];
|
|
266
|
+
}
|
|
240
267
|
return tree;
|
|
241
268
|
}
|
|
242
269
|
/** One input in words: `Plan: Team`; nothing for an empty one. */
|
|
@@ -273,6 +300,87 @@ export function inputsInWords(app, inputs) {
|
|
|
273
300
|
}
|
|
274
301
|
return lines.filter(Boolean).join('\n');
|
|
275
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* The largest file a session takes: what the session API refuses above
|
|
305
|
+
* (`agent_runtimes.loop.apps.sessions.MAX_FILE_BYTES`), said here before
|
|
306
|
+
* the file is sent.
|
|
307
|
+
*/
|
|
308
|
+
export const MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|
309
|
+
/**
|
|
310
|
+
* The files given on the page, as the session API takes them — or why they
|
|
311
|
+
* cannot go: what was given is not files as File upload gives them, or one
|
|
312
|
+
* is larger than a session takes. What becomes of each — put in the
|
|
313
|
+
* application's sandbox, or its text in the message — is the session's to
|
|
314
|
+
* decide (LOOP R-04).
|
|
315
|
+
*/
|
|
316
|
+
export function givenFiles(files) {
|
|
317
|
+
if (files === undefined || files === null) {
|
|
318
|
+
return { files: [] };
|
|
319
|
+
}
|
|
320
|
+
if (!Array.isArray(files)) {
|
|
321
|
+
return { refused: 'What was given as files is not a list of files.' };
|
|
322
|
+
}
|
|
323
|
+
const given = [];
|
|
324
|
+
for (const [index, file] of files.entries()) {
|
|
325
|
+
const item = (file ?? {});
|
|
326
|
+
if (typeof item.name !== 'string' || typeof item.data_url !== 'string') {
|
|
327
|
+
return {
|
|
328
|
+
refused: `File ${index + 1} is not a file as File upload gives it ({name, type, size, data_url}).`,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
const size = typeof item.size === 'number' ? item.size : 0;
|
|
332
|
+
if (size > MAX_FILE_BYTES) {
|
|
333
|
+
return {
|
|
334
|
+
refused: `${item.name} is ${size.toLocaleString('en')} bytes: a session takes files of at most ${MAX_FILE_BYTES.toLocaleString('en')}.`,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
given.push({
|
|
338
|
+
name: item.name,
|
|
339
|
+
type: typeof item.type === 'string' ? item.type : '',
|
|
340
|
+
size,
|
|
341
|
+
data_url: item.data_url,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
return { files: given };
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* The application's settings as the page holds them, as the session API
|
|
348
|
+
* takes them: a choice as the option chosen, a number as a number, a toggle
|
|
349
|
+
* as on or off — each setting the page has a value for.
|
|
350
|
+
*/
|
|
351
|
+
export function settingsOf(app, inputs) {
|
|
352
|
+
const values = inputs && typeof inputs === 'object'
|
|
353
|
+
? inputs
|
|
354
|
+
: {};
|
|
355
|
+
const settings = {};
|
|
356
|
+
for (const setting of app.interface.settings) {
|
|
357
|
+
const value = values[setting.id];
|
|
358
|
+
if (value === undefined || value === null) {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
switch (setting.type) {
|
|
362
|
+
case 'select': {
|
|
363
|
+
const chosen = Array.isArray(value) ? value[0] : value;
|
|
364
|
+
if (chosen !== undefined && chosen !== '') {
|
|
365
|
+
settings[setting.id] = String(chosen);
|
|
366
|
+
}
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
case 'toggle':
|
|
370
|
+
settings[setting.id] = Boolean(value);
|
|
371
|
+
break;
|
|
372
|
+
case 'slider':
|
|
373
|
+
case 'number':
|
|
374
|
+
if (value !== '' && !Number.isNaN(Number(value))) {
|
|
375
|
+
settings[setting.id] = Number(value);
|
|
376
|
+
}
|
|
377
|
+
break;
|
|
378
|
+
default:
|
|
379
|
+
settings[setting.id] = String(value);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return settings;
|
|
383
|
+
}
|
|
276
384
|
/**
|
|
277
385
|
* What a button pressed on the page does.
|
|
278
386
|
*
|
|
@@ -293,15 +401,35 @@ export function appPageAction(app, event, read, lastInputs = '') {
|
|
|
293
401
|
const given = event.context?.message;
|
|
294
402
|
const message = typeof given === 'string' ? given.trim() : '';
|
|
295
403
|
const inputs = paths.inputs ? inputsInWords(app, read('/inputs')) : '';
|
|
404
|
+
const settings = paths.inputs ? settingsOf(app, read('/inputs')) : undefined;
|
|
405
|
+
const payload = Object.fromEntries(Object.entries(event.context ?? {}).filter(([key]) => key !== 'message' && key !== 'files'));
|
|
406
|
+
const loop = (files) => ({
|
|
407
|
+
action: { name: event.name, payload },
|
|
408
|
+
...(files && files.length > 0 ? { files } : {}),
|
|
409
|
+
...(settings ? { settings } : {}),
|
|
410
|
+
});
|
|
296
411
|
switch (event.name) {
|
|
297
412
|
case 'stop':
|
|
298
413
|
return { stop: true };
|
|
299
414
|
case 'new':
|
|
300
415
|
return { newChat: true };
|
|
301
|
-
case 'run':
|
|
302
|
-
|
|
416
|
+
case 'run':
|
|
417
|
+
case 'upload': {
|
|
418
|
+
const context = event.context?.files;
|
|
419
|
+
const files = givenFiles(event.name === 'upload' && context !== undefined
|
|
420
|
+
? context
|
|
421
|
+
: read(FILES.path));
|
|
422
|
+
if ('refused' in files) {
|
|
423
|
+
return files;
|
|
424
|
+
}
|
|
425
|
+
if (event.name === 'upload' && files.files.length === 0) {
|
|
426
|
+
return { refused: 'No file was given: choose one first.' };
|
|
427
|
+
}
|
|
428
|
+
const names = files.files.map(file => file.name).join(', ');
|
|
429
|
+
const text = [message, inputs].filter(Boolean).join('\n') ||
|
|
430
|
+
(names ? `Run on ${names}.` : '');
|
|
303
431
|
return text
|
|
304
|
-
? { send: text, inputs, clear: [] }
|
|
432
|
+
? { send: text, inputs, clear: [], loop: loop(files.files) }
|
|
305
433
|
: { refused: `${app.name} has no inputs to run on: set one first.` };
|
|
306
434
|
}
|
|
307
435
|
default: {
|
|
@@ -315,6 +443,7 @@ export function appPageAction(app, event, read, lastInputs = '') {
|
|
|
315
443
|
send: changed ? `${written}\n\n${inputs}` : written,
|
|
316
444
|
inputs,
|
|
317
445
|
clear: message ? [] : [DRAFT.path],
|
|
446
|
+
loop: loop(),
|
|
318
447
|
};
|
|
319
448
|
}
|
|
320
449
|
}
|
|
@@ -1,8 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@datalayer/loop-plugin-app-page` — an application's page, as a plugin
|
|
3
|
+
* (LOOP R-01, R-01b).
|
|
4
|
+
*
|
|
5
|
+
* A chat, a widget or a worker runs as its conversation; its page — its
|
|
6
|
+
* A2UI surface, or the default page of its kind when none is composed — is
|
|
7
|
+
* contributed as the editor beside the chat (`LoopEditorView`), the place a
|
|
8
|
+
* notebook or a document takes in other workspaces. `AppRenderer` mounts it
|
|
9
|
+
* for an application that has one (`hasAppPage`) and opens the chat on it.
|
|
10
|
+
*
|
|
11
|
+
* What the page shows and takes, by kind, is `APP_KIND_PATHS`.
|
|
12
|
+
*
|
|
13
|
+
* A page the workspace does not draw itself — a decision's, whose page is
|
|
14
|
+
* its host's (the Studio's run page, its hosted address, its embed) — is
|
|
15
|
+
* contributed by the host through `defineAppHostPagePlugin`, as the
|
|
16
|
+
* workspace's one view (`LoopViewType`), in a workspace without a
|
|
17
|
+
* conversation (LOOP R-02).
|
|
18
|
+
*
|
|
19
|
+
* @module loop/plugins/app-page
|
|
20
|
+
*/
|
|
21
|
+
import type { ComponentType } from 'react';
|
|
1
22
|
import { type ReactorPlugin } from '@datalayer/reactor';
|
|
2
23
|
import type { AppSpec } from '../../../types/agentspecs';
|
|
24
|
+
import { type LoopWorkspaceContext } from '../../core';
|
|
3
25
|
export declare const APP_PAGE_PLUGIN_NAME = "@datalayer/loop-plugin-app-page";
|
|
4
26
|
/** The plugin that draws an application's page beside its conversation. */
|
|
5
27
|
export declare function defineAppPagePlugin(app: AppSpec): ReactorPlugin<Record<string, never>, unknown, unknown>;
|
|
28
|
+
/** The view an application's page is, when its host draws it. */
|
|
29
|
+
export declare const APP_HOST_PAGE_VIEW = "app-page";
|
|
30
|
+
/** What a host's page is given: the application, and the workspace it is the view of. */
|
|
31
|
+
export type AppHostPageProps = {
|
|
32
|
+
app: AppSpec;
|
|
33
|
+
workspace: LoopWorkspaceContext;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The plugin that makes a host's page the workspace's view (LOOP R-02): the
|
|
37
|
+
* page a host draws for an application whose kind the workspace has no page
|
|
38
|
+
* for — a decision's. The workspace opens on it; it is ordered before every
|
|
39
|
+
* other view.
|
|
40
|
+
*/
|
|
41
|
+
export declare function defineAppHostPagePlugin(app: AppSpec, Page: ComponentType<AppHostPageProps>): ReactorPlugin<Record<string, never>, unknown, unknown>;
|
|
6
42
|
export { AppPage, type AppPageProps } from './AppPage';
|
|
7
43
|
export * from './appPageModel';
|
|
8
44
|
export default defineAppPagePlugin;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { BrowserIcon } from '@primer/octicons-react';
|
|
3
3
|
import { contribution, definePlugin, } from '@datalayer/reactor';
|
|
4
|
-
import { LoopEditorView } from '../../core';
|
|
4
|
+
import { LoopEditorView, LoopViewType, } from '../../core';
|
|
5
5
|
import { AppPage } from './AppPage';
|
|
6
6
|
import { APP_PAGE_SURFACE } from './appPageModel';
|
|
7
7
|
export const APP_PAGE_PLUGIN_NAME = '@datalayer/loop-plugin-app-page';
|
|
@@ -29,6 +29,37 @@ export function defineAppPagePlugin(app) {
|
|
|
29
29
|
});
|
|
30
30
|
return plugin;
|
|
31
31
|
}
|
|
32
|
+
/** The view an application's page is, when its host draws it. */
|
|
33
|
+
export const APP_HOST_PAGE_VIEW = 'app-page';
|
|
34
|
+
/**
|
|
35
|
+
* The plugin that makes a host's page the workspace's view (LOOP R-02): the
|
|
36
|
+
* page a host draws for an application whose kind the workspace has no page
|
|
37
|
+
* for — a decision's. The workspace opens on it; it is ordered before every
|
|
38
|
+
* other view.
|
|
39
|
+
*/
|
|
40
|
+
export function defineAppHostPagePlugin(app, Page) {
|
|
41
|
+
// One component per plugin: the view host keeps it, and the application with it.
|
|
42
|
+
function View({ workspace, }) {
|
|
43
|
+
return _jsx(Page, { app: app, workspace: workspace });
|
|
44
|
+
}
|
|
45
|
+
const plugin = definePlugin({
|
|
46
|
+
name: `${APP_PAGE_PLUGIN_NAME}-${app.id}`,
|
|
47
|
+
displayName: `${app.name}: its page`,
|
|
48
|
+
description: `The page of ${app.name}, drawn by its host.`,
|
|
49
|
+
octicon: 'browser',
|
|
50
|
+
emoji: app.emoji || undefined,
|
|
51
|
+
contributes: [
|
|
52
|
+
contribution(LoopViewType, {
|
|
53
|
+
viewType: APP_HOST_PAGE_VIEW,
|
|
54
|
+
title: app.name,
|
|
55
|
+
icon: BrowserIcon,
|
|
56
|
+
order: -10,
|
|
57
|
+
load: async () => ({ default: View }),
|
|
58
|
+
}, { id: APP_HOST_PAGE_VIEW, order: -10 }),
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
return plugin;
|
|
62
|
+
}
|
|
32
63
|
export { AppPage } from './AppPage';
|
|
33
64
|
export * from './appPageModel';
|
|
34
65
|
export default defineAppPagePlugin;
|
|
@@ -39,7 +39,7 @@ import { useConfig } from '../../../hooks/useConfig';
|
|
|
39
39
|
import { useSkills, useSkillActions } from '../../../hooks/useSkills';
|
|
40
40
|
import { AI_MODEL_CATALOGUE, isChatModel } from '../../../specs/models';
|
|
41
41
|
import { LoopAgentBlueprint, LoopAgentGate, LoopChatComposer, LoopChatExtras, LoopChatHeader, LoopChatLayout, LoopChatSuggestion, LoopChatSurface, LoopChatTurn, LoopFrontendTool, LoopSlots, canOpenView, onPromptFocusRequest, onSurfaceRequest, useLoopPromptStore, } from '../../core';
|
|
42
|
-
import { turnWritersOf } from './turnState';
|
|
42
|
+
import { messageText, turnWritersOf } from './turnState';
|
|
43
43
|
import { isInactiveSurfaceContribution, orderToolContributions, toolsForChatView, } from './chatViewTools';
|
|
44
44
|
import { useAgentCommandTools } from '../../../tools/adapters/commands';
|
|
45
45
|
import { NONE_EDITOR, seedEditorChoice, setEditorOptions, } from '../shell/editorChoice';
|
|
@@ -225,6 +225,8 @@ export default function ChatView({ workspace }) {
|
|
|
225
225
|
if (!feed) {
|
|
226
226
|
return;
|
|
227
227
|
}
|
|
228
|
+
// The whole conversation, for a page that shows it (a Chat block).
|
|
229
|
+
feed.items(items);
|
|
228
230
|
const SETTLED = new Set([
|
|
229
231
|
'complete',
|
|
230
232
|
'completed',
|
|
@@ -315,24 +317,42 @@ export default function ChatView({ workspace }) {
|
|
|
315
317
|
break;
|
|
316
318
|
}
|
|
317
319
|
}
|
|
318
|
-
const textOf = (message) => {
|
|
319
|
-
const content = message.content;
|
|
320
|
-
return typeof content === 'string'
|
|
321
|
-
? content
|
|
322
|
-
: content
|
|
323
|
-
.map(part => typeof part === 'string'
|
|
324
|
-
? part
|
|
325
|
-
: (part.text ?? ''))
|
|
326
|
-
.join('');
|
|
327
|
-
};
|
|
328
320
|
const reply = messages
|
|
329
321
|
.slice(lastUser + 1)
|
|
330
322
|
.filter(message => message.role === 'assistant')
|
|
331
|
-
.map(
|
|
323
|
+
.map(messageText)
|
|
332
324
|
.filter(text => text.trim().length > 0)
|
|
333
325
|
.join('\n\n');
|
|
334
326
|
feed.assistant(reply);
|
|
335
327
|
}, []);
|
|
328
|
+
const heldForAdapter = useRef([]);
|
|
329
|
+
/** Whether the agent is turned in this page: read by `sendNow`, set below. */
|
|
330
|
+
const inPageRef = useRef(false);
|
|
331
|
+
/*
|
|
332
|
+
* Every message that goes to the agent goes through here: the composer's
|
|
333
|
+
* (through the workspace's dispatch and the prompt channel), a host's, and
|
|
334
|
+
* one sent from the application's page (`viewControls.send`) — so each
|
|
335
|
+
* begins its turn, and a page reading the turn sees it asked and answered.
|
|
336
|
+
*
|
|
337
|
+
* What the page did besides the message goes with it as AG-UI's
|
|
338
|
+
* `forwardedProps` (`loop`, LOOP R-04): the application's session takes a
|
|
339
|
+
* file there. An agent turned in this page has no session, and no file is
|
|
340
|
+
* handed to it: said, rather than dropped.
|
|
341
|
+
*/
|
|
342
|
+
const sendNow = useCallback((message, forwardedProps) => {
|
|
343
|
+
const loop = forwardedProps?.loop;
|
|
344
|
+
if (inPageRef.current && loop?.files && loop.files.length > 0) {
|
|
345
|
+
return 'A file is given to an application running on a runtime: run it on Datalayer or on your machine to give it one.';
|
|
346
|
+
}
|
|
347
|
+
// A new turn: whatever the panel showed is gone, this message is it.
|
|
348
|
+
turnFeedRef.current?.begin(message, controlsRef.current?.thread());
|
|
349
|
+
const send = controlsRef.current?.send;
|
|
350
|
+
if (!send) {
|
|
351
|
+
heldForAdapter.current.push([message, forwardedProps]);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
send(message, forwardedProps);
|
|
355
|
+
}, []);
|
|
336
356
|
// One writer, so neither fact can erase the other.
|
|
337
357
|
useEffect(() => {
|
|
338
358
|
setViewControls(sendReady
|
|
@@ -340,10 +360,10 @@ export default function ChatView({ workspace }) {
|
|
|
340
360
|
busy,
|
|
341
361
|
stop: controlsRef.current?.stop,
|
|
342
362
|
newChat: controlsRef.current?.newChat,
|
|
343
|
-
send:
|
|
363
|
+
send: sendNow,
|
|
344
364
|
}
|
|
345
365
|
: null);
|
|
346
|
-
}, [busy, sendReady, setViewControls]);
|
|
366
|
+
}, [busy, sendReady, setViewControls, sendNow]);
|
|
347
367
|
// Stop reporting when the view goes away, or the shell would keep a stop
|
|
348
368
|
// button for something that is no longer there.
|
|
349
369
|
useEffect(() => () => setViewControls(null), [setViewControls]);
|
|
@@ -358,23 +378,15 @@ export default function ChatView({ workspace }) {
|
|
|
358
378
|
* again. Held instead, and sent the moment the adapter is ready, which is
|
|
359
379
|
* the next second or two rather than minutes.
|
|
360
380
|
*/
|
|
361
|
-
|
|
362
|
-
useEffect(() => workspace.prompts.subscribe(message => {
|
|
363
|
-
const send = controlsRef.current?.send;
|
|
364
|
-
if (!send) {
|
|
365
|
-
heldForAdapter.current.push(message);
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
send(message);
|
|
369
|
-
}), [workspace.prompts]);
|
|
381
|
+
useEffect(() => workspace.prompts.subscribe(sendNow), [workspace.prompts, sendNow]);
|
|
370
382
|
// The adapter arrived: whatever was said while it was starting goes now.
|
|
371
383
|
useEffect(() => {
|
|
372
384
|
if (!sendReady || heldForAdapter.current.length === 0) {
|
|
373
385
|
return;
|
|
374
386
|
}
|
|
375
387
|
const held = heldForAdapter.current.splice(0, heldForAdapter.current.length);
|
|
376
|
-
for (const message of held) {
|
|
377
|
-
controlsRef.current?.send(message);
|
|
388
|
+
for (const [message, forwardedProps] of held) {
|
|
389
|
+
controlsRef.current?.send(message, forwardedProps);
|
|
378
390
|
}
|
|
379
391
|
}, [sendReady]);
|
|
380
392
|
/*
|
|
@@ -397,8 +409,8 @@ export default function ChatView({ workspace }) {
|
|
|
397
409
|
inputPromptConfig?.firstPromptHook?.();
|
|
398
410
|
}
|
|
399
411
|
setTransient(null);
|
|
400
|
-
//
|
|
401
|
-
|
|
412
|
+
// The turn begins where the message goes out (`sendNow`): a command
|
|
413
|
+
// the workspace runs is not one.
|
|
402
414
|
const outcome = await workspace.submit(message);
|
|
403
415
|
if (!outcome.handled) {
|
|
404
416
|
/*
|
|
@@ -628,6 +640,7 @@ export default function ChatView({ workspace }) {
|
|
|
628
640
|
* for — so the location wins, which is the same rule the examples follow.
|
|
629
641
|
*/
|
|
630
642
|
const inPage = targetRunsAgentInPage(workspace.sandbox.target ?? 'local');
|
|
643
|
+
inPageRef.current = inPage;
|
|
631
644
|
/*
|
|
632
645
|
* Who is being addressed, when this workspace runs a team.
|
|
633
646
|
*
|
|
@@ -1115,7 +1128,13 @@ export default function ChatView({ workspace }) {
|
|
|
1115
1128
|
})
|
|
1116
1129
|
: {
|
|
1117
1130
|
type: 'ag-ui',
|
|
1118
|
-
|
|
1131
|
+
// An application's agent is spoken to through its session API
|
|
1132
|
+
// (LOOP R-04): each thread a session, recorded under its uid,
|
|
1133
|
+
// run in the name the application runs in, and taking what the
|
|
1134
|
+
// page did besides the message — the same AG-UI events back.
|
|
1135
|
+
endpoint: blueprintTurn?.createPayload?.app_spec
|
|
1136
|
+
? `${agentServerUrl}/api/v1/apps/agents/${encodeURIComponent(agentId)}/ag-ui/`
|
|
1137
|
+
: `${agentServerUrl}/api/v1/ag-ui/${agentId}/`,
|
|
1119
1138
|
agentId,
|
|
1120
1139
|
// `/api/v1/configure`, not `/api/v1/configure/config`: the hooks
|
|
1121
1140
|
// strip one trailing `config`/`configure` segment to find the API
|
|
@@ -82,7 +82,12 @@ export const ChatPlugin = definePlugin({
|
|
|
82
82
|
*/
|
|
83
83
|
build: ctx => {
|
|
84
84
|
const feed = createTurnFeed();
|
|
85
|
-
ctx.contribute(LoopChatTurn, {
|
|
85
|
+
ctx.contribute(LoopChatTurn, {
|
|
86
|
+
id: 'chat-turn',
|
|
87
|
+
turn: feed.turn,
|
|
88
|
+
conversation: feed.conversation,
|
|
89
|
+
...feedWriters(feed),
|
|
90
|
+
}, { id: 'chat-turn' });
|
|
86
91
|
return {};
|
|
87
92
|
},
|
|
88
93
|
contributes: [
|
|
@@ -7,11 +7,17 @@
|
|
|
7
7
|
* previous turn), `assistant` as the reply arrives, `end` when the agent
|
|
8
8
|
* stops. Readers hold the signal.
|
|
9
9
|
*
|
|
10
|
+
* Beside the turn, the whole conversation (`conversation`), for a page that
|
|
11
|
+
* shows it as the Chat component does: the chat view hands it every change
|
|
12
|
+
* of its items, streaming included.
|
|
13
|
+
*
|
|
10
14
|
* @module loop/plugins/chat/turnState
|
|
11
15
|
*/
|
|
12
16
|
import { type ReadonlySignal } from '@datalayer/reactor';
|
|
13
17
|
import type { ContextSnapshotData } from '../../../types';
|
|
14
|
-
import type {
|
|
18
|
+
import type { DisplayItem } from '../../../types/chat';
|
|
19
|
+
import type { ChatMessage } from '../../../types/messages';
|
|
20
|
+
import type { ChatTurnSnapshot, ChatTurnStatus, ConversationEntry } from '../../core';
|
|
15
21
|
export type TurnFeed = {
|
|
16
22
|
/** The turn, for readers. */
|
|
17
23
|
turn: ReadonlySignal<ChatTurnSnapshot>;
|
|
@@ -28,7 +34,20 @@ export type TurnFeed = {
|
|
|
28
34
|
usage: (snapshot: ContextSnapshotData | undefined) => void;
|
|
29
35
|
/** What the agent is doing now, or nothing. */
|
|
30
36
|
activity: (label: string | undefined) => void;
|
|
37
|
+
/** The conversation, for readers. */
|
|
38
|
+
conversation: ReadonlySignal<ConversationEntry[]>;
|
|
39
|
+
/** The chat's items as they now stand, streaming included. */
|
|
40
|
+
items: (items: DisplayItem[]) => void;
|
|
31
41
|
};
|
|
42
|
+
/** A message's words, whether its content is a string or parts. */
|
|
43
|
+
export declare function messageText(message: ChatMessage): string;
|
|
44
|
+
/**
|
|
45
|
+
* The conversation as a page shows it: each person's and assistant's
|
|
46
|
+
* message with words in it, and each tool call by its name, its arguments
|
|
47
|
+
* and its result once it has one. A system message is the chat's own and is
|
|
48
|
+
* left out.
|
|
49
|
+
*/
|
|
50
|
+
export declare function conversationOf(items: DisplayItem[]): ConversationEntry[];
|
|
32
51
|
export declare function createTurnFeed(): TurnFeed;
|
|
33
52
|
/**
|
|
34
53
|
* The writers, as extra fields on the contribution.
|