@datalayer/agent-runtimes 1.3.84 → 1.3.85

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 (141) hide show
  1. package/lib/chat/ChatFloating.d.ts +5 -4
  2. package/lib/chat/ChatFloating.js +8 -6
  3. package/lib/chat/ChatLanguage.d.ts +17 -0
  4. package/lib/chat/ChatLanguage.js +29 -0
  5. package/lib/chat/assistant/AssistantStage.d.ts +4 -4
  6. package/lib/chat/assistant/AssistantStage.js +4 -2
  7. package/lib/chat/assistant/BalloonParts.d.ts +2 -1
  8. package/lib/chat/assistant/BalloonParts.js +12 -5
  9. package/lib/chat/assistant/BalloonVisual.js +3 -1
  10. package/lib/chat/assistant/ConversationBalloon.d.ts +1 -1
  11. package/lib/chat/assistant/ConversationBalloon.js +3 -2
  12. package/lib/chat/assistant/SpeechBalloon.d.ts +1 -1
  13. package/lib/chat/assistant/SpeechBalloon.js +10 -7
  14. package/lib/chat/assistant/toolLine.d.ts +8 -4
  15. package/lib/chat/assistant/toolLine.js +13 -9
  16. package/lib/chat/base/ChatBase.js +14 -11
  17. package/lib/chat/display/EmptyState.js +3 -1
  18. package/lib/chat/index.d.ts +2 -0
  19. package/lib/chat/index.js +3 -0
  20. package/lib/chat/messages/ChatMessages.js +3 -1
  21. package/lib/chat/messages/TurnFooter.js +3 -1
  22. package/lib/chat/prompt/InputPrompt.js +3 -1
  23. package/lib/chat/prompt/InputPromptLexical.js +3 -1
  24. package/lib/chat/prompt/footer/InPromptFooter.js +3 -1
  25. package/lib/chat/tools/ToolApprovalBanner.js +5 -3
  26. package/lib/chat/tools/ToolApprovalDialog.js +5 -3
  27. package/lib/chat/tools/ToolCallDisplay.js +29 -25
  28. package/lib/chat/words.d.ts +102 -0
  29. package/lib/chat/words.js +460 -0
  30. package/lib/components/a2ui/custom/index.d.ts +39 -0
  31. package/lib/components/a2ui/custom/index.js +95 -0
  32. package/lib/components/a2ui/custom/sandbox.d.ts +73 -0
  33. package/lib/components/a2ui/custom/sandbox.js +188 -0
  34. package/lib/components/a2ui/datalayer/index.d.ts +1 -1
  35. package/lib/components/a2ui/datalayer/index.js +1 -1
  36. package/lib/components/a2ui/datalayer/schema.d.ts +10 -0
  37. package/lib/components/a2ui/datalayer/schema.js +12 -1
  38. package/lib/components/a2ui/index.d.ts +1 -0
  39. package/lib/components/a2ui/index.js +1 -0
  40. package/lib/components/a2ui/visibleWhen.d.ts +12 -1
  41. package/lib/components/a2ui/visibleWhen.js +24 -3
  42. package/lib/components/inspector/AgentInspector.d.ts +25 -44
  43. package/lib/components/inspector/AgentInspector.js +27 -215
  44. package/lib/components/inspector/AgentInspectorDialog.d.ts +6 -6
  45. package/lib/components/inspector/AgentInspectorDialog.js +2 -2
  46. package/lib/components/inspector/a2aSpans.d.ts +51 -0
  47. package/lib/components/inspector/a2aSpans.js +373 -0
  48. package/lib/components/inspector/agentSpans.d.ts +120 -0
  49. package/lib/components/inspector/agentSpans.js +253 -0
  50. package/lib/components/inspector/chatSpans.d.ts +36 -0
  51. package/lib/components/inspector/chatSpans.js +50 -0
  52. package/lib/components/inspector/classify.d.ts +11 -8
  53. package/lib/components/inspector/classify.js +23 -24
  54. package/lib/components/inspector/index.d.ts +7 -7
  55. package/lib/components/inspector/index.js +7 -7
  56. package/lib/components/teams/A2ATeamGraph.d.ts +4 -4
  57. package/lib/components/teams/SceneTranscript.d.ts +29 -0
  58. package/lib/components/teams/SceneTranscript.js +54 -0
  59. package/lib/components/teams/SceneView.d.ts +41 -0
  60. package/lib/components/teams/SceneView.js +28 -0
  61. package/lib/components/teams/index.d.ts +3 -0
  62. package/lib/components/teams/index.js +3 -0
  63. package/lib/components/teams/sceneTranscript.d.ts +74 -0
  64. package/lib/components/teams/sceneTranscript.js +294 -0
  65. package/lib/components/teams/teamConnections.d.ts +2 -2
  66. package/lib/components/teams/teamConnections.js +3 -3
  67. package/lib/components/teams/useA2ATeam.d.ts +4 -4
  68. package/lib/components/teams/useA2ATeam.js +50 -46
  69. package/lib/examples/AgentA2ATeamExample.d.ts +10 -9
  70. package/lib/examples/AgentA2ATeamExample.js +70 -60
  71. package/lib/examples/AssistantExample.js +2 -2
  72. package/lib/loop/apps/AppComposer.d.ts +2 -1
  73. package/lib/loop/apps/AppComposer.js +6 -3
  74. package/lib/loop/apps/AppRenderer.d.ts +2 -1
  75. package/lib/loop/apps/AppRenderer.js +34 -25
  76. package/lib/loop/apps/appspec.d.ts +11 -1
  77. package/lib/loop/apps/appspec.js +115 -1
  78. package/lib/loop/apps/checks.d.ts +8 -0
  79. package/lib/loop/apps/checks.js +106 -4
  80. package/lib/loop/apps/customComponents.d.ts +41 -0
  81. package/lib/loop/apps/customComponents.js +260 -0
  82. package/lib/loop/apps/hostTools.d.ts +8 -1
  83. package/lib/loop/apps/hostTools.js +9 -0
  84. package/lib/loop/apps/index.d.ts +1 -0
  85. package/lib/loop/apps/index.js +1 -0
  86. package/lib/loop/core/a2uiComponents.d.ts +10 -0
  87. package/lib/loop/core/a2uiComponents.js +15 -0
  88. package/lib/loop/core/index.d.ts +1 -0
  89. package/lib/loop/core/index.js +3 -0
  90. package/lib/loop/embed/AppEmbed.d.ts +11 -3
  91. package/lib/loop/embed/AppEmbed.js +42 -16
  92. package/lib/loop/embed/element.d.ts +5 -0
  93. package/lib/loop/embed/element.js +61 -5
  94. package/lib/loop/embed/embedConfig.d.ts +8 -0
  95. package/lib/loop/embed/embedConfig.js +18 -0
  96. package/lib/loop/embed/hostBridge.d.ts +28 -2
  97. package/lib/loop/embed/hostBridge.js +22 -2
  98. package/lib/loop/index.d.ts +4 -3
  99. package/lib/loop/index.js +4 -3
  100. package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +7 -1
  101. package/lib/loop/plugins/a2ui-surface/InlineSurface.js +7 -2
  102. package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.js +4 -1
  103. package/lib/loop/plugins/a2ui-surface/ownComponents.d.ts +6 -0
  104. package/lib/loop/plugins/a2ui-surface/ownComponents.js +30 -0
  105. package/lib/loop/plugins/app-components/index.d.ts +26 -0
  106. package/lib/loop/plugins/app-components/index.js +56 -0
  107. package/lib/loop/plugins/app-elements/AppElements.js +4 -1
  108. package/lib/loop/plugins/app-page/AppPage.d.ts +9 -0
  109. package/lib/loop/plugins/app-page/AppPage.js +106 -8
  110. package/lib/loop/plugins/app-page/appPageModel.d.ts +17 -1
  111. package/lib/loop/plugins/app-page/appPageModel.js +120 -4
  112. package/lib/loop/plugins/app-page/index.d.ts +1 -0
  113. package/lib/loop/plugins/app-page/index.js +1 -0
  114. package/lib/loop/plugins/app-page/pageRun.d.ts +46 -0
  115. package/lib/loop/plugins/app-page/pageRun.js +111 -0
  116. package/lib/loop/plugins/assistant/index.js +8 -6
  117. package/lib/loop/plugins/chat/ChatView.js +6 -4
  118. package/lib/loop/plugins/chat/index.d.ts +1 -1
  119. package/lib/loop/plugins/chat/index.js +2 -1
  120. package/lib/models/PageTag.d.ts +1 -1
  121. package/lib/runtimes/browser/a2aPeer.d.ts +2 -1
  122. package/lib/specs/actions.js +78 -1
  123. package/lib/specs/apps.d.ts +5 -0
  124. package/lib/specs/apps.js +1384 -231
  125. package/lib/specs/appspecSchema.js +191 -1
  126. package/lib/specs/teams/teams.d.ts +3 -0
  127. package/lib/specs/teams/teams.js +249 -0
  128. package/lib/types/agentspecs.d.ts +70 -0
  129. package/lib/types/chat.d.ts +6 -5
  130. package/lib/types/teams.d.ts +13 -2
  131. package/package.json +2 -2
  132. package/scripts/codegen/generate_apps.py +4 -2
  133. package/scripts/codegen/generate_teams.py +15 -2
  134. package/lib/components/inspector/JsonTree.d.ts +0 -24
  135. package/lib/components/inspector/JsonTree.js +0 -89
  136. package/lib/components/inspector/a2aInspect.d.ts +0 -56
  137. package/lib/components/inspector/a2aInspect.js +0 -478
  138. package/lib/components/inspector/agentInspector.d.ts +0 -179
  139. package/lib/components/inspector/agentInspector.js +0 -267
  140. package/lib/components/inspector/chatInspect.d.ts +0 -38
  141. package/lib/components/inspector/chatInspect.js +0 -65
