@datalayer/agent-runtimes 1.3.81 → 1.3.82
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/assistant/SpeechBalloon.js +16 -1
- package/lib/chat/base/ChatBase.js +40 -4
- package/lib/chat/indicators/McpStatusIndicator.d.ts +1 -1
- package/lib/chat/indicators/McpStatusIndicator.js +13 -6
- package/lib/chat/indicators/SkillsStatusIndicator.d.ts +1 -1
- package/lib/chat/indicators/SkillsStatusIndicator.js +7 -3
- package/lib/chat/prompt/InputPrompt.d.ts +4 -1
- package/lib/chat/prompt/InputPrompt.js +2 -2
- package/lib/chat/prompt/InputPromptBase.d.ts +4 -1
- package/lib/chat/prompt/InputPromptBase.js +2 -2
- package/lib/chat/prompt/InputPromptLexical.d.ts +7 -1
- package/lib/chat/prompt/InputPromptLexical.js +5 -3
- package/lib/components/teams/index.d.ts +1 -0
- package/lib/components/teams/index.js +1 -0
- package/lib/components/teams/usePeerSandbox.d.ts +13 -0
- package/lib/components/teams/usePeerSandbox.js +59 -0
- package/lib/examples/AgentA2ATeamExample.js +7 -2
- package/lib/loop/apps/AppComposer.d.ts +31 -0
- package/lib/loop/apps/AppComposer.js +67 -0
- package/lib/loop/apps/AppRenderer.d.ts +16 -5
- package/lib/loop/apps/AppRenderer.js +49 -16
- package/lib/loop/apps/appspec.d.ts +8 -1
- package/lib/loop/apps/appspec.js +87 -0
- package/lib/loop/apps/checks.js +108 -1
- package/lib/loop/apps/composer.d.ts +37 -0
- package/lib/loop/apps/composer.js +61 -0
- package/lib/loop/apps/index.d.ts +2 -0
- package/lib/loop/apps/index.js +2 -0
- package/lib/loop/core/index.d.ts +31 -0
- package/lib/loop/core/index.js +22 -0
- package/lib/loop/embed/AppEmbed.d.ts +46 -7
- package/lib/loop/embed/AppEmbed.js +108 -26
- package/lib/loop/embed/LoopEmbed.js +2 -0
- package/lib/loop/embed/element.js +19 -5
- package/lib/loop/embed/embedConfig.d.ts +10 -1
- package/lib/loop/embed/embedConfig.js +12 -1
- package/lib/loop/embed/embedSession.d.ts +79 -0
- package/lib/loop/embed/embedSession.js +136 -0
- package/lib/loop/embed/embedTheme.d.ts +12 -9
- package/lib/loop/embed/embedTheme.js +32 -22
- package/lib/loop/index.d.ts +4 -3
- package/lib/loop/index.js +4 -3
- package/lib/loop/plugins/agent-capacity/index.d.ts +3 -1
- package/lib/loop/plugins/agent-capacity/index.js +2 -1
- package/lib/loop/plugins/chat/ChatView.js +31 -4
- package/lib/loop/plugins/chat/index.d.ts +6 -1
- package/lib/loop/presets.d.ts +3 -1
- package/lib/loop/presets.js +2 -1
- package/lib/loop/shell/LoopWorkspace.js +3 -0
- package/lib/protocols/BrowserAgentAdapter.d.ts +18 -0
- package/lib/protocols/BrowserAgentAdapter.js +35 -2
- package/lib/runtimes/__tests__/fixtures/accounting-agent-card.json +8 -0
- package/lib/runtimes/browser/a2aPeer.d.ts +11 -0
- package/lib/runtimes/browser/a2aPeer.js +15 -1
- package/lib/runtimes/browser/protocol.d.ts +3 -0
- package/lib/runtimes/browser/protocol.js +2 -1
- package/lib/specs/apps.js +28 -0
- package/lib/specs/appspecSchema.js +191 -0
- package/lib/types/agentspecs.d.ts +58 -0
- package/lib/types/chat.d.ts +29 -1
- package/package.json +1 -1
- package/scripts/codegen/generate_agents.py +4 -2
- package/scripts/codegen/generate_backend_tools.py +4 -12
- package/scripts/codegen/generate_cogs.py +3 -1
- package/scripts/codegen/generate_ui_plugins.py +22 -9
- package/scripts/voice/transcribe.mjs +29 -21
|
@@ -108,6 +108,12 @@ export type AppRendererProps = Omit<LoopEmbedProps, 'agentId'> & {
|
|
|
108
108
|
* `instance.appUid`.
|
|
109
109
|
*/
|
|
110
110
|
sidebar?: boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Draw its computer alone beside its page (R-23): what a deployment's
|
|
113
|
+
* owner and the editors of its application see of it at its address — its
|
|
114
|
+
* runtime shows it to them only (decided 2026-10-06). Implied by `sidebar`.
|
|
115
|
+
*/
|
|
116
|
+
computer?: boolean;
|
|
111
117
|
/**
|
|
112
118
|
* Draw the application in a window (the `window-frame` plugin), its title
|
|
113
119
|
* the application's face and name. A host's own `frameTitle` wins.
|
|
@@ -157,11 +163,13 @@ export type AppPreset = {
|
|
|
157
163
|
* its page (`app-page`) when it has one, with the Canvas's block plugins
|
|
158
164
|
* but those of the UI plugins its organization turned off (`pluginsOff`),
|
|
159
165
|
* whose contributions are the blocks it may draw (R-01b); a thumb and a
|
|
160
|
-
* comment on each answer when its record keeps feedback (V-18);
|
|
166
|
+
* comment on each answer when its record keeps feedback (V-18); its
|
|
167
|
+
* commands in the composer's `/` menu and its modes beside it (P-19); what it
|
|
161
168
|
* keeps and for how long, under its prompt before the first message, when
|
|
162
169
|
* `kept` says it (R-31); with
|
|
163
170
|
* `sidebar`, its rules and approvals card, its activity feed and its
|
|
164
|
-
* computer (R-01b, R-23);
|
|
171
|
+
* computer (R-01b, R-23); with `computer`, its computer alone — at its
|
|
172
|
+
* address, for its owner and the editors of its application;
|
|
165
173
|
* laid out as `interface.layout` says;
|
|
166
174
|
* - a **decision**: the page its host draws (`page`), as the one view of a
|
|
167
175
|
* workspace without a conversation (R-02).
|
|
@@ -174,6 +182,8 @@ export declare function appPreset(app: AppSpec, options?: {
|
|
|
174
182
|
pluginsOff?: readonly string[];
|
|
175
183
|
/** Its rules and approvals, its activity and its computer, beside its page. */
|
|
176
184
|
sidebar?: boolean;
|
|
185
|
+
/** Its computer alone beside its page (R-23): for its owner and editors at its address. */
|
|
186
|
+
computer?: boolean;
|
|
177
187
|
/** What its record is kept under: its activity's. */
|
|
178
188
|
appUid?: string;
|
|
179
189
|
/**
|
|
@@ -207,8 +217,9 @@ export declare const DATALAYER_BOOTSTRAP_AGENTSPEC = "example-simple";
|
|
|
207
217
|
* The application's accent over the theme its conversation wears, in both
|
|
208
218
|
* modes (LOOP T-05): its bubbles and its one button in its own colour, and
|
|
209
219
|
* not in the theme's default — the chat sets its theme again inside it, so
|
|
210
|
-
* what the page around it set does not reach in.
|
|
220
|
+
* what the page around it set does not reach in. The accents are `loop`'s
|
|
221
|
+
* colours: over another theme (T-30) nothing is laid, the theme's own rule.
|
|
211
222
|
*/
|
|
212
|
-
export declare function appThemeOverrides(app: AppSpec): ThemeOverrides | undefined;
|
|
213
|
-
export declare function AppRenderer({ app, plugins, instance, onPresence, onSaying, paused, sidebar, frame, page, pluginsOff, ...embed }: AppRendererProps): React.JSX.Element;
|
|
223
|
+
export declare function appThemeOverrides(app: AppSpec, variant: string): ThemeOverrides | undefined;
|
|
224
|
+
export declare function AppRenderer({ app, plugins, instance, onPresence, onSaying, paused, sidebar, computer, frame, page, pluginsOff, ...embed }: AppRendererProps): React.JSX.Element;
|
|
214
225
|
export default AppRenderer;
|
|
@@ -45,7 +45,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
45
45
|
* @module loop/apps/AppRenderer
|
|
46
46
|
*/
|
|
47
47
|
import { useMemo } from 'react';
|
|
48
|
-
import { loopAccentStyles } from '@datalayer/primer-addons';
|
|
48
|
+
import { loopAccentStyles, useSystemColorMode, useThemeStore, } from '@datalayer/primer-addons';
|
|
49
49
|
import { defineAgentCapacityPlugin } from '../plugins/agent-capacity';
|
|
50
50
|
import { APP_PAGE_SURFACE, defineAppHostPagePlugin, defineAppPagePlugin, hasAppPage, } from '../plugins/app-page';
|
|
51
51
|
import { CANVAS_BLOCK_PLUGINS, canvasBlocksPluginName, } from '../plugins/canvas-blocks';
|
|
@@ -54,6 +54,8 @@ import { dumpAppspec } from './appspec';
|
|
|
54
54
|
import { defineAppActivityPlugin } from '../plugins/app-activity';
|
|
55
55
|
import { defineAppComputerPlugin } from '../plugins/app-computer';
|
|
56
56
|
import { defineAppRulesPlugin } from '../plugins/app-rules';
|
|
57
|
+
import { defineAppComposerPlugin } from './AppComposer';
|
|
58
|
+
import { modeEffect } from './composer';
|
|
57
59
|
import { defineAppFeedbackPlugin } from './AppFeedback';
|
|
58
60
|
import { defineAppKeptPlugin } from './AppKept';
|
|
59
61
|
import { keepsFeedback } from './feedback';
|
|
@@ -105,6 +107,14 @@ export function defineAppPlugin(app) {
|
|
|
105
107
|
// application's model when it names one (`loop/apps/agent`).
|
|
106
108
|
instructions: app.instructions,
|
|
107
109
|
model: app.model,
|
|
110
|
+
// Its modes (LOOP P-19), as the session API applies them: the
|
|
111
|
+
// instructions of the options chosen and the model one names, for the
|
|
112
|
+
// run they are sent with.
|
|
113
|
+
...((app.interface.modes ?? []).length > 0
|
|
114
|
+
? {
|
|
115
|
+
modeEffect: (chosen) => modeEffect(app, chosen),
|
|
116
|
+
}
|
|
117
|
+
: {}),
|
|
108
118
|
});
|
|
109
119
|
}
|
|
110
120
|
/**
|
|
@@ -146,11 +156,13 @@ export function appLayoutOptions(app) {
|
|
|
146
156
|
* its page (`app-page`) when it has one, with the Canvas's block plugins
|
|
147
157
|
* but those of the UI plugins its organization turned off (`pluginsOff`),
|
|
148
158
|
* whose contributions are the blocks it may draw (R-01b); a thumb and a
|
|
149
|
-
* comment on each answer when its record keeps feedback (V-18);
|
|
159
|
+
* comment on each answer when its record keeps feedback (V-18); its
|
|
160
|
+
* commands in the composer's `/` menu and its modes beside it (P-19); what it
|
|
150
161
|
* keeps and for how long, under its prompt before the first message, when
|
|
151
162
|
* `kept` says it (R-31); with
|
|
152
163
|
* `sidebar`, its rules and approvals card, its activity feed and its
|
|
153
|
-
* computer (R-01b, R-23);
|
|
164
|
+
* computer (R-01b, R-23); with `computer`, its computer alone — at its
|
|
165
|
+
* address, for its owner and the editors of its application;
|
|
154
166
|
* laid out as `interface.layout` says;
|
|
155
167
|
* - a **decision**: the page its host draws (`page`), as the one view of a
|
|
156
168
|
* workspace without a conversation (R-02).
|
|
@@ -180,6 +192,12 @@ export function appPreset(app, options = {}) {
|
|
|
180
192
|
plugins: [
|
|
181
193
|
defineAppPlugin(app),
|
|
182
194
|
...(withPage ? [defineAppPagePlugin(app), ...blocks] : []),
|
|
195
|
+
// Its commands in the composer's `/` menu and its modes beside the
|
|
196
|
+
// composer (LOOP P-19): only for an application that has some.
|
|
197
|
+
...((app.interface.commands ?? []).length > 0 ||
|
|
198
|
+
(app.interface.modes ?? []).length > 0
|
|
199
|
+
? [defineAppComposerPlugin(app)]
|
|
200
|
+
: []),
|
|
183
201
|
// A thumb and a comment on each answer, kept in its record (LOOP
|
|
184
202
|
// V-18): only for an application whose record keeps feedback.
|
|
185
203
|
...(keepsFeedback(app) ? [defineAppFeedbackPlugin(app)] : []),
|
|
@@ -196,7 +214,9 @@ export function appPreset(app, options = {}) {
|
|
|
196
214
|
// Its computer, live (R-23).
|
|
197
215
|
defineAppComputerPlugin(app),
|
|
198
216
|
]
|
|
199
|
-
:
|
|
217
|
+
: options.computer
|
|
218
|
+
? [defineAppComputerPlugin(app)]
|
|
219
|
+
: []),
|
|
200
220
|
],
|
|
201
221
|
workspace: appLayoutOptions(app),
|
|
202
222
|
};
|
|
@@ -249,18 +269,19 @@ const NO_PLUGINS = [];
|
|
|
249
269
|
* The application's accent over the theme its conversation wears, in both
|
|
250
270
|
* modes (LOOP T-05): its bubbles and its one button in its own colour, and
|
|
251
271
|
* not in the theme's default — the chat sets its theme again inside it, so
|
|
252
|
-
* what the page around it set does not reach in.
|
|
272
|
+
* what the page around it set does not reach in. The accents are `loop`'s
|
|
273
|
+
* colours: over another theme (T-30) nothing is laid, the theme's own rule.
|
|
253
274
|
*/
|
|
254
|
-
export function appThemeOverrides(app) {
|
|
275
|
+
export function appThemeOverrides(app, variant) {
|
|
255
276
|
const accent = app.interface?.accent;
|
|
256
|
-
return accent
|
|
277
|
+
return accent && variant === 'loop'
|
|
257
278
|
? {
|
|
258
279
|
light: loopAccentStyles(accent, 'light'),
|
|
259
280
|
dark: loopAccentStyles(accent, 'dark'),
|
|
260
281
|
}
|
|
261
282
|
: undefined;
|
|
262
283
|
}
|
|
263
|
-
export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, onSaying, paused = false, sidebar = false, frame = false, page, pluginsOff, ...embed }) {
|
|
284
|
+
export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, onSaying, paused = false, sidebar = false, computer = false, frame = false, page, pluginsOff, ...embed }) {
|
|
264
285
|
/*
|
|
265
286
|
* The kind's plugins, made once per application. `LoopEmbed` rebuilds its
|
|
266
287
|
* whole reactor when its plugins change, so a new plugin on every render
|
|
@@ -282,6 +303,7 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, o
|
|
|
282
303
|
page,
|
|
283
304
|
pluginsOff,
|
|
284
305
|
sidebar,
|
|
306
|
+
computer,
|
|
285
307
|
appUid: instance?.appUid,
|
|
286
308
|
...(deployed ? { kept: { visitor } } : {}),
|
|
287
309
|
});
|
|
@@ -293,7 +315,7 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, o
|
|
|
293
315
|
}
|
|
294
316
|
},
|
|
295
317
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
296
|
-
[source, page, off, sidebar, instance?.appUid, deployed, visitor]);
|
|
318
|
+
[source, page, off, sidebar, computer, instance?.appUid, deployed, visitor]);
|
|
297
319
|
const allPlugins = useMemo(() => ('problem' in preset ? plugins : [...preset.plugins, ...plugins]), [preset, plugins]);
|
|
298
320
|
/*
|
|
299
321
|
* On Datalayer, the application runs on a runtime: allocated with a plain
|
|
@@ -311,10 +333,24 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, o
|
|
|
311
333
|
instance?.deploymentUid,
|
|
312
334
|
instance?.version,
|
|
313
335
|
]);
|
|
336
|
+
/*
|
|
337
|
+
* The theme it runs in (T-30): the one its host says, else the one its
|
|
338
|
+
* Appspec names, else the page's — the person's own. Its mode likewise;
|
|
339
|
+
* `auto` is the device's.
|
|
340
|
+
*/
|
|
341
|
+
const pageTheme = useThemeStore(state => state.theme);
|
|
342
|
+
const system = useSystemColorMode();
|
|
343
|
+
const named = app.interface?.theme;
|
|
344
|
+
const worn = embed.themeVariant ?? named?.variant ?? pageTheme;
|
|
345
|
+
const namedMode = named?.mode === 'auto'
|
|
346
|
+
? system
|
|
347
|
+
: named?.mode === 'light' || named?.mode === 'dark'
|
|
348
|
+
? named.mode
|
|
349
|
+
: undefined;
|
|
314
350
|
const accent = app.interface?.accent;
|
|
315
|
-
const themeOverrides = useMemo(() => appThemeOverrides(app),
|
|
351
|
+
const themeOverrides = useMemo(() => appThemeOverrides(app, worn),
|
|
316
352
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
317
|
-
[accent]);
|
|
353
|
+
[accent, worn]);
|
|
318
354
|
if ('problem' in preset) {
|
|
319
355
|
return (_jsx("div", { role: "status", style: { padding: 16 }, children: preset.problem }));
|
|
320
356
|
}
|
|
@@ -330,13 +366,10 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, o
|
|
|
330
366
|
}
|
|
331
367
|
: {}),
|
|
332
368
|
// Its rules, activity and computer on a rail, one at a time (T-07).
|
|
333
|
-
sidebarRail: sidebar, teamPicker: false, showAgentVariants: false, graph: false, pluginsPanel: false, datalayerAgentSpecId: DATALAYER_BOOTSTRAP_AGENTSPEC,
|
|
369
|
+
sidebarRail: sidebar || computer, teamPicker: false, showAgentVariants: false, graph: false, pluginsPanel: false, datalayerAgentSpecId: DATALAYER_BOOTSTRAP_AGENTSPEC,
|
|
334
370
|
// Where an application runs is decided by its host — the Studio's
|
|
335
371
|
// Preview, the hosted page — not offered to its user.
|
|
336
|
-
targetFixed: true,
|
|
337
|
-
// Applications first (LOOP T-12): an application's conversation wears
|
|
338
|
-
// the `loop` theme; a host may say otherwise.
|
|
339
|
-
themeVariant: "loop", themeOverrides: themeOverrides,
|
|
372
|
+
targetFixed: true, ...(named ? { themeVariant: named.variant } : {}), ...(namedMode ? { colorMode: namedMode } : {}), themeOverrides: themeOverrides,
|
|
340
373
|
// Its own face, name and welcome in the chat (T-08); no counters: a
|
|
341
374
|
// person using an application is not asking about tokens.
|
|
342
375
|
presence: {
|
|
@@ -24,13 +24,20 @@
|
|
|
24
24
|
*
|
|
25
25
|
* @module loop/apps/appspec
|
|
26
26
|
*/
|
|
27
|
-
import type { AppAccent, AppAssistantCharacter, AppEmbedMode, AppVoiceSpec, AppKind, AppLayout, AppSpec } from '../../types/agentspecs';
|
|
27
|
+
import type { AppAccent, AppThemeMode, AppThemeVariant, AppAssistantCharacter, AppEmbedMode, AppVoiceSpec, AppKind, AppLayout, AppSpec } from '../../types/agentspecs';
|
|
28
28
|
/** The version of the spec itself. */
|
|
29
29
|
export declare const APP_SCHEMA = "loop.app/v1";
|
|
30
30
|
/** The face of an application that has not chosen one. */
|
|
31
31
|
export declare const DEFAULT_EMOJI = "\uD83D\uDC40";
|
|
32
32
|
export declare const APP_KINDS: AppKind[];
|
|
33
33
|
export declare const APP_ACCENTS: AppAccent[];
|
|
34
|
+
/**
|
|
35
|
+
* The themes an application may run in (LOOP T-30): Appearance's, as
|
|
36
|
+
* primer-addons lists them (`themeVariants`), in its order.
|
|
37
|
+
*/
|
|
38
|
+
export declare const APP_THEME_VARIANTS: AppThemeVariant[];
|
|
39
|
+
/** The colour modes a theme is worn in. */
|
|
40
|
+
export declare const APP_THEME_MODES: AppThemeMode[];
|
|
34
41
|
/** The four ways an application sits in another product's page (LOOP D-07). */
|
|
35
42
|
export declare const APP_EMBED_MODES: AppEmbedMode[];
|
|
36
43
|
/**
|
package/lib/loop/apps/appspec.js
CHANGED
|
@@ -16,6 +16,23 @@ export const APP_ACCENTS = [
|
|
|
16
16
|
'sun',
|
|
17
17
|
'violet',
|
|
18
18
|
];
|
|
19
|
+
/**
|
|
20
|
+
* The themes an application may run in (LOOP T-30): Appearance's, as
|
|
21
|
+
* primer-addons lists them (`themeVariants`), in its order.
|
|
22
|
+
*/
|
|
23
|
+
export const APP_THEME_VARIANTS = [
|
|
24
|
+
'datalayer',
|
|
25
|
+
'spatial',
|
|
26
|
+
'lovely',
|
|
27
|
+
'matrix',
|
|
28
|
+
'earth',
|
|
29
|
+
'sand',
|
|
30
|
+
'ivory',
|
|
31
|
+
'sun',
|
|
32
|
+
'loop',
|
|
33
|
+
];
|
|
34
|
+
/** The colour modes a theme is worn in. */
|
|
35
|
+
export const APP_THEME_MODES = ['light', 'dark', 'auto'];
|
|
19
36
|
/** The four ways an application sits in another product's page (LOOP D-07). */
|
|
20
37
|
export const APP_EMBED_MODES = [
|
|
21
38
|
'inline',
|
|
@@ -105,6 +122,8 @@ export function emptyAppspec(kind = 'chat') {
|
|
|
105
122
|
accent: 'green',
|
|
106
123
|
welcome: '',
|
|
107
124
|
starters: [],
|
|
125
|
+
commands: [],
|
|
126
|
+
modes: [],
|
|
108
127
|
settings: [],
|
|
109
128
|
components: [],
|
|
110
129
|
outputs: [],
|
|
@@ -190,6 +209,39 @@ function parseRule(data) {
|
|
|
190
209
|
behaviour: oneOf(data.behaviour, BEHAVIOURS, 'ask_first'),
|
|
191
210
|
};
|
|
192
211
|
}
|
|
212
|
+
/** A slash command (LOOP P-19); a wrong one is said by the checks. */
|
|
213
|
+
function parseCommand(data) {
|
|
214
|
+
return {
|
|
215
|
+
name: text(data.name),
|
|
216
|
+
description: text(data.description),
|
|
217
|
+
prompt: text(data.prompt),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
/** One position of a mode switch, what it does said only when said. */
|
|
221
|
+
function parseModeOption(data) {
|
|
222
|
+
const option = {
|
|
223
|
+
id: text(data.id),
|
|
224
|
+
label: text(data.label),
|
|
225
|
+
};
|
|
226
|
+
for (const key of ['description', 'instructions', 'model']) {
|
|
227
|
+
if (typeof data[key] === 'string' && data[key]) {
|
|
228
|
+
option[key] = data[key];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return option;
|
|
232
|
+
}
|
|
233
|
+
/** A mode switch (LOOP P-19). */
|
|
234
|
+
function parseMode(data) {
|
|
235
|
+
const mode = {
|
|
236
|
+
id: text(data.id),
|
|
237
|
+
label: text(data.label),
|
|
238
|
+
options: records(data.options).map(parseModeOption),
|
|
239
|
+
};
|
|
240
|
+
if (typeof data.default === 'string' && data.default) {
|
|
241
|
+
mode.default = data.default;
|
|
242
|
+
}
|
|
243
|
+
return mode;
|
|
244
|
+
}
|
|
193
245
|
function parseSetting(data) {
|
|
194
246
|
const setting = {
|
|
195
247
|
id: text(data.id),
|
|
@@ -246,6 +298,8 @@ function parseInterface(data, kind) {
|
|
|
246
298
|
label: text(starter.label),
|
|
247
299
|
message: text(starter.message),
|
|
248
300
|
})),
|
|
301
|
+
commands: records(data.commands).map(parseCommand),
|
|
302
|
+
modes: records(data.modes).map(parseMode),
|
|
249
303
|
settings: records(data.settings).map(parseSetting),
|
|
250
304
|
components: texts(data.components),
|
|
251
305
|
voice: parseVoice(data.voice),
|
|
@@ -260,6 +314,14 @@ function parseInterface(data, kind) {
|
|
|
260
314
|
if (data.balloon === 'history' || data.balloon === 'current') {
|
|
261
315
|
parsed.balloon = data.balloon;
|
|
262
316
|
}
|
|
317
|
+
// A theme of Appearance's, or none: an unknown one is said by the checks.
|
|
318
|
+
if (isData(data.theme) &&
|
|
319
|
+
APP_THEME_VARIANTS.includes(data.theme.variant)) {
|
|
320
|
+
parsed.theme = { variant: data.theme.variant };
|
|
321
|
+
if (APP_THEME_MODES.includes(data.theme.mode)) {
|
|
322
|
+
parsed.theme.mode = data.theme.mode;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
263
325
|
return parsed;
|
|
264
326
|
}
|
|
265
327
|
function parseCriterion(data) {
|
|
@@ -516,6 +578,26 @@ function dumpInterface(spec, kind) {
|
|
|
516
578
|
label: starter.label,
|
|
517
579
|
message: starter.message,
|
|
518
580
|
})))
|
|
581
|
+
.list('commands', spec.commands.map(command => ({
|
|
582
|
+
name: command.name,
|
|
583
|
+
description: command.description,
|
|
584
|
+
prompt: command.prompt,
|
|
585
|
+
})))
|
|
586
|
+
.list('modes', spec.modes.map(mode => {
|
|
587
|
+
const written = new Writer()
|
|
588
|
+
.text('id', mode.id, '\u0000')
|
|
589
|
+
.text('label', mode.label, '\u0000')
|
|
590
|
+
.list('options', mode.options.map(option => new Writer()
|
|
591
|
+
.text('id', option.id, '\u0000')
|
|
592
|
+
.text('label', option.label, '\u0000')
|
|
593
|
+
.text('description', option.description ?? '')
|
|
594
|
+
.text('instructions', option.instructions ?? '')
|
|
595
|
+
.text('model', option.model ?? '').data));
|
|
596
|
+
if (mode.default) {
|
|
597
|
+
written.data.default = mode.default;
|
|
598
|
+
}
|
|
599
|
+
return written.data;
|
|
600
|
+
}))
|
|
519
601
|
.list('settings', spec.settings.map(setting => {
|
|
520
602
|
const written = new Writer()
|
|
521
603
|
.text('id', setting.id, '\u0000')
|
|
@@ -539,6 +621,11 @@ function dumpInterface(spec, kind) {
|
|
|
539
621
|
if (spec.balloon) {
|
|
540
622
|
writer.data.balloon = spec.balloon;
|
|
541
623
|
}
|
|
624
|
+
if (spec.theme) {
|
|
625
|
+
writer.data.theme = spec.theme.mode
|
|
626
|
+
? { variant: spec.theme.variant, mode: spec.theme.mode }
|
|
627
|
+
: { variant: spec.theme.variant };
|
|
628
|
+
}
|
|
542
629
|
// Its voice, when it says one (VO-41): a spec made before voice has none.
|
|
543
630
|
if (spec.voice) {
|
|
544
631
|
writer.part('voice', new Writer()
|
package/lib/loop/apps/checks.js
CHANGED
|
@@ -39,8 +39,9 @@ import { getTeamSpec } from '../../specs/teams';
|
|
|
39
39
|
import { getBackendToolSpec } from '../../specs/backendTools';
|
|
40
40
|
import { getTrack } from '../../specs/tracks';
|
|
41
41
|
import { pluginsOffSetupNotes } from '../plugins/canvas-blocks';
|
|
42
|
-
import { isAssistantCharacterId, parseAppspec } from './appspec';
|
|
42
|
+
import { APP_THEME_MODES, APP_THEME_VARIANTS, isAssistantCharacterId, parseAppspec, } from './appspec';
|
|
43
43
|
import { classesOf, splitRef, toolBehaviours } from './rules';
|
|
44
|
+
import { COMMAND_INPUT, COMMAND_NAME, MODE_ID } from './composer';
|
|
44
45
|
import { HOST_NAME, hostToolsOf } from './hostTools';
|
|
45
46
|
export const NOT_READY = 'Not ready';
|
|
46
47
|
export const NEEDS_ATTENTION = 'Needs attention';
|
|
@@ -269,6 +270,14 @@ function referenceProblems(app, organizationFrames) {
|
|
|
269
270
|
if (app.model && !getModel(app.model)) {
|
|
270
271
|
missing('model', app.model);
|
|
271
272
|
}
|
|
273
|
+
// The model a mode runs on is the catalogue's (LOOP P-19).
|
|
274
|
+
for (const mode of app.interface.modes ?? []) {
|
|
275
|
+
for (const option of mode.options) {
|
|
276
|
+
if (option.model && !getModel(option.model)) {
|
|
277
|
+
problems.push(`The mode “${mode.id}” runs “${option.id}” on “${option.model}”, which is no model.`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
272
281
|
// The components it may use, and those its surface uses, are the catalog's (C-13).
|
|
273
282
|
for (const name of app.interface.components ?? []) {
|
|
274
283
|
if (!componentNamed(name)) {
|
|
@@ -477,6 +486,8 @@ const ENUMS = {
|
|
|
477
486
|
visibility: ['private', 'invited', 'organization', 'link', 'public'],
|
|
478
487
|
mode: ['inline', 'bubble', 'panel', 'assistant'],
|
|
479
488
|
balloon: ['history', 'current'],
|
|
489
|
+
themeVariant: APP_THEME_VARIANTS,
|
|
490
|
+
themeMode: APP_THEME_MODES,
|
|
480
491
|
trigger: ['schedule', 'event', 'once'],
|
|
481
492
|
criterion: ['metric', 'noul', 'choice', 'score'],
|
|
482
493
|
direction: ['higher', 'lower'],
|
|
@@ -534,6 +545,13 @@ export function documentShapeProblems(document) {
|
|
|
534
545
|
if (item[key] === undefined)
|
|
535
546
|
at(`${where}.${key}`, 'is missing');
|
|
536
547
|
};
|
|
548
|
+
const unknownKeys = (item, known, where, what) => {
|
|
549
|
+
for (const key of Object.keys(item)) {
|
|
550
|
+
if (!known.includes(key)) {
|
|
551
|
+
at(`${where}.${key}`, `is not a key of ${what}: ${known.join(', ')}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
};
|
|
537
555
|
const d = document;
|
|
538
556
|
for (const key of [
|
|
539
557
|
'id',
|
|
@@ -605,6 +623,17 @@ export function documentShapeProblems(document) {
|
|
|
605
623
|
oneOf(ui.layout, ENUMS.layout, 'interface.layout');
|
|
606
624
|
oneOf(ui.accent, ENUMS.accent, 'interface.accent');
|
|
607
625
|
oneOf(ui.balloon, ENUMS.balloon, 'interface.balloon');
|
|
626
|
+
// The theme it runs in by default (T-30): a theme of Appearance's, a mode.
|
|
627
|
+
mapping(ui.theme, 'interface.theme', theme => {
|
|
628
|
+
required(theme, 'variant', 'interface.theme');
|
|
629
|
+
oneOf(theme.variant, ENUMS.themeVariant, 'interface.theme.variant');
|
|
630
|
+
oneOf(theme.mode, ENUMS.themeMode, 'interface.theme.mode');
|
|
631
|
+
for (const key of Object.keys(theme)) {
|
|
632
|
+
if (key !== 'variant' && key !== 'mode') {
|
|
633
|
+
at(`interface.theme.${key}`, 'is not a key of a theme: variant, mode');
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
});
|
|
608
637
|
if (ui.assistant !== undefined && !isAssistantCharacterId(ui.assistant)) {
|
|
609
638
|
at('interface.assistant', 'names a character by the id a plugin contributes it under, lowercase words joined by a hyphen: `paperclip`, `acme-owl`');
|
|
610
639
|
}
|
|
@@ -614,6 +643,84 @@ export function documentShapeProblems(document) {
|
|
|
614
643
|
required(item, 'label', where);
|
|
615
644
|
required(item, 'message', where);
|
|
616
645
|
});
|
|
646
|
+
// Its commands and modes in the composer (LOOP P-19), as agentspecs
|
|
647
|
+
// refuses them: names and ids slash-safe and said once, no key unknown.
|
|
648
|
+
const named = new Set();
|
|
649
|
+
records(ui.commands, 'interface.commands', (item, where) => {
|
|
650
|
+
for (const key of ['name', 'description', 'prompt']) {
|
|
651
|
+
required(item, key, where);
|
|
652
|
+
text(item[key], `${where}.${key}`);
|
|
653
|
+
}
|
|
654
|
+
unknownKeys(item, ['name', 'description', 'prompt'], where, 'a command');
|
|
655
|
+
if (typeof item.name === 'string') {
|
|
656
|
+
if (!COMMAND_NAME.test(item.name)) {
|
|
657
|
+
at(`${where}.name`, 'is what follows the slash: lower-case letters, digits and hyphens, a letter first');
|
|
658
|
+
}
|
|
659
|
+
if (named.has(item.name)) {
|
|
660
|
+
at(`${where}.name`, `names /${item.name} a second time`);
|
|
661
|
+
}
|
|
662
|
+
named.add(item.name);
|
|
663
|
+
}
|
|
664
|
+
if (typeof item.prompt === 'string') {
|
|
665
|
+
for (const placeholder of item.prompt.match(/\{[^{}]*\}/g) ?? []) {
|
|
666
|
+
if (placeholder !== COMMAND_INPUT) {
|
|
667
|
+
at(`${where}.prompt`, `takes the words typed after the command as ${COMMAND_INPUT}, not ${placeholder}`);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
const modeIds = new Set();
|
|
673
|
+
const choosingModel = [];
|
|
674
|
+
records(ui.modes, 'interface.modes', (mode, where) => {
|
|
675
|
+
required(mode, 'id', where);
|
|
676
|
+
required(mode, 'label', where);
|
|
677
|
+
required(mode, 'options', where);
|
|
678
|
+
text(mode.label, `${where}.label`);
|
|
679
|
+
text(mode.default, `${where}.default`);
|
|
680
|
+
unknownKeys(mode, ['id', 'label', 'options', 'default'], where, 'a mode');
|
|
681
|
+
if (typeof mode.id === 'string') {
|
|
682
|
+
if (!MODE_ID.test(mode.id)) {
|
|
683
|
+
at(`${where}.id`, 'is lower-case letters, digits, `_` and `-`, a letter first');
|
|
684
|
+
}
|
|
685
|
+
if (modeIds.has(mode.id)) {
|
|
686
|
+
at(`${where}.id`, `names the mode ${mode.id} a second time`);
|
|
687
|
+
}
|
|
688
|
+
modeIds.add(mode.id);
|
|
689
|
+
}
|
|
690
|
+
const optionIds = [];
|
|
691
|
+
let model = false;
|
|
692
|
+
records(mode.options, `${where}.options`, (option, place) => {
|
|
693
|
+
required(option, 'id', place);
|
|
694
|
+
required(option, 'label', place);
|
|
695
|
+
for (const key of ['label', 'description', 'instructions', 'model']) {
|
|
696
|
+
text(option[key], `${place}.${key}`);
|
|
697
|
+
}
|
|
698
|
+
unknownKeys(option, ['id', 'label', 'description', 'instructions', 'model'], place, 'an option');
|
|
699
|
+
if (typeof option.id === 'string') {
|
|
700
|
+
if (!MODE_ID.test(option.id)) {
|
|
701
|
+
at(`${place}.id`, 'is lower-case letters, digits, `_` and `-`, a letter first');
|
|
702
|
+
}
|
|
703
|
+
if (optionIds.includes(option.id)) {
|
|
704
|
+
at(`${place}.id`, `names the option ${option.id} a second time`);
|
|
705
|
+
}
|
|
706
|
+
optionIds.push(option.id);
|
|
707
|
+
}
|
|
708
|
+
model ||= typeof option.model === 'string' && option.model !== '';
|
|
709
|
+
});
|
|
710
|
+
if (Array.isArray(mode.options) && mode.options.length < 2) {
|
|
711
|
+
at(`${where}.options`, 'are two at least');
|
|
712
|
+
}
|
|
713
|
+
if (typeof mode.default === 'string' &&
|
|
714
|
+
!optionIds.includes(mode.default)) {
|
|
715
|
+
at(`${where}.default`, 'is one of its options');
|
|
716
|
+
}
|
|
717
|
+
if (model) {
|
|
718
|
+
choosingModel.push(String(mode.id));
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
if (choosingModel.length > 1) {
|
|
722
|
+
at('interface.modes', `let only one mode choose the model, not ${choosingModel.join(', ')}`);
|
|
723
|
+
}
|
|
617
724
|
records(ui.settings, 'interface.settings', (item, where) => {
|
|
618
725
|
required(item, 'id', where);
|
|
619
726
|
required(item, 'type', where);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commands and modes in the composer (LOOP P-19), as rules: what a command
|
|
3
|
+
* sends, and which option of each mode a run is in. agentspecs' own
|
|
4
|
+
* (`command_prompt`, `AppInterface.mode_choice`, `mode_effect`), said again
|
|
5
|
+
* for the page.
|
|
6
|
+
*
|
|
7
|
+
* Pure: no React, no network.
|
|
8
|
+
*
|
|
9
|
+
* @module loop/apps/composer
|
|
10
|
+
*/
|
|
11
|
+
import type { AppCommandSpec, AppModeEffect, AppSpec } from '../../types/agentspecs';
|
|
12
|
+
/** Where a command's prompt takes the words typed after it. */
|
|
13
|
+
export declare const COMMAND_INPUT = "{input}";
|
|
14
|
+
/** What follows the slash: lower-case letters, digits and hyphens, a letter first. */
|
|
15
|
+
export declare const COMMAND_NAME: RegExp;
|
|
16
|
+
/** The id of a mode or of one of its options. */
|
|
17
|
+
export declare const MODE_ID: RegExp;
|
|
18
|
+
/**
|
|
19
|
+
* What a command sends: its prompt, the words typed after it in place of
|
|
20
|
+
* `{input}` — or after the prompt, when it has no `{input}`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function commandPrompt(command: Pick<AppCommandSpec, 'prompt'>, words?: string): string;
|
|
23
|
+
/** The option each mode starts on: its default, else its first. */
|
|
24
|
+
export declare function modeDefaults(app: Pick<AppSpec, 'interface'>): Record<string, string>;
|
|
25
|
+
/**
|
|
26
|
+
* The option of every mode a run is in: what was chosen, else where each
|
|
27
|
+
* starts. A mode or an option the application does not have is refused, in a
|
|
28
|
+
* sentence, as the runtime refuses it.
|
|
29
|
+
*/
|
|
30
|
+
export declare function modeChoice(app: Pick<AppSpec, 'name' | 'interface'>, chosen?: Record<string, string>): Record<string, string>;
|
|
31
|
+
/**
|
|
32
|
+
* What a run in the modes chosen is told, and the model it runs on —
|
|
33
|
+
* agentspecs' `mode_effect`: the instructions of every option chosen, in the
|
|
34
|
+
* order of the modes, and the model of the first that names one. Refused as
|
|
35
|
+
* {@link modeChoice} refuses.
|
|
36
|
+
*/
|
|
37
|
+
export declare function modeEffect(app: Pick<AppSpec, 'name' | 'interface'>, chosen?: Record<string, string>): AppModeEffect;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/** Where a command's prompt takes the words typed after it. */
|
|
6
|
+
export const COMMAND_INPUT = '{input}';
|
|
7
|
+
/** What follows the slash: lower-case letters, digits and hyphens, a letter first. */
|
|
8
|
+
export const COMMAND_NAME = /^[a-z](?:[a-z0-9-]{0,30}[a-z0-9])?$/;
|
|
9
|
+
/** The id of a mode or of one of its options. */
|
|
10
|
+
export const MODE_ID = /^[a-z][a-z0-9_-]{0,39}$/;
|
|
11
|
+
/**
|
|
12
|
+
* What a command sends: its prompt, the words typed after it in place of
|
|
13
|
+
* `{input}` — or after the prompt, when it has no `{input}`.
|
|
14
|
+
*/
|
|
15
|
+
export function commandPrompt(command, words = '') {
|
|
16
|
+
const said = words.trim();
|
|
17
|
+
if (command.prompt.includes(COMMAND_INPUT)) {
|
|
18
|
+
return command.prompt.split(COMMAND_INPUT).join(said).trim();
|
|
19
|
+
}
|
|
20
|
+
return said ? `${command.prompt}\n\n${said}` : command.prompt;
|
|
21
|
+
}
|
|
22
|
+
/** The option each mode starts on: its default, else its first. */
|
|
23
|
+
export function modeDefaults(app) {
|
|
24
|
+
return Object.fromEntries((app.interface.modes ?? [])
|
|
25
|
+
.filter(mode => mode.options.length > 0)
|
|
26
|
+
.map(mode => [mode.id, mode.default || mode.options[0].id]));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The option of every mode a run is in: what was chosen, else where each
|
|
30
|
+
* starts. A mode or an option the application does not have is refused, in a
|
|
31
|
+
* sentence, as the runtime refuses it.
|
|
32
|
+
*/
|
|
33
|
+
export function modeChoice(app, chosen = {}) {
|
|
34
|
+
const modes = app.interface.modes ?? [];
|
|
35
|
+
for (const [modeId, optionId] of Object.entries(chosen)) {
|
|
36
|
+
const mode = modes.find(candidate => candidate.id === modeId);
|
|
37
|
+
if (!mode) {
|
|
38
|
+
throw new Error(`${app.name} has no mode “${modeId}”.`);
|
|
39
|
+
}
|
|
40
|
+
if (!mode.options.some(option => option.id === optionId)) {
|
|
41
|
+
throw new Error(`The mode ${mode.label} has no option “${optionId}”.`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { ...modeDefaults(app), ...chosen };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* What a run in the modes chosen is told, and the model it runs on —
|
|
48
|
+
* agentspecs' `mode_effect`: the instructions of every option chosen, in the
|
|
49
|
+
* order of the modes, and the model of the first that names one. Refused as
|
|
50
|
+
* {@link modeChoice} refuses.
|
|
51
|
+
*/
|
|
52
|
+
export function modeEffect(app, chosen = {}) {
|
|
53
|
+
const choice = modeChoice(app, chosen);
|
|
54
|
+
const options = (app.interface.modes ?? []).flatMap(mode => mode.options.filter(option => option.id === choice[mode.id]));
|
|
55
|
+
const instructions = options
|
|
56
|
+
.map(option => (option.instructions ?? '').trim())
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.join('\n\n');
|
|
59
|
+
const model = options.find(option => option.model)?.model ?? undefined;
|
|
60
|
+
return model ? { instructions, model } : { instructions };
|
|
61
|
+
}
|
package/lib/loop/apps/index.d.ts
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
* @module loop/apps
|
|
5
5
|
*/
|
|
6
6
|
export * from './agent';
|
|
7
|
+
export * from './AppComposer';
|
|
7
8
|
export * from './AppFeedback';
|
|
8
9
|
export * from './AppKept';
|
|
9
10
|
export * from './AppRenderer';
|
|
10
11
|
export * from './appspec';
|
|
11
12
|
export * from './checks';
|
|
13
|
+
export * from './composer';
|
|
12
14
|
export * from './computer';
|
|
13
15
|
export * from './feedback';
|
|
14
16
|
export * from './kept';
|
package/lib/loop/apps/index.js
CHANGED
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
* @module loop/apps
|
|
9
9
|
*/
|
|
10
10
|
export * from './agent';
|
|
11
|
+
export * from './AppComposer';
|
|
11
12
|
export * from './AppFeedback';
|
|
12
13
|
export * from './AppKept';
|
|
13
14
|
export * from './AppRenderer';
|
|
14
15
|
export * from './appspec';
|
|
15
16
|
export * from './checks';
|
|
17
|
+
export * from './composer';
|
|
16
18
|
export * from './computer';
|
|
17
19
|
export * from './feedback';
|
|
18
20
|
export * from './kept';
|