@datalayer/agent-runtimes 1.3.83 → 1.3.84

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 (49) hide show
  1. package/lib/chat/base/ChatBase.js +16 -0
  2. package/lib/chat/base/loopElement.d.ts +53 -0
  3. package/lib/chat/base/loopElement.js +78 -0
  4. package/lib/chat/base/loopWindow.d.ts +31 -0
  5. package/lib/chat/base/loopWindow.js +74 -0
  6. package/lib/components/a2ui/datalayer/FileUpload.d.ts +10 -2
  7. package/lib/components/a2ui/datalayer/FileUpload.js +10 -7
  8. package/lib/components/a2ui/datalayer/Form.d.ts +2 -0
  9. package/lib/components/a2ui/datalayer/Form.js +9 -2
  10. package/lib/components/a2ui/datalayer/formWidgets.d.ts +19 -0
  11. package/lib/components/a2ui/datalayer/formWidgets.js +51 -0
  12. package/lib/loop/apps/AppComposer.d.ts +39 -4
  13. package/lib/loop/apps/AppComposer.js +191 -19
  14. package/lib/loop/apps/AppRenderer.d.ts +20 -3
  15. package/lib/loop/apps/AppRenderer.js +58 -27
  16. package/lib/loop/apps/appspec.d.ts +2 -0
  17. package/lib/loop/apps/appspec.js +190 -11
  18. package/lib/loop/apps/checks.d.ts +6 -0
  19. package/lib/loop/apps/checks.js +170 -1
  20. package/lib/loop/apps/composer.d.ts +21 -10
  21. package/lib/loop/apps/composer.js +41 -8
  22. package/lib/loop/apps/language.d.ts +68 -0
  23. package/lib/loop/apps/language.js +275 -0
  24. package/lib/loop/apps/settingsInputs.d.ts +29 -0
  25. package/lib/loop/apps/settingsInputs.js +147 -0
  26. package/lib/loop/apps/uploads.d.ts +43 -0
  27. package/lib/loop/apps/uploads.js +66 -0
  28. package/lib/loop/core/index.d.ts +14 -1
  29. package/lib/loop/embed/AppEmbed.d.ts +14 -0
  30. package/lib/loop/embed/AppEmbed.js +64 -2
  31. package/lib/loop/embed/element.js +23 -0
  32. package/lib/loop/embed/embedSession.d.ts +14 -0
  33. package/lib/loop/embed/embedSession.js +34 -0
  34. package/lib/loop/embed/hostBridge.d.ts +22 -1
  35. package/lib/loop/plugins/app-elements/AppElements.d.ts +27 -0
  36. package/lib/loop/plugins/app-elements/AppElements.js +76 -0
  37. package/lib/loop/plugins/app-elements/index.d.ts +20 -0
  38. package/lib/loop/plugins/app-elements/index.js +43 -0
  39. package/lib/loop/plugins/app-page/appPageModel.js +2 -0
  40. package/lib/loop/plugins/chat/ChatView.js +10 -2
  41. package/lib/protocols/BrowserAgentAdapter.d.ts +8 -6
  42. package/lib/protocols/BrowserAgentAdapter.js +13 -10
  43. package/lib/runtimes/browser/protocol.d.ts +1 -1
  44. package/lib/specs/apps.js +149 -0
  45. package/lib/specs/appspecSchema.js +439 -2
  46. package/lib/specs/uiPlugins.js +6 -1
  47. package/lib/types/agentspecs.d.ts +130 -1
  48. package/package.json +1 -1
  49. package/scripts/codegen/generate_apps.py +16 -0
@@ -3,8 +3,11 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
  import { BEHAVIOURS } from './rules';
6
+ import { DEFAULT_MAX_FILES, DEFAULT_UPLOAD_MB } from './uploads';
6
7
  /** The version of the spec itself. */
7
8
  export const APP_SCHEMA = 'loop.app/v1';
9
+ /** The language an application's own words are in, unless it says (LOOP P-26). */
10
+ export const DEFAULT_LANGUAGE = 'en';
8
11
  /** The face of an application that has not chosen one. */