@@ -39,12 +39,13 @@ 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 { APP_THEME_MODES, APP_THEME_VARIANTS, isAssistantCharacterId, parseAppspec, } from './appspec';
42
+ import { APP_THEME_MODES, APP_THEME_VARIANTS, isAssistantCharacterId, PAGE_OUTPUT_SHOWS, parseAppspec, } from './appspec';
43
43
  import { classesOf, splitRef, toolBehaviours } from './rules';
44
44
  import { COMMAND_INPUT, COMMAND_NAME, MODE_ID } from './composer';
45
- import { HOST_NAME, hostToolsOf } from './hostTools';
45
+ import { HOST_NAME, HOST_USERS, hostToolsOf } from './hostTools';
46
46
  import { MAX_UPLOAD_MB, UPLOAD_KIND } from './uploads';
47
47
  import { formUiProblems } from './settingsInputs';
48
+ import { customComponentOf, customComponentsProblems, customNodeRefused, customPropsRefused, } from './customComponents';
48
49
  import { translationProblems } from './language';
49
50
  export const NOT_READY = 'Not ready';
50
51
  export const NEEDS_ATTENTION = 'Needs attention';
@@ -129,6 +130,82 @@ export function ownCodeProblems(app) {
129
130
  if (new Set(decided).size !== decided.length) {
130
131
  problems.push('Two tests are decided by the same function of its code.');
131
132
  }
133
+ problems.push(...pageProblems(app));
134
+ // The components its developer wrote, reviewed as the catalog's (P-17).
135
+ problems.push(...customComponentsProblems(app));
136
+ return problems;
137
+ }
138
+ /**
139
+ * What agentspecs refuses of a widget's page written in its code (LOOP
140
+ * P-05): a page that is not a widget's, inputs that are not a form, outputs
141
+ * of one name twice, drawn with another component, saying in their `props`
142
+ * what their value fills or lacking what their component needs, and an input
143
+ * named as a setting — both are on the page at `/inputs/<name>`.
144
+ */
145
+ export function pageProblems(app) {
146
+ const page = app.interface.page;
147
+ if (!page) {
148
+ return [];
149
+ }
150
+ const problems = [];
151
+ if (app.kind !== 'widget') {
152
+ problems.push(`A page of inputs and outputs is a widget's: a ${app.kind} application has none. Remove \`interface.page\`, or make it a widget.`);
153
+ }
154
+ if (!CODE_NAME.test(page.function)) {
155
+ problems.push(`Cannot use “${page.function}” as the function of its page: letters, digits and \`_\`, as in Python.`);
156
+ }
157
+ problems.push(...formProblems({
158
+ id: 'page inputs',
159
+ schema: page.inputs,
160
+ ui: page.inputsUi,
161
+ }));
162
+ if (page.outputs.length === 0) {
163
+ problems.push('Its page shows one output at least.');
164
+ }
165
+ const names = page.outputs.map(output => output.name);
166
+ if (new Set(names).size !== names.length) {
167
+ problems.push('Two outputs of its page have the same name.');
168
+ }
169
+ for (const output of page.outputs) {
170
+ if (!CODE_NAME.test(output.name)) {
171
+ problems.push(`Cannot use “${output.name}” as an output's name: letters, digits and \`_\`, as in Python.`);
172
+ }
173
+ // A component its developer wrote (P-17): its value what it shows first.
174
+ const custom = output.component in PAGE_OUTPUT_SHOWS
175
+ ? undefined
176
+ : customComponentOf(app, output.component);
177
+ if (custom) {
178
+ if (custom.shows.length === 0) {
179
+ problems.push(`The output “${output.name}” is drawn with ${output.component}, which shows nothing: its value is what it shows first, under \`shows\`.`);
180
+ continue;
181
+ }
182
+ const refused = customPropsRefused(custom, output.props);
183
+ if (refused.length > 0) {
184
+ problems.push(`The output “${output.name}” is a ${output.component} its schema refuses: ${refused.join('; ')}.`);
185
+ }
186
+ continue;
187
+ }
188
+ const shows = PAGE_OUTPUT_SHOWS[output.component];
189
+ if (!shows) {
190
+ problems.push(`The output “${output.name}” is drawn with ${output.component}: an output is one of ${Object.keys(PAGE_OUTPUT_SHOWS).join(', ')} or a component of its own (\`custom_components\`).`);
191
+ continue;
192
+ }
193
+ const said = [shows, 'id', 'component'].filter(key => key in output.props);
194
+ if (said.length > 0) {
195
+ problems.push(`The output “${output.name}” says ${said.join(', ')} in its \`props\`: its id is its name, and what it shows is its value.`);
196
+ }
197
+ const schema = getComponent(output.component)?.properties;
198
+ const missing = (schema?.required ?? []).filter(key => key !== shows && !(key in output.props));
199
+ if (missing.length > 0) {
200
+ problems.push(`The output “${output.name}” is a ${output.component} without ${missing.join(', ')}: say it in its \`props\`.`);
201
+ }
202
+ }
203
+ const settings = Object.keys(app.interface.settings?.properties ?? {});
204
+ const inputs = Object.keys(page.inputs?.properties ?? {});
205
+ const shared = settings.filter(name => inputs.includes(name));
206
+ if (shared.length > 0) {
207
+ problems.push(`${shared.map(name => `“${name}”`).join(', ')} is both a setting and an input of its page: name one otherwise.`);
208
+ }
132
209
  return problems;
133
210
  }
