@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.
Files changed (97) hide show
  1. package/lib/chat/assistant/AssistantStage.d.ts +9 -4
  2. package/lib/chat/assistant/AssistantStage.js +18 -6
  3. package/lib/chat/assistant/SpeechBalloon.js +16 -1
  4. package/lib/chat/assistant/SpriteCharacter.d.ts +25 -3
  5. package/lib/chat/assistant/SpriteCharacter.js +68 -3
  6. package/lib/chat/assistant/formats/acf.d.ts +56 -0
  7. package/lib/chat/assistant/formats/acf.js +317 -0
  8. package/lib/chat/assistant/formats/acs.d.ts +85 -7
  9. package/lib/chat/assistant/formats/acs.js +111 -39
  10. package/lib/chat/assistant/formats/files.d.ts +6 -2
  11. package/lib/chat/assistant/formats/files.js +18 -4
  12. package/lib/chat/assistant/formats/index.d.ts +6 -3
  13. package/lib/chat/assistant/formats/index.js +4 -2
  14. package/lib/chat/assistant/formats/types.d.ts +14 -0
  15. package/lib/chat/assistant/state.d.ts +2 -0
  16. package/lib/chat/assistant/state.js +9 -0
  17. package/lib/chat/base/ChatBase.js +47 -4
  18. package/lib/chat/base/loopStep.d.ts +40 -0
  19. package/lib/chat/base/loopStep.js +73 -0
  20. package/lib/chat/indicators/McpStatusIndicator.d.ts +1 -1
  21. package/lib/chat/indicators/McpStatusIndicator.js +13 -6
  22. package/lib/chat/indicators/SkillsStatusIndicator.d.ts +1 -1
  23. package/lib/chat/indicators/SkillsStatusIndicator.js +7 -3
  24. package/lib/chat/prompt/InputPrompt.d.ts +4 -1
  25. package/lib/chat/prompt/InputPrompt.js +2 -2
  26. package/lib/chat/prompt/InputPromptBase.d.ts +4 -1
  27. package/lib/chat/prompt/InputPromptBase.js +2 -2
  28. package/lib/chat/prompt/InputPromptLexical.d.ts +7 -1
  29. package/lib/chat/prompt/InputPromptLexical.js +5 -3
  30. package/lib/components/teams/index.d.ts +1 -0
  31. package/lib/components/teams/index.js +1 -0
  32. package/lib/components/teams/usePeerSandbox.d.ts +13 -0
  33. package/lib/components/teams/usePeerSandbox.js +59 -0
  34. package/lib/examples/AgentA2ATeamExample.js +7 -2
  35. package/lib/loop/apps/AppComposer.d.ts +31 -0
  36. package/lib/loop/apps/AppComposer.js +67 -0
  37. package/lib/loop/apps/AppRenderer.d.ts +16 -5
  38. package/lib/loop/apps/AppRenderer.js +49 -16
  39. package/lib/loop/apps/appspec.d.ts +8 -1
  40. package/lib/loop/apps/appspec.js +119 -2
  41. package/lib/loop/apps/checks.js +161 -1
  42. package/lib/loop/apps/composer.d.ts +37 -0
  43. package/lib/loop/apps/composer.js +61 -0
  44. package/lib/loop/apps/hostTools.d.ts +17 -0
  45. package/lib/loop/apps/hostTools.js +20 -0
  46. package/lib/loop/apps/index.d.ts +2 -0
  47. package/lib/loop/apps/index.js +2 -0
  48. package/lib/loop/apps/rules.d.ts +22 -1
  49. package/lib/loop/apps/rules.js +49 -1
  50. package/lib/loop/core/index.d.ts +31 -0
  51. package/lib/loop/core/index.js +22 -0
  52. package/lib/loop/embed/AppEmbed.d.ts +100 -6
  53. package/lib/loop/embed/AppEmbed.js +172 -27
  54. package/lib/loop/embed/LoopEmbed.js +2 -0
  55. package/lib/loop/embed/element.js +58 -5
  56. package/lib/loop/embed/embedConfig.d.ts +10 -1
  57. package/lib/loop/embed/embedConfig.js +12 -1
  58. package/lib/loop/embed/embedSession.d.ts +79 -0
  59. package/lib/loop/embed/embedSession.js +136 -0
  60. package/lib/loop/embed/embedTheme.d.ts +12 -9
  61. package/lib/loop/embed/embedTheme.js +32 -22
  62. package/lib/loop/embed/hostBridge.d.ts +79 -0
  63. package/lib/loop/embed/hostBridge.js +104 -0
  64. package/lib/loop/index.d.ts +5 -3
  65. package/lib/loop/index.js +6 -3
  66. package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.d.ts +5 -2
  67. package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.js +7 -1
  68. package/lib/loop/plugins/a2ui-surface/index.d.ts +1 -1
  69. package/lib/loop/plugins/a2ui-surface/index.js +4 -4
  70. package/lib/loop/plugins/a2ui-surface/toolResult.d.ts +27 -0
  71. package/lib/loop/plugins/a2ui-surface/toolResult.js +22 -0
  72. package/lib/loop/plugins/agent-capacity/index.d.ts +3 -1
  73. package/lib/loop/plugins/agent-capacity/index.js +2 -1
  74. package/lib/loop/plugins/chat/ChatView.js +31 -4
  75. package/lib/loop/plugins/chat/index.d.ts +6 -1
  76. package/lib/loop/presets.d.ts +3 -1
  77. package/lib/loop/presets.js +2 -1
  78. package/lib/loop/shell/LoopWorkspace.js +3 -0
  79. package/lib/protocols/BrowserAgentAdapter.d.ts +18 -0
  80. package/lib/protocols/BrowserAgentAdapter.js +35 -2
  81. package/lib/runtimes/__tests__/fixtures/accounting-agent-card.json +8 -0
  82. package/lib/runtimes/browser/a2aPeer.d.ts +11 -0
  83. package/lib/runtimes/browser/a2aPeer.js +15 -1
  84. package/lib/runtimes/browser/protocol.d.ts +3 -0
  85. package/lib/runtimes/browser/protocol.js +2 -1
  86. package/lib/specs/agents/agents.js +6 -25
  87. package/lib/specs/apps.js +50 -8
  88. package/lib/specs/appspecSchema.js +258 -0
  89. package/lib/types/agentspecs.d.ts +86 -0
  90. package/lib/types/chat.d.ts +29 -1
  91. package/package.json +1 -1
  92. package/scripts/codegen/generate_agents.py +4 -2
  93. package/scripts/codegen/generate_apps.py +7 -0
  94. package/scripts/codegen/generate_backend_tools.py +4 -12
  95. package/scripts/codegen/generate_cogs.py +3 -1
  96. package/scripts/codegen/generate_ui_plugins.py +22 -9
  97. package/scripts/voice/transcribe.mjs +29 -21
@@ -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
+ }
@@ -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';
@@ -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';
@@ -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. */
@@ -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. */
@@ -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. */
@@ -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. */