@datalayer/agent-runtimes 1.3.83 → 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 (168) 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 +30 -11
  17. package/lib/chat/base/loopElement.d.ts +53 -0
  18. package/lib/chat/base/loopElement.js +78 -0
  19. package/lib/chat/base/loopWindow.d.ts +31 -0
  20. package/lib/chat/base/loopWindow.js +74 -0
  21. package/lib/chat/display/EmptyState.js +3 -1
  22. package/lib/chat/index.d.ts +2 -0
  23. package/lib/chat/index.js +3 -0
  24. package/lib/chat/messages/ChatMessages.js +3 -1
  25. package/lib/chat/messages/TurnFooter.js +3 -1
  26. package/lib/chat/prompt/InputPrompt.js +3 -1
  27. package/lib/chat/prompt/InputPromptLexical.js +3 -1
  28. package/lib/chat/prompt/footer/InPromptFooter.js +3 -1
  29. package/lib/chat/tools/ToolApprovalBanner.js +5 -3
  30. package/lib/chat/tools/ToolApprovalDialog.js +5 -3
  31. package/lib/chat/tools/ToolCallDisplay.js +29 -25
  32. package/lib/chat/words.d.ts +102 -0
  33. package/lib/chat/words.js +460 -0
  34. package/lib/components/a2ui/custom/index.d.ts +39 -0
  35. package/lib/components/a2ui/custom/index.js +95 -0
  36. package/lib/components/a2ui/custom/sandbox.d.ts +73 -0
  37. package/lib/components/a2ui/custom/sandbox.js +188 -0
  38. package/lib/components/a2ui/datalayer/FileUpload.d.ts +10 -2
  39. package/lib/components/a2ui/datalayer/FileUpload.js +10 -7
  40. package/lib/components/a2ui/datalayer/Form.d.ts +2 -0
  41. package/lib/components/a2ui/datalayer/Form.js +9 -2
  42. package/lib/components/a2ui/datalayer/formWidgets.d.ts +19 -0
  43. package/lib/components/a2ui/datalayer/formWidgets.js +51 -0
  44. package/lib/components/a2ui/datalayer/index.d.ts +1 -1
  45. package/lib/components/a2ui/datalayer/index.js +1 -1
  46. package/lib/components/a2ui/datalayer/schema.d.ts +10 -0
  47. package/lib/components/a2ui/datalayer/schema.js +12 -1
  48. package/lib/components/a2ui/index.d.ts +1 -0
  49. package/lib/components/a2ui/index.js +1 -0
  50. package/lib/components/a2ui/visibleWhen.d.ts +12 -1
  51. package/lib/components/a2ui/visibleWhen.js +24 -3
  52. package/lib/components/inspector/AgentInspector.d.ts +25 -44
  53. package/lib/components/inspector/AgentInspector.js +27 -215
  54. package/lib/components/inspector/AgentInspectorDialog.d.ts +6 -6
  55. package/lib/components/inspector/AgentInspectorDialog.js +2 -2
  56. package/lib/components/inspector/a2aSpans.d.ts +51 -0
  57. package/lib/components/inspector/a2aSpans.js +373 -0
  58. package/lib/components/inspector/agentSpans.d.ts +120 -0
  59. package/lib/components/inspector/agentSpans.js +253 -0
  60. package/lib/components/inspector/chatSpans.d.ts +36 -0
  61. package/lib/components/inspector/chatSpans.js +50 -0
  62. package/lib/components/inspector/classify.d.ts +11 -8
  63. package/lib/components/inspector/classify.js +23 -24
  64. package/lib/components/inspector/index.d.ts +7 -7
  65. package/lib/components/inspector/index.js +7 -7
  66. package/lib/components/teams/A2ATeamGraph.d.ts +4 -4
  67. package/lib/components/teams/SceneTranscript.d.ts +29 -0
  68. package/lib/components/teams/SceneTranscript.js +54 -0
  69. package/lib/components/teams/SceneView.d.ts +41 -0
  70. package/lib/components/teams/SceneView.js +28 -0
  71. package/lib/components/teams/index.d.ts +3 -0
  72. package/lib/components/teams/index.js +3 -0
  73. package/lib/components/teams/sceneTranscript.d.ts +74 -0
  74. package/lib/components/teams/sceneTranscript.js +294 -0
  75. package/lib/components/teams/teamConnections.d.ts +2 -2
  76. package/lib/components/teams/teamConnections.js +3 -3
  77. package/lib/components/teams/useA2ATeam.d.ts +4 -4
  78. package/lib/components/teams/useA2ATeam.js +50 -46
  79. package/lib/examples/AgentA2ATeamExample.d.ts +10 -9
  80. package/lib/examples/AgentA2ATeamExample.js +70 -60
  81. package/lib/examples/AssistantExample.js +2 -2
  82. package/lib/loop/apps/AppComposer.d.ts +40 -4
  83. package/lib/loop/apps/AppComposer.js +194 -19
  84. package/lib/loop/apps/AppRenderer.d.ts +21 -3
  85. package/lib/loop/apps/AppRenderer.js +88 -48
  86. package/lib/loop/apps/appspec.d.ts +13 -1
  87. package/lib/loop/apps/appspec.js +305 -12
  88. package/lib/loop/apps/checks.d.ts +14 -0
  89. package/lib/loop/apps/checks.js +276 -5
  90. package/lib/loop/apps/composer.d.ts +21 -10
  91. package/lib/loop/apps/composer.js +41 -8
  92. package/lib/loop/apps/customComponents.d.ts +41 -0
  93. package/lib/loop/apps/customComponents.js +260 -0
  94. package/lib/loop/apps/hostTools.d.ts +8 -1
  95. package/lib/loop/apps/hostTools.js +9 -0
  96. package/lib/loop/apps/index.d.ts +1 -0
  97. package/lib/loop/apps/index.js +1 -0
  98. package/lib/loop/apps/language.d.ts +68 -0
  99. package/lib/loop/apps/language.js +275 -0
  100. package/lib/loop/apps/settingsInputs.d.ts +29 -0
  101. package/lib/loop/apps/settingsInputs.js +147 -0
  102. package/lib/loop/apps/uploads.d.ts +43 -0
  103. package/lib/loop/apps/uploads.js +66 -0
  104. package/lib/loop/core/a2uiComponents.d.ts +10 -0
  105. package/lib/loop/core/a2uiComponents.js +15 -0
  106. package/lib/loop/core/index.d.ts +15 -1
  107. package/lib/loop/core/index.js +3 -0
  108. package/lib/loop/embed/AppEmbed.d.ts +25 -3
  109. package/lib/loop/embed/AppEmbed.js +106 -18
  110. package/lib/loop/embed/element.d.ts +5 -0
  111. package/lib/loop/embed/element.js +84 -5
  112. package/lib/loop/embed/embedConfig.d.ts +8 -0
  113. package/lib/loop/embed/embedConfig.js +18 -0
  114. package/lib/loop/embed/embedSession.d.ts +14 -0
  115. package/lib/loop/embed/embedSession.js +34 -0
  116. package/lib/loop/embed/hostBridge.d.ts +50 -3
  117. package/lib/loop/embed/hostBridge.js +22 -2
  118. package/lib/loop/index.d.ts +4 -3
  119. package/lib/loop/index.js +4 -3
  120. package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +7 -1
  121. package/lib/loop/plugins/a2ui-surface/InlineSurface.js +7 -2
  122. package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.js +4 -1
  123. package/lib/loop/plugins/a2ui-surface/ownComponents.d.ts +6 -0
  124. package/lib/loop/plugins/a2ui-surface/ownComponents.js +30 -0
  125. package/lib/loop/plugins/app-components/index.d.ts +26 -0
  126. package/lib/loop/plugins/app-components/index.js +56 -0
  127. package/lib/loop/plugins/app-elements/AppElements.d.ts +27 -0
  128. package/lib/loop/plugins/app-elements/AppElements.js +79 -0
  129. package/lib/loop/plugins/app-elements/index.d.ts +20 -0
  130. package/lib/loop/plugins/app-elements/index.js +43 -0
  131. package/lib/loop/plugins/app-page/AppPage.d.ts +9 -0
  132. package/lib/loop/plugins/app-page/AppPage.js +106 -8
  133. package/lib/loop/plugins/app-page/appPageModel.d.ts +17 -1
  134. package/lib/loop/plugins/app-page/appPageModel.js +122 -4
  135. package/lib/loop/plugins/app-page/index.d.ts +1 -0
  136. package/lib/loop/plugins/app-page/index.js +1 -0
  137. package/lib/loop/plugins/app-page/pageRun.d.ts +46 -0
  138. package/lib/loop/plugins/app-page/pageRun.js +111 -0
  139. package/lib/loop/plugins/assistant/index.js +8 -6
  140. package/lib/loop/plugins/chat/ChatView.js +16 -6
  141. package/lib/loop/plugins/chat/index.d.ts +1 -1
  142. package/lib/loop/plugins/chat/index.js +2 -1
  143. package/lib/models/PageTag.d.ts +1 -1
  144. package/lib/protocols/BrowserAgentAdapter.d.ts +8 -6
  145. package/lib/protocols/BrowserAgentAdapter.js +13 -10
  146. package/lib/runtimes/browser/a2aPeer.d.ts +2 -1
  147. package/lib/runtimes/browser/protocol.d.ts +1 -1
  148. package/lib/specs/actions.js +78 -1
  149. package/lib/specs/apps.d.ts +5 -0
  150. package/lib/specs/apps.js +1518 -216
  151. package/lib/specs/appspecSchema.js +630 -3
  152. package/lib/specs/teams/teams.d.ts +3 -0
  153. package/lib/specs/teams/teams.js +249 -0
  154. package/lib/specs/uiPlugins.js +6 -1
  155. package/lib/types/agentspecs.d.ts +200 -1
  156. package/lib/types/chat.d.ts +6 -5
  157. package/lib/types/teams.d.ts +13 -2
  158. package/package.json +2 -2
  159. package/scripts/codegen/generate_apps.py +20 -2
  160. package/scripts/codegen/generate_teams.py +15 -2
  161. package/lib/components/inspector/JsonTree.d.ts +0 -24
  162. package/lib/components/inspector/JsonTree.js +0 -89
  163. package/lib/components/inspector/a2aInspect.d.ts +0 -56
  164. package/lib/components/inspector/a2aInspect.js +0 -478
  165. package/lib/components/inspector/agentInspector.d.ts +0 -179
  166. package/lib/components/inspector/agentInspector.js +0 -267
  167. package/lib/components/inspector/chatInspect.d.ts +0 -38
  168. package/lib/components/inspector/chatInspect.js +0 -65