9
12
  export const DEFAULT_EMOJI = '\u{1F440}';
10
13
  export const APP_KINDS = ['chat', 'widget', 'decision', 'worker'];
@@ -124,6 +127,9 @@ export function emptyAppspec(kind = 'chat') {
124
127
  starters: [],
125
128
  commands: [],
126
129
  modes: [],
130
+ profiles: [],
131
+ language: DEFAULT_LANGUAGE,
132
+ translations: {},
127
133
  components: [],
128
134
  outputs: [],
129
135
  },
@@ -170,6 +176,7 @@ const KNOWN_KEYS = [
170
176
  'model',
171
177
  'skills',
172
178
  'backend_tools',
179
+ 'tools',
173
180
  'context',
174
181
  'contents',
175
182
  'connections',
@@ -193,6 +200,19 @@ const KNOWN_KEYS = [
193
200
  'avatar',
194
201
  'banner',
195
202
  ];
203
+ /** Where a check of its code runs (LOOP P-06). */
204
+ const CHECK_STAGES = ['answer', 'tool_call'];
205
+ /** A tool of its own, written in its code (LOOP P-06); a wrong one is said by the checks. */
206
+ function parseTool(data) {
207
+ return {
208
+ name: text(data.name),
209
+ description: text(data.description),
210
+ parameters: isData(data.parameters)
211
+ ? data.parameters
212
+ : { ...NO_HOST_PARAMETERS },
213
+ does: texts(data.does),
214
+ };
215
+ }
196
216
  function parseConnection(data) {
197
217
  return {
198
218
  server: text(data.server),
@@ -243,6 +263,67 @@ function parseMode(data) {
243
263
  }
244
264
  return mode;
245
265
  }
266
+ /** A starter, its category said only when it has one (LOOP P-20). */
267
+ function parseStarter(data) {
268
+ const starter = {
269
+ label: text(data.label),
270
+ message: text(data.message),
271
+ };
272
+ if (typeof data.category === 'string' && data.category) {
273
+ starter.category = data.category;
274
+ }
275
+ return starter;
276
+ }
277
+ /** One of several assistants in one application (LOOP P-20). */
278
+ function parseProfile(data) {
279
+ return {
280
+ id: text(data.id),
281
+ label: text(data.label),
282
+ description: text(data.description),
283
+ instructions: text(data.instructions),
284
+ model: typeof data.model === 'string' && data.model ? data.model : null,
285
+ starters: records(data.starters).map(parseStarter),
286
+ };
287
+ }
288
+ /** Words by key, what is not a word left out. */
289
+ function wordsBy(value) {
290
+ return Object.fromEntries(Object.entries(isData(value) ? value : {}).filter((entry) => typeof entry[1] === 'string'));
291
+ }
292
+ /** Records by key, what is not one left out. */
293
+ function recordsBy(value) {
294
+ return Object.fromEntries(Object.entries(isData(value) ? value : {}).filter((entry) => isData(entry[1])));
295
+ }
296
+ const mapValues = (value, map) => Object.fromEntries(Object.entries(value).map(([key, item]) => [key, map(item)]));
297
+ /** An application's words in one other language (LOOP P-26), every key present. */
298
+ function parseTranslation(data) {
299
+ return {
300
+ name: text(data.name),
301
+ description: text(data.description),
302
+ welcome: text(data.welcome),
303
+ starters: mapValues(recordsBy(data.starters), starter => ({
304
+ label: text(starter.label),
305
+ message: text(starter.message),
306
+ })),
307
+ categories: wordsBy(data.categories),
308
+ settings: mapValues(recordsBy(data.settings), field => ({
309
+ title: text(field.title),
310
+ description: text(field.description),
311
+ options: wordsBy(field.options),
312
+ })),
313
+ commands: wordsBy(data.commands),
314
+ modes: mapValues(recordsBy(data.modes), mode => ({
315
+ label: text(mode.label),
316
+ options: mapValues(recordsBy(mode.options), option => ({
317
+ label: text(option.label),
318
+ description: text(option.description),
319
+ })),
320
+ })),
321
+ profiles: mapValues(recordsBy(data.profiles), profile => ({
322
+ label: text(profile.label),
323
+ description: text(profile.description),
324
+ })),
325
+ };
326
+ }
246
327
  function parseSurface(data) {
247
328
  return {
248
329
  protocol: text(data.protocol, DEFAULT_PROTOCOL),
@@ -272,17 +353,27 @@ function parseVoice(data) {
272
353
  where: oneOf(voice.where, ['auto', 'device', 'server'], DEFAULT_VOICE.where),
273
354
  };
274
355
  }
356
+ /** What a person may send without being asked (P-21), as the spec says it. */
357
+ function parseUploads(data) {
358
+ return {
359
+ kinds: records(data.kinds).map(kind => ({
360
+ type: text(kind.type),
361
+ maxMb: numberOf(kind.max_mb, DEFAULT_UPLOAD_MB),
362
+ })),
363
+ maxFiles: numberOf(data.max_files, DEFAULT_MAX_FILES),
364
+ };
365
+ }
275
366
  function parseInterface(data, kind) {
276
367
  const parsed = {
277
368
  layout: oneOf(data.layout, ['chat', 'page', 'split'], DEFAULT_LAYOUTS[kind]),
278
369
  accent: oneOf(data.accent, APP_ACCENTS, 'green'),
279
370
  welcome: text(data.welcome),
280
- starters: records(data.starters).map(starter => ({
281
- label: text(starter.label),
282
- message: text(starter.message),
283
- })),
371
+ starters: records(data.starters).map(parseStarter),
284
372
  commands: records(data.commands).map(parseCommand),
285
373
  modes: records(data.modes).map(parseMode),
374
+ profiles: records(data.profiles).map(parseProfile),
375
+ language: text(data.language, DEFAULT_LANGUAGE),
376
+ translations: mapValues(recordsBy(data.translations), parseTranslation),
286
377
  components: texts(data.components),
287
378
  voice: parseVoice(data.voice),
288
379
  outputs: texts(data.outputs),
@@ -293,6 +384,14 @@ function parseInterface(data, kind) {
293
384
  if (isData(data.settings)) {
294
385
  parsed.settings = JSON.parse(JSON.stringify(data.settings));
295
386
  }
387
+ // How its settings' fields are drawn (P-20), kept as written: a wrong
388
+ // widget is said by the checks.
389
+ if (isData(data.settings_ui)) {
390
+ parsed.settingsUi = JSON.parse(JSON.stringify(data.settings_ui));
391
+ }
392
+ if (isData(data.uploads)) {
393
+ parsed.uploads = parseUploads(data.uploads);
394
+ }
296
395
  if (isData(data.surface)) {
297
396
  parsed.surface = parseSurface(data.surface);
298
397
  }
@@ -416,6 +515,12 @@ export function parseAppspec(document) {
416
515
  const verified = isData(tests.verified) ? tests.verified : {};
417
516
  const record = isData(data.record) ? data.record : {};
418
517
  const checks = isData(data.checks) ? data.checks : {};
518
+ // A check of its code runs on an answer or a tool call (LOOP P-06).
519
+ for (const check of records(checks.code)) {
520
+ if (!CHECK_STAGES.includes(check.on)) {
521
+ problems.push(`The check \`${text(check.name)}\` of its code runs on answer or tool_call, not \`${String(check.on)}\`.`);
522
+ }
523
+ }
419
524
  const keepFor = text(record.keep_for, DEFAULT_KEEP_FOR);
420
525
  if (retentionDays(keepFor) === 0) {
421
526
  problems.push(`Cannot read the retention \`${keepFor}\`: write 90_days, 18_months or 1_years.`);
@@ -435,6 +540,7 @@ export function parseAppspec(document) {
435
540
  model: text(data.model),
436
541
  skills: texts(data.skills),
437
542
  backendTools: texts(data.backend_tools),
543
+ tools: records(data.tools).map(parseTool),
438
544
  context: texts(data.context),
439
545
  contents: texts(data.contents),
440
546
  connections: records(data.connections).map(parseConnection),
@@ -457,6 +563,8 @@ export function parseAppspec(document) {
457
563
  cases: records(tests.cases).map(testCase => ({
458
564
  ask: text(testCase.ask),
459
565
  expect: text(testCase.expect),
566
+ // Decided by its code (LOOP P-06), when it names the function.
567
+ ...(text(testCase.code) ? { code: text(testCase.code) } : {}),
460
568
  })),
461
569
  verified: {
462
570
  live: texts(verified.live),
@@ -476,6 +584,11 @@ export function parseAppspec(document) {
476
584
  guards: texts(checks.guards),
477
585
  gates: texts(checks.gates),
478
586
  track: text(checks.track),
587
+ code: records(checks.code).map(check => ({
588
+ name: text(check.name),
589
+ on: oneOf(check.on, CHECK_STAGES, 'answer'),
590
+ description: text(check.description),
591
+ })),
479
592
  },
480
593
  deployment: parseDeployment(isData(data.deployment) ? data.deployment : {}),
481
594
  goal: text(data.goal),
@@ -557,16 +670,44 @@ function dumpSurface(surface) {
557
670
  .text('composed_by', surface.composedBy)
558
671
  .text('composed_at', surface.composedAt).data;
559
672
  }
673
+ function dumpStarter(starter) {
674
+ return new Writer()
675
+ .text('label', starter.label, '\u0000')
676
+ .text('message', starter.message, '\u0000')
677
+ .text('category', starter.category ?? '').data;
678
+ }
679
+ /** Words by key, what is empty left out. */
680
+ const saidWords = (words) => Object.fromEntries(Object.entries(words).filter(([, said]) => said !== ''));
681
+ /** One translation as it is written (P-26): only what it says. */
682
+ function dumpTranslation(words) {
683
+ const parts = (value, dump) => Object.fromEntries(Object.entries(value).map(([key, item]) => [key, dump(item)]));
684
+ return new Writer()
685
+ .text('name', words.name)
686
+ .text('description', words.description)
687
+ .text('welcome', words.welcome)
688
+ .part('starters', parts(words.starters, starter => new Writer()
689
+ .text('label', starter.label)
690
+ .text('message', starter.message).data))
691
+ .part('categories', saidWords(words.categories))
692
+ .part('settings', parts(words.settings, field => new Writer()
693
+ .text('title', field.title)
694
+ .text('description', field.description)
695
+ .part('options', saidWords(field.options)).data))
696
+ .part('commands', saidWords(words.commands))
697
+ .part('modes', parts(words.modes, mode => new Writer().text('label', mode.label).part('options', parts(mode.options, option => new Writer()
698
+ .text('label', option.label)
699
+ .text('description', option.description).data)).data))
700
+ .part('profiles', parts(words.profiles, profile => new Writer()
701
+ .text('label', profile.label)
702
+ .text('description', profile.description).data)).data;
703
+ }
560
704
  function dumpInterface(spec, kind) {
561
705
  const writer = new Writer()
562
706
  // A layout is written when it is not the one its kind starts with.
563
707
  .value('layout', spec.layout, DEFAULT_LAYOUTS[kind])
564
708
  .value('accent', spec.accent, 'green')
565
709
  .text('welcome', spec.welcome)
566
- .list('starters', spec.starters.map(starter => ({
567
- label: starter.label,
568
- message: starter.message,
569
- })))
710
+ .list('starters', spec.starters.map(dumpStarter))
570
711
  .list('commands', spec.commands.map(command => ({
571
712
  name: command.name,
572
713
  description: command.description,
@@ -587,8 +728,30 @@ function dumpInterface(spec, kind) {
587
728
  }
588
729
  return written.data;
589
730
  }))
590
- .part('settings', spec.settings ? { ...spec.settings } : {})
591
- .list('components', spec.components);
731
+ .list('profiles', (spec.profiles ?? []).map(profile => new Writer()
732
+ .text('id', profile.id, '\u0000')
733
+ .text('label', profile.label, '\u0000')
734
+ .text('description', profile.description)
735
+ .text('instructions', profile.instructions)
736
+ .text('model', profile.model ?? '')
737
+ .list('starters', profile.starters.map(dumpStarter)).data))
738
+ .part('settings', spec.settings ? { ...spec.settings } : {});
739
+ // How its settings' fields are drawn (P-20): written when said.
740
+ if (spec.settingsUi) {
741
+ writer.data.settings_ui = { ...spec.settingsUi };
742
+ }
743
+ writer
744
+ .text('language', spec.language ?? DEFAULT_LANGUAGE, DEFAULT_LANGUAGE)
745
+ .part('translations', mapValues(spec.translations ?? {}, dumpTranslation));
746
+ // What a person may send without being asked (P-21): written when said.
747
+ if (spec.uploads) {
748
+ writer.data.uploads = new Writer()
749
+ .list('kinds', spec.uploads.kinds.map(kind => new Writer()
750
+ .text('type', kind.type, '\u0000')
751
+ .value('max_mb', kind.maxMb, DEFAULT_UPLOAD_MB).data))
752
+ .value('max_files', spec.uploads.maxFiles, DEFAULT_MAX_FILES).data;
753
+ }
754
+ writer.list('components', spec.components);
592
755
  if (spec.surface) {
593
756
  writer.data.surface = dumpSurface(spec.surface);
594
757
  }
@@ -664,6 +827,16 @@ export function dumpAppspec(app) {
664
827
  .text('model', app.model)
665
828
  .list('skills', app.skills)
666
829
  .list('backend_tools', app.backendTools)
830
+ .list('tools', (app.tools ?? []).map(tool => ({
831
+ name: tool.name,
832
+ description: tool.description,
833
+ // No arguments is what it is unless said.
834
+ ...(JSON.stringify(tool.parameters) ===
835
+ JSON.stringify(NO_HOST_PARAMETERS)
836
+ ? {}
837
+ : { parameters: tool.parameters }),
838
+ does: tool.does,
839
+ })))
667
840
  .list('context', app.context)
668
841
  .list('contents', app.contents)
669
842
  .list('connections', app.connections.map(connection => new Writer()
@@ -692,6 +865,7 @@ export function dumpAppspec(app) {
692
865
  .list('cases', app.tests.cases.map(testCase => ({
693
866
  ask: testCase.ask,
694
867
  expect: testCase.expect,
868
+ ...(testCase.code ? { code: testCase.code } : {}),
695
869
  })))
696
870
  .part('verified', new Writer()
697
871
  .list('live', app.tests.verified.live)
@@ -704,7 +878,12 @@ export function dumpAppspec(app) {
704
878
  .part('checks', new Writer()
705
879
  .list('guards', app.checks.guards)
706
880
  .list('gates', app.checks.gates)
707
- .text('track', app.checks.track).data)
881
+ .text('track', app.checks.track)
882
+ .list('code', (app.checks.code ?? []).map(check => ({
883
+ name: check.name,
884
+ on: check.on,
885
+ description: check.description,
886
+ }))).data)
708
887
  .part('deployment', new Writer()
709
888
  .present('hosted', app.deployment.hosted &&
710
889
  new Writer()
@@ -14,6 +14,12 @@ export interface AppCheck {
14
14
  * UI plugins, by the name a surface gives it.
15
15
  */
16
16
  export declare const componentNamed: (name: string) => ComponentSpec | undefined;
17
+ /**
18
+ * What its code declares that agentspecs refuses (LOOP P-06): its tools,
19
+ * its checks and the tests it decides, each by a Python name, each name
20
+ * once; a tool says what it does, by class, each class once.
21
+ */
22
+ export declare function ownCodeProblems(app: AppSpec): string[];
17
23
  /** How an organization's own context is named: never a catalogue id (agentspecs' `ORGANIZATION_FRAME_PREFIX`). */
18
24
  /**
19
25
  * What stops a Form block from asking (LOOP C-16), in agentspecs'
@@ -43,6 +43,9 @@ import { APP_THEME_MODES, APP_THEME_VARIANTS, isAssistantCharacterId, parseAppsp
43
43
  import { classesOf, splitRef, toolBehaviours } from './rules';
44
44
  import { COMMAND_INPUT, COMMAND_NAME, MODE_ID } from './composer';
45
45
  import { HOST_NAME, hostToolsOf } from './hostTools';
46
+ import { MAX_UPLOAD_MB, UPLOAD_KIND } from './uploads';
47
+ import { formUiProblems } from './settingsInputs';
48
+ import { translationProblems } from './language';
46
49
  export const NOT_READY = 'Not ready';
47
50
  export const NEEDS_ATTENTION = 'Needs attention';
48
51
  export const PASSES = 'Passes the instant checks';
@@ -80,6 +83,54 @@ const matches = (name, pattern) => new RegExp('^' +
80
83
  ? '[\\s\\S]'
81
84
  : c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('') +
82
85
  '$').test(name);
86
+ /** The name of a function of an application's code (agentspecs' `CODE_NAME`). */
87
+ const CODE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
88
+ /**
89
+ * What its code declares that agentspecs refuses (LOOP P-06): its tools,
90
+ * its checks and the tests it decides, each by a Python name, each name
91
+ * once; a tool says what it does, by class, each class once.
92
+ */
93
+ export function ownCodeProblems(app) {
94
+ const problems = [];
95
+ const tools = app.tools ?? [];
96
+ for (const tool of tools) {
97
+ if (!CODE_NAME.test(tool.name)) {
98
+ problems.push(`Cannot use “${tool.name}” as a tool's name: letters, digits and \`_\`, as in Python.`);
99
+ }
100
+ if (!tool.description.trim()) {
101
+ problems.push(`The tool “${tool.name}” says what it does, for the agent.`);
102
+ }
103
+ if (tool.does.length === 0) {
104
+ problems.push(`The tool “${tool.name}” says what it does: read, write, send, buy, delete or publish.`);
105
+ }
106
+ for (const action of tool.does) {
107
+ if (!CLASS_NAMES.has(action)) {
108
+ problems.push(`The tool “${tool.name}” does “${action}”, which is no class of action.`);
109
+ }
110
+ }
111
+ if (new Set(tool.does).size !== tool.does.length) {
112
+ problems.push('A tool says each thing it does once.');
113
+ }
114
+ }
115
+ const names = tools.map(tool => tool.name);
116
+ if (new Set(names).size !== names.length) {
117
+ problems.push('Two of its tools have the same name.');
118
+ }
119
+ const checks = (app.checks.code ?? []).map(check => check.name);
120
+ for (const name of checks) {
121
+ if (!CODE_NAME.test(name)) {
122
+ problems.push(`Cannot use “${name}” as a check's name: letters, digits and \`_\`, as in Python.`);
123
+ }
124
+ }
125
+ if (new Set(checks).size !== checks.length) {
126
+ problems.push('Two checks of its code have the same name.');
127
+ }
128
+ const decided = app.tests.cases.flatMap(testCase => testCase.code ? [testCase.code] : []);
129
+ if (new Set(decided).size !== decided.length) {
130
+ problems.push('Two tests are decided by the same function of its code.');
131
+ }
132
+ return problems;
133
+ }
83
134
  /** What the spec says of itself that the tolerant reader lets through. */
84
135
  function shapeProblems(app) {
85
136
  const problems = [];
@@ -120,6 +171,36 @@ function shapeProblems(app) {
120
171
  else if (app.triggers.length > 0) {
121
172
  problems.push(`A ${app.kind} application starts when somebody opens it: \`triggers\` are a worker's.`);
122
173
  }
174
+ // Its profiles (LOOP P-20): two at least, or none; each id slash-safe and
175
+ // said once, each with a label.
176
+ const profiles = app.interface.profiles ?? [];
177
+ if (profiles.length === 1) {
178
+ problems.push('One profile is the application itself: say two at least, or put its instructions, model and starters on the application.');
179
+ }
180
+ const profileIds = new Set();
181
+ for (const profile of profiles) {
182
+ if (!MODE_ID.test(profile.id)) {
183
+ problems.push(`Cannot use “${profile.id}” as a profile's id: lower-case letters, digits, \`_\` and \`-\`, a letter first.`);
184
+ }
185
+ if (!profile.label.trim()) {
186
+ problems.push(`The profile “${profile.id}” has no label.`);
187
+ }
188
+ if (profileIds.has(profile.id)) {
189
+ problems.push(`Two profiles are “${profile.id}”.`);
190
+ }
191
+ profileIds.add(profile.id);
192
+ }
193
+ // How its settings' fields are drawn (P-20): by a widget that draws each.
194
+ if (app.interface.settingsUi) {
195
+ if (!app.interface.settings) {
196
+ problems.push('`settings_ui` draws the settings’ fields: say `settings` first.');
197
+ }
198
+ else {
199
+ problems.push(...formUiProblems('The form “settings”', app.interface.settings, app.interface.settingsUi));
200
+ }
201
+ }
202
+ // Its words in other languages (P-26): of languages, of what it says.
203
+ problems.push(...translationProblems(app.interface));
123
204
  // What the host page passes and offers (D-10): named once each, in words
124
205
  // its agent's tools can carry, each function saying what it does.
125
206
  const host = app.deployment.embedded?.host;
@@ -154,6 +235,8 @@ function shapeProblems(app) {
154
235
  }
155
236
  }
156
237
  }
238
+ // What its code declares (LOOP P-06), as agentspecs refuses it.
239
+ problems.push(...ownCodeProblems(app));
157
240
  const servers = app.connections.map(connection => idOf(connection.server));
158
241
  if (new Set(servers).size !== servers.length) {
159
242
  problems.push('The application connects to the same server twice.');
@@ -212,6 +295,10 @@ export function formProblems(node) {
212
295
  if (missing.length > 0) {
213
296
  problems.push(`${said} requires ${missing.map(name => `“${String(name)}”`).join(', ')}, which it does not ask.`);
214
297
  }
298
+ // How its fields are drawn (P-20): by a widget that draws each.
299
+ if (node.ui !== undefined && node.ui !== null) {
300
+ problems.push(...formUiProblems(said, schema, node.ui));
301
+ }
215
302
  return problems;
216
303
  }
217
304
  const isPlainRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -278,6 +365,12 @@ function referenceProblems(app, organizationFrames) {
278
365
  if (app.model && !getModel(app.model)) {
279
366
  missing('model', app.model);
280
367
  }
368
+ // The model a profile runs on is the catalogue's (LOOP P-20).
369
+ for (const profile of app.interface.profiles ?? []) {
370
+ if (profile.model && !getModel(profile.model)) {
371
+ problems.push(`The profile “${profile.id}” runs on “${profile.model}”, which is no model.`);
372
+ }
373
+ }
281
374
  // The model a mode runs on is the catalogue's (LOOP P-19).
282
375
  for (const mode of app.interface.modes ?? []) {
283
376
  for (const option of mode.options) {
@@ -336,6 +429,9 @@ function referenceProblems(app, organizationFrames) {
336
429
  if (server === undefined) {
337
430
  if (hostTools.includes(target))
338
431
  continue;
432
+ // A tool of its own, written in its code (LOOP P-06).
433
+ if ((app.tools ?? []).some(tool => tool.name === target))
434
+ continue;
339
435
  if (!getBackendToolSpec(name)) {
340
436
  problems.push(`The rule “${rule.action}” names the tool “${target}”, which the catalogue does not have.`);
341
437
  }
@@ -646,9 +742,44 @@ export function documentShapeProblems(document) {
646
742
  }
647
743
  text(ui.welcome, 'interface.welcome');
648
744
  texts(ui.components, 'interface.components');
649
- records(ui.starters, 'interface.starters', (item, where) => {
745
+ const starter = (item, where) => {
650
746
  required(item, 'label', where);
651
747
  required(item, 'message', where);
748
+ text(item.category, `${where}.category`);
749
+ unknownKeys(item, ['label', 'message', 'category'], where, 'a starter');
750
+ };
751
+ records(ui.starters, 'interface.starters', starter);
752
+ // Its profiles (LOOP P-20), as agentspecs refuses them: no key unknown.
753
+ records(ui.profiles, 'interface.profiles', (profile, where) => {
754
+ required(profile, 'id', where);
755
+ required(profile, 'label', where);
756
+ for (const key of [
757
+ 'id',
758
+ 'label',
759
+ 'description',
760
+ 'instructions',
761
+ 'model',
762
+ ]) {
763
+ text(profile[key], `${where}.${key}`);
764
+ }
765
+ unknownKeys(profile, ['id', 'label', 'description', 'instructions', 'model', 'starters'], where, 'a profile');
766
+ records(profile.starters, `${where}.starters`, starter);
767
+ });
768
+ text(ui.language, 'interface.language');
769
+ mapping(ui.translations, 'interface.translations', translations => {
770
+ for (const [tag, words] of Object.entries(translations)) {
771
+ mapping(words, `interface.translations.${tag}`, said => unknownKeys(said, [
772
+ 'name',
773
+ 'description',
774
+ 'welcome',
775
+ 'starters',
776
+ 'categories',
777
+ 'settings',
778
+ 'commands',
779
+ 'modes',
780
+ 'profiles',
781
+ ], `interface.translations.${tag}`, 'a translation'));
782
+ }
652
783
  });
653
784
  // Its commands and modes in the composer (LOOP P-19), as agentspecs
654
785
  // refuses them: names and ids slash-safe and said once, no key unknown.
@@ -738,6 +869,44 @@ export function documentShapeProblems(document) {
738
869
  problems.push(...formProblems({ id: 'settings', schema: settings }));
739
870
  });
740
871
  }
872
+ // What a person may send without being asked (LOOP P-21), as agentspecs
873
+ // refuses it: a kind at least, each a type said once, sizes it takes.
874
+ mapping(ui.uploads, 'interface.uploads', uploads => {
875
+ unknownKeys(uploads, ['kinds', 'max_files'], 'interface.uploads', 'uploads');
876
+ required(uploads, 'kinds', 'interface.uploads');
877
+ const types = new Set();
878
+ records(uploads.kinds, 'interface.uploads.kinds', (kind, where) => {
879
+ required(kind, 'type', where);
880
+ unknownKeys(kind, ['type', 'max_mb'], where, 'a kind of upload');
881
+ if (kind.type !== undefined) {
882
+ if (typeof kind.type !== 'string' || !UPLOAD_KIND.test(kind.type)) {
883
+ at(`${where}.type`, 'is a media type (application/pdf), a family (image/*) or an extension (.csv), lowercase');
884
+ }
885
+ else if (types.has(kind.type)) {
886
+ at(`${where}.type`, `names ${kind.type} a second time`);
887
+ }
888
+ else {
889
+ types.add(kind.type);
890
+ }
891
+ }
892
+ const size = kind.max_mb;
893
+ if (size !== undefined &&
894
+ (typeof size !== 'number' || size <= 0 || size > MAX_UPLOAD_MB)) {
895
+ at(`${where}.max_mb`, `is more than 0 and ${MAX_UPLOAD_MB} at most`);
896
+ }
897
+ });
898
+ if (Array.isArray(uploads.kinds) && uploads.kinds.length === 0) {
899
+ at('interface.uploads.kinds', 'are one at least');
900
+ }
901
+ const most = uploads.max_files;
902
+ if (most !== undefined &&
903
+ (typeof most !== 'number' ||
904
+ !Number.isInteger(most) ||
905
+ most < 1 ||
906
+ most > 20)) {
907
+ at('interface.uploads.max_files', 'is a whole number from 1 to 20');
908
+ }
909
+ });
741
910
  mapping(ui.surface, 'interface.surface', surface => {
742
911
  records(surface.components, 'interface.surface.components', (item, where) => {
743
912
  required(item, 'id', where);
@@ -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;