134
211
  /** What the spec says of itself that the tolerant reader lets through. */
@@ -228,6 +305,10 @@ function shapeProblems(app) {
228
305
  if (new Set(names).size !== names.length) {
229
306
  problems.push('The host’s functions are named once each.');
230
307
  }
308
+ // Who its user is (D-21): what the page says, or what its server signed.
309
+ if (host.user !== undefined && !HOST_USERS.includes(host.user)) {
310
+ problems.push(`“${host.user}” is not how the host’s user is taken: claimed or signed.`);
311
+ }
231
312
  const named = new Set(app.rules.flatMap(rule => rule.appliesTo));
232
313
  for (const tool of hostToolsOf(host)) {
233
314
  if (!named.has(tool)) {
@@ -380,13 +461,21 @@ function referenceProblems(app, organizationFrames) {
380
461
  }
381
462
  }
382
463
  // The components it may use, and those its surface uses, are the catalog's (C-13).
464
+ // Those its developer wrote are of the catalog for it alone (P-17).
383
465
  for (const name of app.interface.components ?? []) {
384
- if (!componentNamed(name)) {
466
+ if (!componentNamed(name) && !customComponentOf(app, name)) {
385
467
  problems.push(`There is no component named “${name}” in the catalog.`);
386
468
  }
387
469
  }
388
470
  for (const node of app.interface.surface?.components ?? []) {
389
- if (!componentNamed(String(node.component))) {
471
+ const custom = customComponentOf(app, String(node.component));
472
+ if (custom) {
473
+ const refused = customNodeRefused(custom, node);
474
+ if (refused.length > 0) {
475
+ problems.push(`The surface's “${String(node.id)}” is a ${custom.name} its schema refuses: ${refused.join('; ')}.`);
476
+ }
477
+ }
478
+ else if (!componentNamed(String(node.component))) {
390
479
  problems.push(`The surface's “${String(node.id)}” is a “${String(node.component)}”, which the catalog does not have.`);
391
480
  }
392
481
  else if (node.component === 'Form') {
@@ -912,6 +1001,19 @@ export function documentShapeProblems(document) {
912
1001
  required(item, 'id', where);
913
1002
  required(item, 'component', where);
914
1003
  });
1004
+ }); // A widget's page written in its code (LOOP P-05), as agentspecs reads it.
1005
+ mapping(ui.page, 'interface.page', page => {
1006
+ unknownKeys(page, ['function', 'inputs', 'inputs_ui', 'outputs', 'live'], 'interface.page', 'a page');
1007
+ required(page, 'function', 'interface.page');
1008
+ required(page, 'inputs', 'interface.page');
1009
+ required(page, 'outputs', 'interface.page');
1010
+ if (page.live !== undefined && typeof page.live !== 'boolean') {
1011
+ at('interface.page.live', 'is true or false');
1012
+ }
1013
+ records(page.outputs, 'interface.page.outputs', (output, where) => {
1014
+ required(output, 'name', where);
1015
+ unknownKeys(output, ['name', 'title', 'component', 'props'], where, 'an output');
1016
+ });
915
1017
  });
916
1018
  });
917
1019
  mapping(d.tests, 'tests', tests => {
@@ -0,0 +1,41 @@
1
+ import type { AppCustomComponentSpec, AppSpec, ComponentSpec } from '../../types/agentspecs';
2
+ /** Its name: a word starting with a capital letter, as the catalog's are named. */
3
+ export declare const CUSTOM_COMPONENT_NAME: RegExp;
4
+ /** What one of its properties may be typed: what the renderer tells apart. */
5
+ export declare const CUSTOM_PROPERTY_TYPES: readonly ["string", "integer", "number", "boolean", "array", "object"];
6
+ /** What every component of a surface already has, and a custom one cannot declare. */
7
+ export declare const CUSTOM_RESERVED_NAMES: readonly ["id", "component", "action", "weight", "visible_when", "children", "child"];
8
+ /**
9
+ * Where its module is loaded from: an address served over HTTPS, or this
10
+ * machine while it is written. A file of the application's folder waits for
11
+ * its packaging (P-29).
12
+ */
13
+ export declare const CUSTOM_SOURCE: RegExp;
14
+ /** A Subresource Integrity hash: the module as it was reviewed. */
15
+ export declare const INTEGRITY: RegExp;
16
+ /**
17
+ * What its schema refuses of the properties given, in the words agentspecs
18
+ * says them: one it does not have, a value of another type, one it needs.
19
+ * `bound` are given elsewhere — bound to what the application publishes.
20
+ */
21
+ export declare function customPropsRefused(component: AppCustomComponentSpec, props: Record<string, unknown>, bound?: readonly string[]): string[];
22
+ /** What stops one component its developer wrote from being reviewed, in sentences. */
23
+ export declare function customComponentProblems(component: AppCustomComponentSpec): string[];
24
+ /** What stops the components an application's developer wrote, in sentences. */
25
+ export declare function customComponentsProblems(app: AppSpec): string[];
26
+ /** A component its developer wrote, by name, or undefined. */
27
+ export declare function customComponentOf(app: Pick<AppSpec, 'interface'>, name: string): AppCustomComponentSpec | undefined;
28
+ /**
29
+ * It as the catalog lists a component (C-13): what the palette, the
30
+ * components page and the checks read — under the application's version, its
31
+ * category `custom`.
32
+ */
33
+ export declare function customComponentEntry(component: AppCustomComponentSpec, version: string): ComponentSpec;
34
+ /**
35
+ * The components an application may place: the catalog's own, given, and
36
+ * those its developer wrote, for it alone. A name of its own that the
37
+ * catalog has is not listed twice: the checks refuse it.
38
+ */
39
+ export declare function appComponents(app: Pick<AppSpec, 'interface' | 'version'>, catalog: readonly ComponentSpec[]): ComponentSpec[];
40
+ /** What its schema refuses of a node of a surface drawn by it, in sentences; empty when nothing. */
41
+ export declare function customNodeRefused(component: AppCustomComponentSpec, node: Record<string, unknown>): string[];
@@ -0,0 +1,260 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * Components a developer writes (LOOP P-17): an A2UI component of one
7
+ * application only, declared in its Appspec (`interface.custom_components`)
8
+ * with its name, its properties as a JSON Schema, what it shows and sends,
9
+ * and the address of the built ES module that draws it.
10
+ *
11
+ * Reviewed like any component of the catalog — what agentspecs refuses is
12
+ * refused here in the same sentences — and listed beside the catalog's for
13
+ * that application alone (`appComponents`): the catalog grows for it, it
14
+ * does not open. How it is drawn, in a sandboxed frame of no origin, is
15
+ * `components/a2ui/custom`.
16
+ *
17
+ * @module loop/apps/customComponents
18
+ */
19
+ import { getComponent } from '../../specs/uiPlugins';
20
+ /** Its name: a word starting with a capital letter, as the catalog's are named. */
21
+ export const CUSTOM_COMPONENT_NAME = /^[A-Z][A-Za-z0-9]{0,63}$/;
22
+ /** What one of its properties may be typed: what the renderer tells apart. */
23
+ export const CUSTOM_PROPERTY_TYPES = [
24
+ 'string',
25
+ 'integer',
26
+ 'number',
27
+ 'boolean',
28
+ 'array',
29
+ 'object',
30
+ ];
31
+ /** What every component of a surface already has, and a custom one cannot declare. */
32
+ export const CUSTOM_RESERVED_NAMES = [
33
+ 'id',
34
+ 'component',
35
+ 'action',
36
+ 'weight',
37
+ 'visible_when',
38
+ 'children',
39
+ 'child',
40
+ ];
41
+ /**
42
+ * Where its module is loaded from: an address served over HTTPS, or this
43
+ * machine while it is written. A file of the application's folder waits for
44
+ * its packaging (P-29).
45
+ */
46
+ export const CUSTOM_SOURCE = /^(?:https:\/\/[A-Za-z0-9.-]+(?::\d+)?|http:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?)\/\S*$/;
47
+ /** A Subresource Integrity hash: the module as it was reviewed. */
48
+ export const INTEGRITY = /^sha(?:256|384|512)-[A-Za-z0-9+/]+={0,2}$/;
49
+ const PROPERTY_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
50
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
51
+ const isBound = (value) => isRecord(value) && 'path' in value;
52
+ const quoted = (name) => `'${name}'`;
53
+ /** Why a value is not one a property takes, or null. */
54
+ function valueRefused(name, field, value) {
55
+ if (Array.isArray(field.enum)) {
56
+ return field.enum.includes(value)
57
+ ? null
58
+ : `${quoted(name)} is one of ${field.enum.map(each => quoted(String(each))).join(', ')}`;
59
+ }
60
+ const kind = String(field.type);
61
+ const takes = {
62
+ string: v => typeof v === 'string',
63
+ integer: v => typeof v === 'number' && Number.isInteger(v),
64
+ number: v => typeof v === 'number',
65
+ boolean: v => typeof v === 'boolean',
66
+ array: v => Array.isArray(v),
67
+ object: isRecord,
68
+ };
69
+ if (typeof value === 'boolean' && kind !== 'boolean') {
70
+ return `${quoted(name)} is ${kind}, not true or false`;
71
+ }
72
+ return takes[kind]?.(value) ? null : `${quoted(name)} is ${kind}`;
73
+ }
74
+ /** Its properties, as its schema declares them. */
75
+ const fieldsOf = (component) => (isRecord(component.props.properties)
76
+ ? component.props.properties
77
+ : {});
78
+ /**
79
+ * What its schema refuses of the properties given, in the words agentspecs
80
+ * says them: one it does not have, a value of another type, one it needs.
81
+ * `bound` are given elsewhere — bound to what the application publishes.
82
+ */
83
+ export function customPropsRefused(component, props, bound = []) {
84
+ const fields = fieldsOf(component);
85
+ const problems = Object.keys(props)
86
+ .filter(name => !(name in fields))
87
+ .map(name => `it has no property ${quoted(name)}`);
88
+ for (const [name, value] of Object.entries(props)) {
89
+ if (name in fields && !isBound(value)) {
90
+ const refused = valueRefused(name, fields[name], value);
91
+ if (refused) {
92
+ problems.push(refused);
93
+ }
94
+ }
95
+ }
96
+ const required = Array.isArray(component.props.required)
97
+ ? component.props.required
98
+ : [];
99
+ for (const name of required) {
100
+ if (!(name in props) && !bound.includes(name)) {
101
+ problems.push(`it needs ${quoted(name)}`);
102
+ }
103
+ }
104
+ return problems;
105
+ }
106
+ /** What stops one component its developer wrote from being reviewed, in sentences. */
107
+ export function customComponentProblems(component) {
108
+ const said = `The component ${component.name || '(unnamed)'}`;
109
+ const problems = [];
110
+ if (!CUSTOM_COMPONENT_NAME.test(component.name)) {
111
+ problems.push(`Cannot use “${component.name}” as a component's name: a word starting with a capital letter, as Gauge.`);
112
+ }
113
+ else if (getComponent(component.name)) {
114
+ problems.push(`${said} is a component of the catalog: name yours otherwise.`);
115
+ }
116
+ if (!component.description.trim()) {
117
+ problems.push(`${said} says what it is for, under \`description\`.`);
118
+ }
119
+ if (!/^[a-z][a-z0-9+.-]*:/.test(component.source)) {
120
+ problems.push(`${said}'s module “${component.source}” is a file of the application's folder: it is drawn once the application is packaged with it (LOOP P-29); give the address of a built ES module.`);
121
+ }
122
+ else if (!CUSTOM_SOURCE.test(component.source)) {
123
+ problems.push(`${said}'s module “${component.source}” is loaded over \`https://\`, or from \`http://localhost\` while it is written.`);
124
+ }
125
+ if (component.integrity && !INTEGRITY.test(component.integrity)) {
126
+ problems.push(`${said}'s integrity “${component.integrity}” is no Subresource Integrity hash: \`sha384-\` and the hash in base64.`);
127
+ }
128
+ if (component.height < 40 || component.height > 2000) {
129
+ problems.push(`${said} is between 40 and 2000 pixels high.`);
130
+ }
131
+ const schema = component.props;
132
+ if (schema.type !== 'object' || !isRecord(schema.properties)) {
133
+ problems.push(`${said}'s props are the JSON Schema of an object, with its \`properties\`.`);
134
+ return problems;
135
+ }
136
+ const fields = fieldsOf(component);
137
+ for (const [name, field] of Object.entries(fields)) {
138
+ if (!PROPERTY_NAME.test(name) ||
139
+ CUSTOM_RESERVED_NAMES.includes(name)) {
140
+ problems.push(`${said} cannot have a property named ${quoted(name)}.`);
141
+ continue;
142
+ }
143
+ if (!isRecord(field)) {
144
+ problems.push(`${said}'s property ${quoted(name)} is not a schema.`);
145
+ continue;
146
+ }
147
+ if ('enum' in field) {
148
+ const values = field.enum;
149
+ if (!Array.isArray(values) ||
150
+ values.length === 0 ||
151
+ !values.every(value => typeof value === 'string')) {
152
+ problems.push(`${said}'s property ${quoted(name)} is an \`enum\` of words, one at least.`);
153
+ }
154
+ }
155
+ else if (!CUSTOM_PROPERTY_TYPES.includes(field.type)) {
156
+ problems.push(`${said}'s property ${quoted(name)} is typed ${quoted(String(field.type))}: one of ${CUSTOM_PROPERTY_TYPES.join(', ')}, or an \`enum\`.`);
157
+ continue;
158
+ }
159
+ if ('default' in field) {
160
+ const refused = valueRefused(name, field, field.default);
161
+ if (refused) {
162
+ problems.push(`${said}'s default for ${refused}.`);
163
+ }
164
+ }
165
+ }
166
+ const required = Array.isArray(schema.required)
167
+ ? schema.required
168
+ : [];
169
+ const missing = required.filter(name => !(name in fields));
170
+ if (missing.length > 0) {
171
+ problems.push(`${said} requires ${missing.map(quoted).join(', ')}, which its props do not have.`);
172
+ }
173
+ for (const name of [...component.shows, ...component.sends]) {
174
+ if (!PROPERTY_NAME.test(name) ||
175
+ CUSTOM_RESERVED_NAMES.includes(name)) {
176
+ problems.push(`${said} cannot bind through ${quoted(name)}.`);
177
+ }
178
+ else if (name in fields) {
179
+ problems.push(`${said} both has the property ${quoted(name)} and binds through it.`);
180
+ }
181
+ }
182
+ for (const [bindings, what] of [
183
+ [component.shows, 'shows'],
184
+ [component.sends, 'sends'],
185
+ ]) {
186
+ if (new Set(bindings).size !== bindings.length) {
187
+ problems.push(`${said} ${what} the same binding twice.`);
188
+ }
189
+ }
190
+ if (component.example) {
191
+ const bindings = new Set([...component.shows, ...component.sends]);
192
+ const given = Object.fromEntries(Object.entries(component.example).filter(([key]) => !bindings.has(key)));
193
+ const refused = customPropsRefused(component, given);
194
+ if (refused.length > 0) {
195
+ problems.push(`${said}'s example is refused: ${refused.join('; ')}.`);
196
+ }
197
+ }
198
+ return problems;
199
+ }
200
+ /** What stops the components an application's developer wrote, in sentences. */
201
+ export function customComponentsProblems(app) {
202
+ const own = app.interface.customComponents ?? [];
203
+ const problems = own.flatMap(customComponentProblems);
204
+ const names = own.map(component => component.name);
205
+ if (new Set(names).size !== names.length) {
206
+ problems.push('Two of its own components have the same name.');
207
+ }
208
+ return problems;
209
+ }
210
+ /** A component its developer wrote, by name, or undefined. */
211
+ export function customComponentOf(app, name) {
212
+ return (app.interface.customComponents ?? []).find(component => component.name === name);
213
+ }
214
+ /**
215
+ * It as the catalog lists a component (C-13): what the palette, the
216
+ * components page and the checks read — under the application's version, its
217
+ * category `custom`.
218
+ */
219
+ export function customComponentEntry(component, version) {
220
+ return {
221
+ id: component.name,
222
+ name: component.name,
223
+ description: component.description,
224
+ category: 'custom',
225
+ emoji: '🧩',
226
+ version,
227
+ standard: false,
228
+ properties: component.props,
229
+ bindings: { shows: [...component.shows], sends: [...component.sends] },
230
+ events: component.sends.length > 0 ? ['send'] : [],
231
+ ...(component.example ? { example: component.example } : {}),
232
+ };
233
+ }
234
+ /**
235
+ * The components an application may place: the catalog's own, given, and
236
+ * those its developer wrote, for it alone. A name of its own that the
237
+ * catalog has is not listed twice: the checks refuse it.
238
+ */
239
+ export function appComponents(app, catalog) {
240
+ const names = new Set(catalog.map(component => component.id));
241
+ return [
242
+ ...catalog,
243
+ ...(app.interface.customComponents ?? [])
244
+ .filter(component => !names.has(component.name))
245
+ .map(component => customComponentEntry(component, app.version)),
246
+ ];
247
+ }
248
+ /** What its schema refuses of a node of a surface drawn by it, in sentences; empty when nothing. */
249
+ export function customNodeRefused(component, node) {
250
+ const skip = new Set([
251
+ ...CUSTOM_RESERVED_NAMES,
252
+ ...component.shows,
253
+ ...component.sends,
254
+ ]);
255
+ const given = Object.fromEntries(Object.entries(node).filter(([key]) => !skip.has(key)));
256
+ const bound = Object.entries(given)
257
+ .filter(([, value]) => isBound(value))
258
+ .map(([key]) => key);
259
+ return customPropsRefused(component, given, bound);
260
+ }
@@ -6,12 +6,19 @@
6
6
  *
7
7
  * @module loop/apps/hostTools
8
8
  */
9
- import type { AppHostBridgeSpec } from '../../types/agentspecs';
9
+ import type { AppHostBridgeSpec, AppHostUser } from '../../types/agentspecs';
10
10
  /** The tool its agent reads what the page passes it with. */
11
11
  export declare const HOST_CONTEXT_TOOL = "host_context";
12
12
  /** The tool its agent calls a function of the page with. */
13
13
  export declare const hostTool: (name: string) => string;
14
14
  /** How a value or a function of the host is named. */
15
15
  export declare const HOST_NAME: RegExp;
16
+ /** What the page's word on its visitor may be worth (LOOP D-21). */
17
+ export declare const HOST_USERS: readonly AppHostUser[];
18
+ /**
19
+ * Whether a session is opened only for a user the host's server signed
20
+ * (LOOP D-21): `deployment.embedded.host.user: signed`.
21
+ */
22
+ export declare function signedUser(bridge: AppHostBridgeSpec | undefined): boolean;
16
23
  /** The tools its agent is given for the host. */
17
24
  export declare function hostToolsOf(bridge: AppHostBridgeSpec | undefined): string[];
@@ -8,6 +8,15 @@ export const HOST_CONTEXT_TOOL = 'host_context';
8
8
  export const hostTool = (name) => `host_${name}`;
9
9
  /** How a value or a function of the host is named. */
10
10
  export const HOST_NAME = /^[a-z][a-z0-9_]{0,62}$/;
11
+ /** What the page's word on its visitor may be worth (LOOP D-21). */
12
+ export const HOST_USERS = ['claimed', 'signed'];
13
+ /**
14
+ * Whether a session is opened only for a user the host's server signed
15
+ * (LOOP D-21): `deployment.embedded.host.user: signed`.
16
+ */
17
+ export function signedUser(bridge) {
18
+ return bridge?.user === 'signed';
19
+ }
11
20
  /** The tools its agent is given for the host. */
12
21
  export function hostToolsOf(bridge) {
13
22
  if (!bridge) {
@@ -18,3 +18,4 @@ export * from './records';
18
18
  export * from './rules';
19
19
  export * from './saved';
20
20
  export * from './yaml';
21
+ export * from './customComponents';
@@ -22,3 +22,4 @@ export * from './records';
22
22
  export * from './rules';
23
23
  export * from './saved';
24
24
  export * from './yaml';
25
+ export * from './customComponents';
@@ -0,0 +1,10 @@
1
+ import type { ReactComponentImplementation } from '@a2ui/react/v0_9';
2
+ export type A2uiComponentContribution = {
3
+ /** The name a surface gives it (e.g. 'Gauge'). */
4
+ id: string;
5
+ /** The application whose developer wrote it, by id. */
6
+ app: string;
7
+ /** Its renderer, as A2UI's catalog registers one. */
8
+ implementation: ReactComponentImplementation;
9
+ };
10
+ export declare const LoopA2uiComponent: import("@datalayer/reactor").ContributionPoint<A2uiComponentContribution>;
@@ -0,0 +1,15 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * A component an application adds to the A2UI catalog it is drawn with (LOOP
7
+ * P-17), as a contribution point, re-exported by `loop/core`: the components
8
+ * its developer wrote, contributed by the application's own plugin, so that
9
+ * its page, the elements its code shows and its answers draw them — in that
10
+ * application's workspace, and in no other.
11
+ *
12
+ * @module loop/core/a2uiComponents
13
+ */
14
+ import { defineContributionPoint } from '@datalayer/reactor';
15
+ export const LoopA2uiComponent = defineContributionPoint('loop.a2ui.component');
@@ -579,6 +579,7 @@ export type AssistantMenuContribution = {
579
579
  };
580
580
  export declare const LoopAssistantMenu: import("@datalayer/reactor").ContributionPoint<AssistantMenuContribution>;
581
581
  export { LoopCanvasBlock, type CanvasBlockContribution } from './canvasBlocks';
582
+ export { LoopA2uiComponent, type A2uiComponentContribution, } from './a2uiComponents';
582
583
  /**
583
584
  * The per-example chat extras a host feeds the loop's conversation live.
584
585
  *
@@ -146,6 +146,9 @@ export const LoopAssistantMenu = defineContributionPoint('loop.assistant.menu');
146
146
  // A block the Canvas may place (LOOP C-12), in a module of its own so that
147
147
  // the palette is read without the rest of the contracts.
148
148
  export { LoopCanvasBlock } from './canvasBlocks';
149
+ // A component an application adds to the catalog it is drawn with (LOOP
150
+ // P-17): the components its developer wrote, for its workspace alone.
151
+ export { LoopA2uiComponent, } from './a2uiComponents';
149
152
  export const LoopChatExtras = defineContributionPoint('loop.chat.extras');
150
153
  export const LoopOpeningNotebook = defineContributionPoint('loop.notebook.opening');
151
154
  export const LoopOpeningDocument = defineContributionPoint('loop.document.opening');
@@ -1,5 +1,5 @@
1
1
  import type { CSSProperties, JSX, ReactNode } from 'react';
2
- import type { PluginRef } from '@datalayer/reactor';
2
+ import { type PluginRef } from '@datalayer/reactor';
3
3
  import type { AppAccent, AppEmbedMode, AppSpec, AppThemeVariant } from '../../types/agentspecs';
4
4
  import type { ResumedThread, ThemeOverrides } from '../../types/chat';
5
5
  import type { AssistantCharacter } from '../../chat/assistant/characters';
@@ -24,6 +24,12 @@ export type AppEmbedProps = {
24
24
  colorMode?: EmbedColorMode;
25
25
  /** The host's face, a CSS font family; the theme's by default. */
26
26
  font?: string;
27
+ /**
28
+ * The language the visitor reads it in, as BCP 47 tags it (`fr`, `pt-BR`):
29
+ * its translation into it and the chat's own words (LOOP P-26). The
30
+ * visitor's browser's by default.
31
+ */
32
+ language?: string;
27
33
  /**
28
34
  * An agent-runtimes server the application's agent runs on. Without one,
29
35
  * it runs on a Datalayer runtime, launched with the visitor's Datalayer
@@ -150,6 +156,8 @@ export type AppFloatingProps = {
150
156
  host?: AppEmbedHost;
151
157
  /** The visit's session, picked up again after a reload (D-13). */
152
158
  session?: EmbedSession;
159
+ /** The language the visitor reads it in (P-26); the browser's by default. */
160
+ language?: string;
153
161
  };
154
162
  /**
155
163
  * The bubble, the panel and the assistant: `ChatFloating`'s chrome in that
@@ -160,7 +168,7 @@ export type AppFloatingProps = {
160
168
  * The workspace tells the chrome what the application is doing and what it
161
169
  * last said (`onPresence`, `onSaying`).
162
170
  */
163
- export declare function AppFloating({ app, view, colorMode, themeVariant, themeOverrides, character, serverUrl, instance, embedToken, renderer, host, session, }: AppFloatingProps): JSX.Element;
171
+ export declare function AppFloating({ app, view, colorMode, themeVariant, themeOverrides, character, serverUrl, instance, embedToken, renderer, host, session, language, }: AppFloatingProps): JSX.Element;
164
172
  /** The application's theme (`loop` unless it names one), with its (or the host's) accent and face. */
165
173
  export declare function EmbedThemed({ accent, colorMode, font, variant, children, style, }: {
166
174
  accent: AppAccent;
@@ -170,5 +178,5 @@ export declare function EmbedThemed({ accent, colorMode, font, variant, children
170
178
  children: ReactNode;
171
179
  style?: CSSProperties;
172
180
  }): JSX.Element;
173
- export declare function AppEmbed({ app, mode, accent, colorMode, font, serverUrl, instance, embedToken, height, ownPortal, plugins, host, resume, }: AppEmbedProps): JSX.Element;
181
+ export declare function AppEmbed({ app, mode, accent, colorMode, font, language, serverUrl, instance, embedToken, height, ownPortal, plugins, host, resume, }: AppEmbedProps): JSX.Element;
174
182
  export default AppEmbed;