@@ -39,10 +39,14 @@ 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
+ import { MAX_UPLOAD_MB, UPLOAD_KIND } from './uploads';
47
+ import { formUiProblems } from './settingsInputs';
48
+ import { customComponentOf, customComponentsProblems, customNodeRefused, customPropsRefused, } from './customComponents';
49
+ import { translationProblems } from './language';
46
50
  export const NOT_READY = 'Not ready';
47
51
  export const NEEDS_ATTENTION = 'Needs attention';
48
52
  export const PASSES = 'Passes the instant checks';
@@ -80,6 +84,130 @@ const matches = (name, pattern) => new RegExp('^' +
80
84
  ? '[\\s\\S]'
81
85
  : c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('') +
82
86
  '$').test(name);
87
+ /** The name of a function of an application's code (agentspecs' `CODE_NAME`). */
88
+ const CODE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
89
+ /**
90
+ * What its code declares that agentspecs refuses (LOOP P-06): its tools,
91
+ * its checks and the tests it decides, each by a Python name, each name
92
+ * once; a tool says what it does, by class, each class once.
93
+ */
94
+ export function ownCodeProblems(app) {
95
+ const problems = [];
96
+ const tools = app.tools ?? [];
97
+ for (const tool of tools) {
98
+ if (!CODE_NAME.test(tool.name)) {
99
+ problems.push(`Cannot use “${tool.name}” as a tool's name: letters, digits and \`_\`, as in Python.`);
100
+ }
101
+ if (!tool.description.trim()) {
102
+ problems.push(`The tool “${tool.name}” says what it does, for the agent.`);
103
+ }
104
+ if (tool.does.length === 0) {
105
+ problems.push(`The tool “${tool.name}” says what it does: read, write, send, buy, delete or publish.`);
106
+ }
107
+ for (const action of tool.does) {
108
+ if (!CLASS_NAMES.has(action)) {
109
+ problems.push(`The tool “${tool.name}” does “${action}”, which is no class of action.`);
110
+ }
111
+ }
112
+ if (new Set(tool.does).size !== tool.does.length) {
113
+ problems.push('A tool says each thing it does once.');
114
+ }
115
+ }
116
+ const names = tools.map(tool => tool.name);
117
+ if (new Set(names).size !== names.length) {
118
+ problems.push('Two of its tools have the same name.');
119
+ }
120
+ const checks = (app.checks.code ?? []).map(check => check.name);
121
+ for (const name of checks) {
122
+ if (!CODE_NAME.test(name)) {
123
+ problems.push(`Cannot use “${name}” as a check's name: letters, digits and \`_\`, as in Python.`);
124
+ }
125
+ }
126
+ if (new Set(checks).size !== checks.length) {
127
+ problems.push('Two checks of its code have the same name.');
128
+ }
129
+ const decided = app.tests.cases.flatMap(testCase => testCase.code ? [testCase.code] : []);
130
+ if (new Set(decided).size !== decided.length) {
131
+ problems.push('Two tests are decided by the same function of its code.');
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
+ }
209
+ return problems;
210
+ }
83
211
  /** What the spec says of itself that the tolerant reader lets through. */
