@datalayer/agent-runtimes 1.3.80 → 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/AssistantStage.d.ts +9 -4
- package/lib/chat/assistant/AssistantStage.js +18 -6
- package/lib/chat/assistant/SpeechBalloon.js +16 -1
- package/lib/chat/assistant/SpriteCharacter.d.ts +25 -3
- package/lib/chat/assistant/SpriteCharacter.js +68 -3
- package/lib/chat/assistant/formats/acf.d.ts +56 -0
- package/lib/chat/assistant/formats/acf.js +317 -0
- package/lib/chat/assistant/formats/acs.d.ts +85 -7
- package/lib/chat/assistant/formats/acs.js +111 -39
- package/lib/chat/assistant/formats/files.d.ts +6 -2
- package/lib/chat/assistant/formats/files.js +18 -4
- package/lib/chat/assistant/formats/index.d.ts +6 -3
- package/lib/chat/assistant/formats/index.js +4 -2
- package/lib/chat/assistant/formats/types.d.ts +14 -0
- package/lib/chat/assistant/state.d.ts +2 -0
- package/lib/chat/assistant/state.js +9 -0
- package/lib/chat/base/ChatBase.js +47 -4
- package/lib/chat/base/loopStep.d.ts +40 -0
- package/lib/chat/base/loopStep.js +73 -0
- 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 +119 -2
- package/lib/loop/apps/checks.js +161 -1
- package/lib/loop/apps/composer.d.ts +37 -0
- package/lib/loop/apps/composer.js +61 -0
- package/lib/loop/apps/hostTools.d.ts +17 -0
- package/lib/loop/apps/hostTools.js +20 -0
- package/lib/loop/apps/index.d.ts +2 -0
- package/lib/loop/apps/index.js +2 -0
- package/lib/loop/apps/rules.d.ts +22 -1
- package/lib/loop/apps/rules.js +49 -1
- package/lib/loop/core/index.d.ts +31 -0
- package/lib/loop/core/index.js +22 -0
- package/lib/loop/embed/AppEmbed.d.ts +100 -6
- package/lib/loop/embed/AppEmbed.js +172 -27
- package/lib/loop/embed/LoopEmbed.js +2 -0
- package/lib/loop/embed/element.js +58 -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/embed/hostBridge.d.ts +79 -0
- package/lib/loop/embed/hostBridge.js +104 -0
- package/lib/loop/index.d.ts +5 -3
- package/lib/loop/index.js +6 -3
- package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.d.ts +5 -2
- package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.js +7 -1
- package/lib/loop/plugins/a2ui-surface/index.d.ts +1 -1
- package/lib/loop/plugins/a2ui-surface/index.js +4 -4
- package/lib/loop/plugins/a2ui-surface/toolResult.d.ts +27 -0
- package/lib/loop/plugins/a2ui-surface/toolResult.js +22 -0
- 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/agents/agents.js +6 -25
- package/lib/specs/apps.js +50 -8
- package/lib/specs/appspecSchema.js +258 -0
- package/lib/types/agentspecs.d.ts +86 -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_apps.py +7 -0
- 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
package/lib/loop/apps/checks.js
CHANGED
|
@@ -39,8 +39,10 @@ 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';
|
|
45
|
+
import { HOST_NAME, hostToolsOf } from './hostTools';
|
|
44
46
|
export const NOT_READY = 'Not ready';
|
|
45
47
|
export const NEEDS_ATTENTION = 'Needs attention';
|
|
46
48
|
export const PASSES = 'Passes the instant checks';
|
|
@@ -110,6 +112,40 @@ function shapeProblems(app) {
|
|
|
110
112
|
else if (app.triggers.length > 0) {
|
|
111
113
|
problems.push(`A ${app.kind} application starts when somebody opens it: \`triggers\` are a worker's.`);
|
|
112
114
|
}
|
|
115
|
+
// What the host page passes and offers (D-10): named once each, in words
|
|
116
|
+
// its agent's tools can carry, each function saying what it does.
|
|
117
|
+
const host = app.deployment.embedded?.host;
|
|
118
|
+
if (host) {
|
|
119
|
+
for (const name of host.context) {
|
|
120
|
+
if (!HOST_NAME.test(name)) {
|
|
121
|
+
problems.push(`Cannot use “${name}” as a value of the host: lower-case letters, digits and \`_\`.`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (new Set(host.context).size !== host.context.length) {
|
|
125
|
+
problems.push('The host’s values are named once each.');
|
|
126
|
+
}
|
|
127
|
+
for (const fn of host.functions) {
|
|
128
|
+
if (!HOST_NAME.test(fn.name)) {
|
|
129
|
+
problems.push(`Cannot use “${fn.name}” as a host function: lower-case letters, digits and \`_\`.`);
|
|
130
|
+
}
|
|
131
|
+
if (!fn.description.trim()) {
|
|
132
|
+
problems.push(`The host function “${fn.name}” says what it does.`);
|
|
133
|
+
}
|
|
134
|
+
if (fn.parameters.type !== 'object') {
|
|
135
|
+
problems.push(`The host function “${fn.name}” takes its arguments as a JSON Schema of \`type: object\`.`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const names = host.functions.map(fn => fn.name);
|
|
139
|
+
if (new Set(names).size !== names.length) {
|
|
140
|
+
problems.push('The host’s functions are named once each.');
|
|
141
|
+
}
|
|
142
|
+
const named = new Set(app.rules.flatMap(rule => rule.appliesTo));
|
|
143
|
+
for (const tool of hostToolsOf(host)) {
|
|
144
|
+
if (!named.has(tool)) {
|
|
145
|
+
problems.push(`No rule names “${tool}”, which the host page offers it: it is left to the person until a rule decides it.`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
113
149
|
const servers = app.connections.map(connection => idOf(connection.server));
|
|
114
150
|
if (new Set(servers).size !== servers.length) {
|
|
115
151
|
problems.push('The application connects to the same server twice.');
|
|
@@ -234,6 +270,14 @@ function referenceProblems(app, organizationFrames) {
|
|
|
234
270
|
if (app.model && !getModel(app.model)) {
|
|
235
271
|
missing('model', app.model);
|
|
236
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
|
+
}
|
|
237
281
|
// The components it may use, and those its surface uses, are the catalog's (C-13).
|
|
238
282
|
for (const name of app.interface.components ?? []) {
|
|
239
283
|
if (!componentNamed(name)) {
|
|
@@ -274,12 +318,16 @@ function referenceProblems(app, organizationFrames) {
|
|
|
274
318
|
problems.push(`The application may write through “${connection.server}”, whose tools nobody has classed: every one of them is left to the person until they are.`);
|
|
275
319
|
}
|
|
276
320
|
}
|
|
321
|
+
// The tools of the host page are the application's own (D-10).
|
|
322
|
+
const hostTools = hostToolsOf(app.deployment.embedded?.host);
|
|
277
323
|
for (const rule of app.rules) {
|
|
278
324
|
for (const target of rule.appliesTo) {
|
|
279
325
|
if (CLASS_NAMES.has(target))
|
|
280
326
|
continue;
|
|
281
327
|
const [server, name] = splitRef(target);
|
|
282
328
|
if (server === undefined) {
|
|
329
|
+
if (hostTools.includes(target))
|
|
330
|
+
continue;
|
|
283
331
|
if (!getBackendToolSpec(name)) {
|
|
284
332
|
problems.push(`The rule “${rule.action}” names the tool “${target}”, which the catalogue does not have.`);
|
|
285
333
|
}
|
|
@@ -438,6 +486,8 @@ const ENUMS = {
|
|
|
438
486
|
visibility: ['private', 'invited', 'organization', 'link', 'public'],
|
|
439
487
|
mode: ['inline', 'bubble', 'panel', 'assistant'],
|
|
440
488
|
balloon: ['history', 'current'],
|
|
489
|
+
themeVariant: APP_THEME_VARIANTS,
|
|
490
|
+
themeMode: APP_THEME_MODES,
|
|
441
491
|
trigger: ['schedule', 'event', 'once'],
|
|
442
492
|
criterion: ['metric', 'noul', 'choice', 'score'],
|
|
443
493
|
direction: ['higher', 'lower'],
|
|
@@ -495,6 +545,13 @@ export function documentShapeProblems(document) {
|
|
|
495
545
|
if (item[key] === undefined)
|
|
496
546
|
at(`${where}.${key}`, 'is missing');
|
|
497
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
|
+
};
|
|
498
555
|
const d = document;
|
|
499
556
|
for (const key of [
|
|
500
557
|
'id',
|
|
@@ -566,6 +623,17 @@ export function documentShapeProblems(document) {
|
|
|
566
623
|
oneOf(ui.layout, ENUMS.layout, 'interface.layout');
|
|
567
624
|
oneOf(ui.accent, ENUMS.accent, 'interface.accent');
|
|
568
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
|
+
});
|
|
569
637
|
if (ui.assistant !== undefined && !isAssistantCharacterId(ui.assistant)) {
|
|
570
638
|
at('interface.assistant', 'names a character by the id a plugin contributes it under, lowercase words joined by a hyphen: `paperclip`, `acme-owl`');
|
|
571
639
|
}
|
|
@@ -575,6 +643,84 @@ export function documentShapeProblems(document) {
|
|
|
575
643
|
required(item, 'label', where);
|
|
576
644
|
required(item, 'message', where);
|
|
577
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
|
+
}
|
|
578
724
|
records(ui.settings, 'interface.settings', (item, where) => {
|
|
579
725
|
required(item, 'id', where);
|
|
580
726
|
required(item, 'type', where);
|
|
@@ -628,6 +774,10 @@ export function documentShapeProblems(document) {
|
|
|
628
774
|
!/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/.test(hosted.slug)))) {
|
|
629
775
|
at('deployment.hosted.slug', 'is lower-case letters, digits and hyphens');
|
|
630
776
|
}
|
|
777
|
+
if (hosted.character_alone !== undefined &&
|
|
778
|
+
typeof hosted.character_alone !== 'boolean') {
|
|
779
|
+
at('deployment.hosted.character_alone', 'is true or false');
|
|
780
|
+
}
|
|
631
781
|
});
|
|
632
782
|
mapping(deployment.embedded, 'deployment.embedded', embedded => {
|
|
633
783
|
oneOf(embedded.mode, ENUMS.mode, 'deployment.embedded.mode');
|
|
@@ -640,6 +790,16 @@ export function documentShapeProblems(document) {
|
|
|
640
790
|
}
|
|
641
791
|
});
|
|
642
792
|
}
|
|
793
|
+
mapping(embedded.host, 'deployment.embedded.host', host => {
|
|
794
|
+
texts(host.context, 'deployment.embedded.host.context');
|
|
795
|
+
records(host.functions, 'deployment.embedded.host.functions', (item, where) => {
|
|
796
|
+
required(item, 'name', where);
|
|
797
|
+
required(item, 'description', where);
|
|
798
|
+
text(item.name, `${where}.name`);
|
|
799
|
+
text(item.description, `${where}.description`);
|
|
800
|
+
mapping(item.parameters, `${where}.parameters`, () => undefined);
|
|
801
|
+
});
|
|
802
|
+
});
|
|
643
803
|
});
|
|
644
804
|
});
|
|
645
805
|
records(d.triggers, 'triggers', (item, 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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tools an embedded application is given for its host page (LOOP D-10),
|
|
3
|
+
* by name: `host_context` for the values the page passes, `host_<name>` for
|
|
4
|
+
* each function it offers. Apart from `embed/hostBridge`, which calls the
|
|
5
|
+
* page, so that the checks read them as a pure module.
|
|
6
|
+
*
|
|
7
|
+
* @module loop/apps/hostTools
|
|
8
|
+
*/
|
|
9
|
+
import type { AppHostBridgeSpec } from '../../types/agentspecs';
|
|
10
|
+
/** The tool its agent reads what the page passes it with. */
|
|
11
|
+
export declare const HOST_CONTEXT_TOOL = "host_context";
|
|
12
|
+
/** The tool its agent calls a function of the page with. */
|
|
13
|
+
export declare const hostTool: (name: string) => string;
|
|
14
|
+
/** How a value or a function of the host is named. */
|
|
15
|
+
export declare const HOST_NAME: RegExp;
|
|
16
|
+
/** The tools its agent is given for the host. */
|
|
17
|
+
export declare function hostToolsOf(bridge: AppHostBridgeSpec | undefined): string[];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/** The tool its agent reads what the page passes it with. */
|
|
6
|
+
export const HOST_CONTEXT_TOOL = 'host_context';
|
|
7
|
+
/** The tool its agent calls a function of the page with. */
|
|
8
|
+
export const hostTool = (name) => `host_${name}`;
|
|
9
|
+
/** How a value or a function of the host is named. */
|
|
10
|
+
export const HOST_NAME = /^[a-z][a-z0-9_]{0,62}$/;
|
|
11
|
+
/** The tools its agent is given for the host. */
|
|
12
|
+
export function hostToolsOf(bridge) {
|
|
13
|
+
if (!bridge) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
return [
|
|
17
|
+
...(bridge.context.length > 0 ? [HOST_CONTEXT_TOOL] : []),
|
|
18
|
+
...bridge.functions.map(fn => hostTool(fn.name)),
|
|
19
|
+
];
|
|
20
|
+
}
|
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';
|
package/lib/loop/apps/rules.d.ts
CHANGED
|
@@ -28,11 +28,32 @@ export declare function matchesPattern(name: string, pattern: string): boolean;
|
|
|
28
28
|
export declare const isComparable: (value: unknown) => boolean;
|
|
29
29
|
/** Whether the arguments of a call make a condition true. */
|
|
30
30
|
export declare function conditionHolds(condition: ActionConditionSpec, args: ToolArguments): boolean;
|
|
31
|
+
/** Where a call of a tool that forwards mail says what it forwards, from where, and to whom. */
|
|
32
|
+
export type Forwarding = {
|
|
33
|
+
/** The argument naming the message forwarded; a call without it does not forward. */
|
|
34
|
+
message: string;
|
|
35
|
+
/** The argument holding the address of the mailbox it sends from. */
|
|
36
|
+
mailbox: string;
|
|
37
|
+
/** The arguments holding its recipients. */
|
|
38
|
+
recipients: string[];
|
|
39
|
+
};
|
|
40
|
+
/** The tools that forward a message, by `server.tool` — as `agentspecs.actions.FORWARDING`. */
|
|
41
|
+
export declare const FORWARDING: Record<string, Forwarding>;
|
|
42
|
+
/** The mail addresses an argument holds — one, several separated by commas, or a list — in lower case. */
|
|
43
|
+
export declare function addresses(value: unknown): string[];
|
|
44
|
+
/**
|
|
45
|
+
* Whether a call forwards a message to somebody outside the organization
|
|
46
|
+
* (LOOP W-04): the domain of the mailbox it sends from. Fails closed: a
|
|
47
|
+
* forward that does not say its mailbox is outside. A reply is not a forward.
|
|
48
|
+
*/
|
|
49
|
+
export declare function forwardsOutside(server: string, name: string, args: ToolArguments): boolean;
|
|
31
50
|
/**
|
|
32
51
|
* The classes of a tool, by reference; empty when nobody classed it.
|
|
33
52
|
*
|
|
34
53
|
* With the arguments of a call, the classes of that call. Without them,
|
|
35
|
-
* everything the tool can do: nobody said what it is asked.
|
|
54
|
+
* everything the tool can do: nobody said what it is asked. A forward
|
|
55
|
+
* outside the organization is told from the call (`forwardsOutside`): it
|
|
56
|
+
* publishes besides sending.
|
|
36
57
|
*/
|
|
37
58
|
export declare function classesOf(ref: string, args?: ToolArguments): ActionClass[];
|
|
38
59
|
/** Whether a tool only reads. An unknown tool — no class — does not. */
|
package/lib/loop/apps/rules.js
CHANGED
|
@@ -129,11 +129,53 @@ function entryOf(server, name) {
|
|
|
129
129
|
}
|
|
130
130
|
return [[...actions.default], []];
|
|
131
131
|
}
|
|
132
|
+
/** The tools that forward a message, by `server.tool` — as `agentspecs.actions.FORWARDING`. */
|
|
133
|
+
export const FORWARDING = {
|
|
134
|
+
'google-workspace.send_gmail_message': {
|
|
135
|
+
message: 'forward_message_id',
|
|
136
|
+
mailbox: 'user_google_email',
|
|
137
|
+
recipients: ['to', 'cc', 'bcc'],
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
/** The mail addresses an argument holds — one, several separated by commas, or a list — in lower case. */
|
|
141
|
+
export function addresses(value) {
|
|
142
|
+
const items = Array.isArray(value) ? value : [value];
|
|
143
|
+
return items
|
|
144
|
+
.filter((item) => typeof item === 'string')
|
|
145
|
+
.flatMap(item => item.split(','))
|
|
146
|
+
.map(part => {
|
|
147
|
+
const angled = /<([^>]*)>/.exec(part);
|
|
148
|
+
return (angled ? angled[1] : part).trim().toLowerCase();
|
|
149
|
+
})
|
|
150
|
+
.filter(address => address.includes('@'));
|
|
151
|
+
}
|
|
152
|
+
const domainOf = (address) => address.slice(address.lastIndexOf('@') + 1).trim();
|
|
153
|
+
/**
|
|
154
|
+
* Whether a call forwards a message to somebody outside the organization
|
|
155
|
+
* (LOOP W-04): the domain of the mailbox it sends from. Fails closed: a
|
|
156
|
+
* forward that does not say its mailbox is outside. A reply is not a forward.
|
|
157
|
+
*/
|
|
158
|
+
export function forwardsOutside(server, name, args) {
|
|
159
|
+
const forwarding = own(FORWARDING, `${server}.${name}`);
|
|
160
|
+
const message = forwarding ? args[forwarding.message] : undefined;
|
|
161
|
+
if (!forwarding || typeof message !== 'string' || !message.trim()) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
const [home] = addresses(args[forwarding.mailbox]);
|
|
165
|
+
if (!home) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
return forwarding.recipients
|
|
169
|
+
.flatMap(field => addresses(args[field]))
|
|
170
|
+
.some(address => domainOf(address) !== domainOf(home));
|
|
171
|
+
}
|
|
132
172
|
/**
|
|
133
173
|
* The classes of a tool, by reference; empty when nobody classed it.
|
|
134
174
|
*
|
|
135
175
|
* With the arguments of a call, the classes of that call. Without them,
|
|
136
|
-
* everything the tool can do: nobody said what it is asked.
|
|
176
|
+
* everything the tool can do: nobody said what it is asked. A forward
|
|
177
|
+
* outside the organization is told from the call (`forwardsOutside`): it
|
|
178
|
+
* publishes besides sending.
|
|
137
179
|
*/
|
|
138
180
|
export function classesOf(ref, args) {
|
|
139
181
|
const [server, name] = splitRef(ref);
|
|
@@ -150,6 +192,12 @@ export function classesOf(ref, args) {
|
|
|
150
192
|
}
|
|
151
193
|
}
|
|
152
194
|
}
|
|
195
|
+
if (args !== undefined &&
|
|
196
|
+
classes.length > 0 &&
|
|
197
|
+
!classes.includes('publish') &&
|
|
198
|
+
forwardsOutside(server, name, args)) {
|
|
199
|
+
classes.push('publish');
|
|
200
|
+
}
|
|
153
201
|
return classes;
|
|
154
202
|
}
|
|
155
203
|
/** Whether a tool only reads. An unknown tool — no class — does not. */
|
package/lib/loop/core/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { ComponentType, ReactNode } from 'react';
|
|
|
3
3
|
import type { ExecutionState } from '@datalayer/jupyter-react/kernel-indicator';
|
|
4
4
|
import type { ToolbarItem } from '@datalayer/primer-addons';
|
|
5
5
|
import type { FrontendToolDefinition } from '../../types/tools';
|
|
6
|
+
import type { AppModeEffect } from '../../types/agentspecs';
|
|
6
7
|
import type { ContextSnapshotData } from '../../types/context';
|
|
7
8
|
import type { AssistantCharacter } from '../../chat/assistant/characters';
|
|
8
9
|
import type { AssistantCharacterData } from '../../chat/assistant/formats/types';
|
|
@@ -331,8 +332,25 @@ export type CommandContribution = {
|
|
|
331
332
|
*/
|
|
332
333
|
keybinding?: string;
|
|
333
334
|
args?: readonly CommandArgSpec[];
|
|
335
|
+
/**
|
|
336
|
+
* Listed in the composer's `/` menu, as an application's commands are
|
|
337
|
+
* (LOOP P-19). Most commands are not: the menu is for what a person sends,
|
|
338
|
+
* the palette and `/help` for the rest.
|
|
339
|
+
*/
|
|
340
|
+
composer?: boolean;
|
|
334
341
|
run: (ctx: LoopCommandContext) => Promise<CommandResult | void>;
|
|
335
342
|
};
|
|
343
|
+
/**
|
|
344
|
+
* What goes with every run besides the message, as AG-UI's `forwardedProps`:
|
|
345
|
+
* an application's modes say there which option the person is in (LOOP
|
|
346
|
+
* P-19). Read when a message is sent, so it says what is true then; what a
|
|
347
|
+
* page says with its own message (its action, its files) is merged over it,
|
|
348
|
+
* the `loop` part key by key.
|
|
349
|
+
*/
|
|
350
|
+
export type RunPropsContribution = {
|
|
351
|
+
id: string;
|
|
352
|
+
props: () => Record<string, unknown>;
|
|
353
|
+
};
|
|
336
354
|
/** A candidate offered by an `@` namespace. */
|
|
337
355
|
export type MentionCandidate = {
|
|
338
356
|
id: string;
|
|
@@ -496,6 +514,12 @@ export type AgentBlueprintContribution = {
|
|
|
496
514
|
instructions?: string;
|
|
497
515
|
/** The model it runs on in the page, in place of its spec's. */
|
|
498
516
|
model?: string;
|
|
517
|
+
/**
|
|
518
|
+
* What the modes chosen in the composer (`forwardedProps.loop.modes`, LOOP
|
|
519
|
+
* P-19) tell a run turned in the page, and the model it runs on — as the
|
|
520
|
+
* runtime's session API applies them. Unsaid for an agent with no modes.
|
|
521
|
+
*/
|
|
522
|
+
modeEffect?: (chosen: Record<string, string>) => AppModeEffect;
|
|
499
523
|
};
|
|
500
524
|
export declare const LoopAgentBlueprint: import("@datalayer/reactor").ContributionPoint<AgentBlueprintContribution>;
|
|
501
525
|
/** One conversation opener a plugin offers on the empty chat. */
|
|
@@ -781,6 +805,13 @@ export { onSurfaceRequest, requestSurface, type SurfaceRequestListener, } from '
|
|
|
781
805
|
export { focusPrompt, onPromptFocusRequest } from './focusRequests';
|
|
782
806
|
/** Slash commands, shared in shape with the CLI. */
|
|
783
807
|
export declare const LoopCommand: import("@datalayer/reactor").ContributionPoint<CommandContribution>;
|
|
808
|
+
/** What goes with every run besides the message (`forwardedProps`). */
|
|
809
|
+
export declare const LoopRunProps: import("@datalayer/reactor").ContributionPoint<RunPropsContribution>;
|
|
810
|
+
/**
|
|
811
|
+
* The `forwardedProps` of a run: what the contributions say, then what the
|
|
812
|
+
* sender gave, the `loop` part merged key by key.
|
|
813
|
+
*/
|
|
814
|
+
export declare function runForwardedProps(contributed: readonly RunPropsContribution[], given?: Record<string, unknown>): Record<string, unknown> | undefined;
|
|
784
815
|
/** `@` namespaces. */
|
|
785
816
|
export declare const LoopMention: import("@datalayer/reactor").ContributionPoint<MentionContribution>;
|
|
786
817
|
/** Slot names the shell renders. Slots render everything; points choose one. */
|
package/lib/loop/core/index.js
CHANGED
|
@@ -189,6 +189,28 @@ export { onSurfaceRequest, requestSurface, } from './surfaceRequests';
|
|
|
189
189
|
export { focusPrompt, onPromptFocusRequest } from './focusRequests';
|
|
190
190
|
/** Slash commands, shared in shape with the CLI. */
|
|
191
191
|
export const LoopCommand = defineContributionPoint('loop.command');
|
|
192
|
+
/** What goes with every run besides the message (`forwardedProps`). */
|
|
193
|
+
export const LoopRunProps = defineContributionPoint('loop.runProps');
|
|
194
|
+
/**
|
|
195
|
+
* The `forwardedProps` of a run: what the contributions say, then what the
|
|
196
|
+
* sender gave, the `loop` part merged key by key.
|
|
197
|
+
*/
|
|
198
|
+
export function runForwardedProps(contributed, given) {
|
|
199
|
+
let merged;
|
|
200
|
+
for (const part of [...contributed.map(entry => entry.props()), given]) {
|
|
201
|
+
if (!part)
|
|
202
|
+
continue;
|
|
203
|
+
const loop = {
|
|
204
|
+
...(merged?.loop ?? {}),
|
|
205
|
+
...(part.loop ?? {}),
|
|
206
|
+
};
|
|
207
|
+
merged = { ...merged, ...part };
|
|
208
|
+
if (Object.keys(loop).length > 0) {
|
|
209
|
+
merged.loop = loop;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return merged;
|
|
213
|
+
}
|
|
192
214
|
/** `@` namespaces. */
|
|
193
215
|
export const LoopMention = defineContributionPoint('loop.mention');
|
|
194
216
|
/** Slot names the shell renders. Slots render everything; points choose one. */
|