84
212
  function shapeProblems(app) {
85
213
  const problems = [];
@@ -120,6 +248,36 @@ function shapeProblems(app) {
120
248
  else if (app.triggers.length > 0) {
121
249
  problems.push(`A ${app.kind} application starts when somebody opens it: \`triggers\` are a worker's.`);
122
250
  }
251
+ // Its profiles (LOOP P-20): two at least, or none; each id slash-safe and
252
+ // said once, each with a label.
253
+ const profiles = app.interface.profiles ?? [];
254
+ if (profiles.length === 1) {
255
+ problems.push('One profile is the application itself: say two at least, or put its instructions, model and starters on the application.');
256
+ }
257
+ const profileIds = new Set();
258
+ for (const profile of profiles) {
259
+ if (!MODE_ID.test(profile.id)) {
260
+ problems.push(`Cannot use “${profile.id}” as a profile's id: lower-case letters, digits, \`_\` and \`-\`, a letter first.`);
261
+ }
262
+ if (!profile.label.trim()) {
263
+ problems.push(`The profile “${profile.id}” has no label.`);
264
+ }
265
+ if (profileIds.has(profile.id)) {
266
+ problems.push(`Two profiles are “${profile.id}”.`);
267
+ }
268
+ profileIds.add(profile.id);
269
+ }
270
+ // How its settings' fields are drawn (P-20): by a widget that draws each.
271
+ if (app.interface.settingsUi) {
272
+ if (!app.interface.settings) {
273
+ problems.push('`settings_ui` draws the settings’ fields: say `settings` first.');
274
+ }
275
+ else {
276
+ problems.push(...formUiProblems('The form “settings”', app.interface.settings, app.interface.settingsUi));
277
+ }
278
+ }
279
+ // Its words in other languages (P-26): of languages, of what it says.
280
+ problems.push(...translationProblems(app.interface));
123
281
  // What the host page passes and offers (D-10): named once each, in words
124
282
  // its agent's tools can carry, each function saying what it does.
125
283
  const host = app.deployment.embedded?.host;
@@ -147,6 +305,10 @@ function shapeProblems(app) {
147
305
  if (new Set(names).size !== names.length) {
148
306
  problems.push('The host’s functions are named once each.');
149
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
+ }
150
312
  const named = new Set(app.rules.flatMap(rule => rule.appliesTo));
151
313
  for (const tool of hostToolsOf(host)) {
152
314
  if (!named.has(tool)) {
@@ -154,6 +316,8 @@ function shapeProblems(app) {
154
316
  }
155
317
  }
156
318
  }
319
+ // What its code declares (LOOP P-06), as agentspecs refuses it.
320
+ problems.push(...ownCodeProblems(app));
157
321
  const servers = app.connections.map(connection => idOf(connection.server));
158
322
  if (new Set(servers).size !== servers.length) {
159
323
  problems.push('The application connects to the same server twice.');
@@ -212,6 +376,10 @@ export function formProblems(node) {
212
376
  if (missing.length > 0) {
213
377
  problems.push(`${said} requires ${missing.map(name => `“${String(name)}”`).join(', ')}, which it does not ask.`);
214
378
  }
379
+ // How its fields are drawn (P-20): by a widget that draws each.
380
+ if (node.ui !== undefined && node.ui !== null) {
381
+ problems.push(...formUiProblems(said, schema, node.ui));
382
+ }
215
383
  return problems;
216
384
  }
217
385
  const isPlainRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -278,6 +446,12 @@ function referenceProblems(app, organizationFrames) {
278
446
  if (app.model && !getModel(app.model)) {
279
447
  missing('model', app.model);
280
448
  }
449
+ // The model a profile runs on is the catalogue's (LOOP P-20).
450
+ for (const profile of app.interface.profiles ?? []) {
451
+ if (profile.model && !getModel(profile.model)) {
452
+ problems.push(`The profile “${profile.id}” runs on “${profile.model}”, which is no model.`);
453
+ }
454
+ }
281
455
  // The model a mode runs on is the catalogue's (LOOP P-19).
282
456
  for (const mode of app.interface.modes ?? []) {
283
457
  for (const option of mode.options) {
@@ -287,13 +461,21 @@ function referenceProblems(app, organizationFrames) {
287
461
  }
288
462
  }
289
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).
290
465
  for (const name of app.interface.components ?? []) {
291
- if (!componentNamed(name)) {
466
+ if (!componentNamed(name) && !customComponentOf(app, name)) {
292
467
  problems.push(`There is no component named “${name}” in the catalog.`);
293
468
  }
294
469
  }
295
470
  for (const node of app.interface.surface?.components ?? []) {
296
- 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))) {
297
479
  problems.push(`The surface's “${String(node.id)}” is a “${String(node.component)}”, which the catalog does not have.`);
298
480
  }
299
481
  else if (node.component === 'Form') {
@@ -336,6 +518,9 @@ function referenceProblems(app, organizationFrames) {
336
518
  if (server === undefined) {
337
519
  if (hostTools.includes(target))
338
520
  continue;
521
+ // A tool of its own, written in its code (LOOP P-06).
522
+ if ((app.tools ?? []).some(tool => tool.name === target))
523
+ continue;
339
524
  if (!getBackendToolSpec(name)) {
340
525
  problems.push(`The rule “${rule.action}” names the tool “${target}”, which the catalogue does not have.`);
341
526
  }
@@ -646,9 +831,44 @@ export function documentShapeProblems(document) {
646
831
  }
647
832
  text(ui.welcome, 'interface.welcome');
648
833
  texts(ui.components, 'interface.components');
649
- records(ui.starters, 'interface.starters', (item, where) => {
834
+ const starter = (item, where) => {
650
835
  required(item, 'label', where);
651
836
  required(item, 'message', where);
837
+ text(item.category, `${where}.category`);
838
+ unknownKeys(item, ['label', 'message', 'category'], where, 'a starter');
839
+ };
840
+ records(ui.starters, 'interface.starters', starter);
841
+ // Its profiles (LOOP P-20), as agentspecs refuses them: no key unknown.
842
+ records(ui.profiles, 'interface.profiles', (profile, where) => {
843
+ required(profile, 'id', where);
844
+ required(profile, 'label', where);
845
+ for (const key of [
846
+ 'id',
847
+ 'label',
848
+ 'description',
849
+ 'instructions',
850
+ 'model',
851
+ ]) {
852
+ text(profile[key], `${where}.${key}`);
853
+ }
854
+ unknownKeys(profile, ['id', 'label', 'description', 'instructions', 'model', 'starters'], where, 'a profile');
855
+ records(profile.starters, `${where}.starters`, starter);
856
+ });
857
+ text(ui.language, 'interface.language');
858
+ mapping(ui.translations, 'interface.translations', translations => {
859
+ for (const [tag, words] of Object.entries(translations)) {
860
+ mapping(words, `interface.translations.${tag}`, said => unknownKeys(said, [
861
+ 'name',
862
+ 'description',
863
+ 'welcome',
864
+ 'starters',
865
+ 'categories',
866
+ 'settings',
867
+ 'commands',
868
+ 'modes',
869
+ 'profiles',
870
+ ], `interface.translations.${tag}`, 'a translation'));
871
+ }
652
872
  });
653
873
  // Its commands and modes in the composer (LOOP P-19), as agentspecs
654
874
  // refuses them: names and ids slash-safe and said once, no key unknown.
@@ -738,11 +958,62 @@ export function documentShapeProblems(document) {
738
958
  problems.push(...formProblems({ id: 'settings', schema: settings }));
739
959
  });
740
960
  }
961
+ // What a person may send without being asked (LOOP P-21), as agentspecs
962
+ // refuses it: a kind at least, each a type said once, sizes it takes.
963
+ mapping(ui.uploads, 'interface.uploads', uploads => {
964
+ unknownKeys(uploads, ['kinds', 'max_files'], 'interface.uploads', 'uploads');
965
+ required(uploads, 'kinds', 'interface.uploads');
966
+ const types = new Set();
967
+ records(uploads.kinds, 'interface.uploads.kinds', (kind, where) => {
968
+ required(kind, 'type', where);
969
+ unknownKeys(kind, ['type', 'max_mb'], where, 'a kind of upload');
970
+ if (kind.type !== undefined) {
971
+ if (typeof kind.type !== 'string' || !UPLOAD_KIND.test(kind.type)) {
972
+ at(`${where}.type`, 'is a media type (application/pdf), a family (image/*) or an extension (.csv), lowercase');
973
+ }
974
+ else if (types.has(kind.type)) {
975
+ at(`${where}.type`, `names ${kind.type} a second time`);
976
+ }
977
+ else {
978
+ types.add(kind.type);
979
+ }
980
+ }
981
+ const size = kind.max_mb;
982
+ if (size !== undefined &&
983
+ (typeof size !== 'number' || size <= 0 || size > MAX_UPLOAD_MB)) {
984
+ at(`${where}.max_mb`, `is more than 0 and ${MAX_UPLOAD_MB} at most`);
985
+ }
986
+ });
987
+ if (Array.isArray(uploads.kinds) && uploads.kinds.length === 0) {
988
+ at('interface.uploads.kinds', 'are one at least');
989
+ }
990
+ const most = uploads.max_files;
991
+ if (most !== undefined &&
992
+ (typeof most !== 'number' ||
993
+ !Number.isInteger(most) ||
994
+ most < 1 ||
995
+ most > 20)) {
996
+ at('interface.uploads.max_files', 'is a whole number from 1 to 20');
997
+ }
998
+ });
741
999
  mapping(ui.surface, 'interface.surface', surface => {
742
1000
  records(surface.components, 'interface.surface.components', (item, where) => {
743
1001
  required(item, 'id', where);
744
1002
  required(item, 'component', where);
745
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
+ });
746
1017
  });
747
1018
  });
748
1019
  mapping(d.tests, 'tests', tests => {
@@ -1,14 +1,15 @@
1
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.
2
+ * Commands, modes and profiles in the composer (LOOP P-19, P-20), as rules:
3
+ * what a command sends, which option of each mode a run is in, which profile
4
+ * a conversation is with and what it offers. agentspecs' own
5
+ * (`command_prompt`, `AppInterface.mode_choice`, `mode_effect`,
6
+ * `profile_choice`, `starters_for`), said again for the page.
6
7
  *
7
8
  * Pure: no React, no network.
8
9
  *
9
10
  * @module loop/apps/composer
10
11
  */
11
- import type { AppCommandSpec, AppModeEffect, AppSpec } from '../../types/agentspecs';
12
+ import type { AppCommandSpec, AppModeEffect, AppProfileSpec, AppSpec, AppStarterSpec } from '../../types/agentspecs';
12
13
  /** Where a command's prompt takes the words typed after it. */
13
14
  export declare const COMMAND_INPUT = "{input}";
14
15
  /** What follows the slash: lower-case letters, digits and hyphens, a letter first. */
@@ -29,9 +30,19 @@ export declare function modeDefaults(app: Pick<AppSpec, 'interface'>): Record<st
29
30
  */
30
31
  export declare function modeChoice(app: Pick<AppSpec, 'name' | 'interface'>, chosen?: Record<string, string>): Record<string, string>;
31
32
  /**
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.
33
+ * The profile a conversation is with (LOOP P-20): the one chosen, else the
34
+ * first; undefined for an application without profiles. One it does not have
35
+ * is refused, in a sentence, as the runtime refuses it.
36
36
  */
37
- export declare function modeEffect(app: Pick<AppSpec, 'name' | 'interface'>, chosen?: Record<string, string>): AppModeEffect;
37
+ export declare function profileChoice(app: Pick<AppSpec, 'name' | 'interface'>, chosen?: string): AppProfileSpec | undefined;
38
+ /** The starters offered with a profile: its own, else the application's (P-20). */
39
+ export declare function startersFor(app: Pick<AppSpec, 'name' | 'interface'>, profile?: string): AppStarterSpec[];
40
+ /**
41
+ * What a run in the modes chosen, with the profile chosen, is told, and the
42
+ * model it runs on — agentspecs' `mode_effect`, and the runtime's
43
+ * `run_effect`: the profile's instructions first, then those of every option
44
+ * chosen, in the order of the modes; the model of the first option that
45
+ * names one, else the profile's. Refused as {@link modeChoice} and
46
+ * {@link profileChoice} refuse.
47
+ */
48
+ export declare function modeEffect(app: Pick<AppSpec, 'name' | 'interface'>, chosen?: Record<string, string>, profile?: string): AppModeEffect;
@@ -44,18 +44,51 @@ export function modeChoice(app, chosen = {}) {
44
44
  return { ...modeDefaults(app), ...chosen };
45
45
  }
46
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.
47
+ * The profile a conversation is with (LOOP P-20): the one chosen, else the
48
+ * first; undefined for an application without profiles. One it does not have
49
+ * is refused, in a sentence, as the runtime refuses it.
51
50
  */
52
- export function modeEffect(app, chosen = {}) {
51
+ export function profileChoice(app, chosen) {
52
+ const profiles = app.interface.profiles ?? [];
53
+ if (chosen) {
54
+ const profile = profiles.find(candidate => candidate.id === chosen);
55
+ if (!profile) {
56
+ throw new Error(profiles.length === 0
57
+ ? `${app.name} has no profiles: “${chosen}” cannot be chosen.`
58
+ : `${app.name} has no profile “${chosen}”.`);
59
+ }
60
+ return profile;
61
+ }
62
+ return profiles[0];
63
+ }
64
+ /** The starters offered with a profile: its own, else the application's (P-20). */
65
+ export function startersFor(app, profile) {
66
+ const chosen = profileChoice(app, profile);
67
+ return chosen && chosen.starters.length > 0
68
+ ? chosen.starters
69
+ : app.interface.starters;
70
+ }
71
+ /**
72
+ * What a run in the modes chosen, with the profile chosen, is told, and the
73
+ * model it runs on — agentspecs' `mode_effect`, and the runtime's
74
+ * `run_effect`: the profile's instructions first, then those of every option
75
+ * chosen, in the order of the modes; the model of the first option that
76
+ * names one, else the profile's. Refused as {@link modeChoice} and
77
+ * {@link profileChoice} refuse.
78
+ */
79
+ export function modeEffect(app, chosen = {}, profile) {
53
80
  const choice = modeChoice(app, chosen);
81
+ const withProfile = profileChoice(app, profile);
54
82
  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())
83
+ const instructions = [
84
+ withProfile?.instructions ?? '',
85
+ ...options.map(option => option.instructions ?? ''),
86
+ ]
87
+ .map(part => part.trim())
57
88
  .filter(Boolean)
58
89
  .join('\n\n');
59
- const model = options.find(option => option.model)?.model ?? undefined;
90
+ const model = options.find(option => option.model)?.model ??
91
+ withProfile?.model ??
92
+ undefined;
60
93
  return model ? { instructions, model } : { instructions };
61
94
  }
@@ -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[];