@datalayer/agent-runtimes 1.3.94 → 1.3.96

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 (44) hide show
  1. package/dist-embed/chunks/AgentInspector-DaMCROPs.js +1 -0
  2. package/dist-embed/chunks/{AgentInspectorDialog-LBbHMnDa.js → AgentInspectorDialog-zGMiFR5z.js} +1 -1
  3. package/dist-embed/chunks/ChatView-DE-jo1oJ.js +288 -0
  4. package/dist-embed/datalayer-app-main.js +110 -110
  5. package/dist-embed/datalayer-app.js +1 -1
  6. package/lib/apps/__tests__/fixtures/sceneCheckCases.json +352 -0
  7. package/lib/apps/apps/computer.d.ts +25 -1
  8. package/lib/apps/apps/computer.js +45 -2
  9. package/lib/apps/apps/sceneChecks.d.ts +92 -0
  10. package/lib/apps/apps/sceneChecks.js +649 -0
  11. package/lib/apps/apps/sceneShape.d.ts +8 -0
  12. package/lib/apps/apps/sceneShape.js +191 -0
  13. package/lib/apps/apps/sceneYaml.d.ts +64 -0
  14. package/lib/apps/apps/sceneYaml.js +253 -0
  15. package/lib/apps/core/canvasParts.d.ts +28 -0
  16. package/lib/apps/core/canvasParts.js +14 -0
  17. package/lib/apps/core/index.d.ts +1 -0
  18. package/lib/apps/core/index.js +2 -0
  19. package/lib/apps/embed/loader.js +10 -0
  20. package/lib/apps/plugins/agents/SandboxSelector.js +13 -2
  21. package/lib/apps/plugins/agents/host.d.ts +2 -0
  22. package/lib/apps/plugins/agents/host.js +16 -0
  23. package/lib/apps/plugins/agents/plugin.js +10 -2
  24. package/lib/apps/plugins/agents/switchable.d.ts +12 -0
  25. package/lib/apps/plugins/agents/switchable.js +23 -0
  26. package/lib/apps/plugins/app-computer/AppComputer.js +18 -2
  27. package/lib/apps/plugins/canvas-parts/index.d.ts +44 -0
  28. package/lib/apps/plugins/canvas-parts/index.js +209 -0
  29. package/lib/components/teams/useA2ATeam.js +1 -1
  30. package/lib/examples/HomeExample.js +1 -1
  31. package/lib/examples/ScenesExample.d.ts +1 -1
  32. package/lib/examples/example-selector.js +1 -1
  33. package/lib/hooks/useAgUi.js +4 -1
  34. package/lib/protocols/AGUIAdapter.js +5 -1
  35. package/lib/protocols/agUiRefusal.d.ts +47 -0
  36. package/lib/protocols/agUiRefusal.js +87 -0
  37. package/lib/specs/apps.js +32 -24
  38. package/lib/specs/scenes.js +147 -142
  39. package/lib/specs/teams/teams.js +13 -13
  40. package/package.json +3 -1
  41. package/scripts/dist-embed.mjs +116 -0
  42. package/scripts/record-scene-checks.py +600 -0
  43. package/dist-embed/chunks/AgentInspector-DfPciQ79.js +0 -1
  44. package/dist-embed/chunks/ChatView-BzFbN_mp.js +0 -288
@@ -0,0 +1,649 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * One validation of a scene (plans/STUDIO.md S-10), said in agentspecs' own
7
+ * sentences: what the spec editor refuses, the stage refuses and `loop`
8
+ * refuses, in the same words wherever a person meets them.
9
+ *
10
+ * This is the browser's half of `agentspecs.scenes.scene_problems` — the
11
+ * same checks, in the same order, each sentence written exactly as the
12
+ * Python writes it, so that a person who reads a refusal here reads it
13
+ * again word for word from `loop scenes rehearse`. Both surfaces of the
14
+ * Studio read this module and nothing else: the stage's line above the
15
+ * board and the panel under the spec editor are the same list.
16
+ *
17
+ * The order is agentspecs' order, because its first layer stops: a cast
18
+ * that makes no team is one sentence and nothing more.
19
+ *
20
+ * 1. the team on stage (`SceneSpec.team_of`): the team it names, or the one
21
+ * its cast makes — a supervisor with no definition, a member that is two
22
+ * things at once, a system that asks, a link to nobody, a link over the
23
+ * wrong protocol. Said, as agentspecs says it, under *Its cast makes no
24
+ * team*;
25
+ * 2. the scene over that team: its entry, its face, its setting, its
26
+ * script beat by beat, its stage directions, its audience, where it
27
+ * plays, its rehearsal.
28
+ *
29
+ * Each sentence carries the section of the spec it is about, so that the
30
+ * stage's own refusals (S-09: who runs where, what the audience may do) are
31
+ * read on their own without a second set of checks.
32
+ *
33
+ * **What stays agentspecs'.** Three checks need more than the browser has,
34
+ * and are made when the scene is loaded by `loop`: a tool a system does not
35
+ * offer or offers for something else (`_tool_problem`, the action classes of
36
+ * a running server); a connection a member of the person's *own*
37
+ * applications reaches a system through (the catalogue's applications are
38
+ * read here, an application kept in a Space is not); and a rehearsal's
39
+ * recording beside the scenes on disk. They are named here so that nobody
40
+ * writes them a second time.
41
+ *
42
+ * Pure: no React, no network.
43
+ *
44
+ * It lives in agent-runtimes beside the application's checks (`checks.ts`),
45
+ * so that every shell that edits a scene — the Studio, and agent-runtimes' own —
46
+ * refuses the same things in the same words (plans/STUDIO.md S-10; moved from
47
+ * the landing 2026-10-10). `__tests__/scene-checks.test.ts` holds it to what
48
+ * agentspecs itself says, sentence for sentence.
49
+ *
50
+ * @module apps/apps/sceneChecks
51
+ */
52
+ import { SERVER_ACTIONS } from '../../specs/actions';
53
+ import { APP_CATALOGUE } from '../../specs/apps';
54
+ import { FRAME_CATALOGUE } from '../../specs/frames';
55
+ import { MCP_SERVER_LIBRARY } from '../../specs/mcpServers';
56
+ import { getTeamSpec } from '../../specs/teams';
57
+ import { classesOf, matchesPattern } from './rules';
58
+ /**
59
+ * The sections a stage is written in (S-09): where each player runs and
60
+ * what it is given (`stage`, `deployment`, a member's `runs_in`), and what
61
+ * the audience may do (`audience`).
62
+ */
63
+ export const STAGE_SECTIONS = ['stage', 'audience'];
64
+ /**
65
+ * The parts a person reads one at a time in the spec editor (S-08, S-09),
66
+ * in the order a scene is written. The scene's own fields are always in
67
+ * view, so they are not one of them.
68
+ */
69
+ export const SCENE_PARTS = [
70
+ 'setting',
71
+ 'cast',
72
+ 'script',
73
+ 'stage',
74
+ 'audience',
75
+ 'rehearsal',
76
+ ];
77
+ /**
78
+ * Each section of a scene, and the keys of the spec it is written in.
79
+ * *Where it plays* is the stage directions and the addresses together: where
80
+ * each player runs is one thing, said in two keys. Read by the editor's
81
+ * parts (S-09) and by `sceneShape`, which files a refusal of the scene's
82
+ * shape under the part whose key it names.
83
+ */
84
+ export const SCENE_SECTION_KEYS = {
85
+ scene: ['name', 'description', 'emoji', 'icon', 'tags', 'version'],
86
+ cast: ['entry', 'team', 'cast'],
87
+ setting: ['setting'],
88
+ script: ['script'],
89
+ stage: ['stage', 'deployment'],
90
+ audience: ['audience'],
91
+ rehearsal: ['rehearsal'],
92
+ };
93
+ /** The section a key of the spec is written in; the scene's own otherwise. */
94
+ export const sectionOfKey = (key) => Object.entries(SCENE_SECTION_KEYS).find(([, keys]) => keys.includes(key))?.[0] ?? 'scene';
95
+ /** What a person reads each section as, in the editor's panel. */
96
+ export const SECTION_WORDS = {
97
+ scene: 'The scene',
98
+ cast: 'Who is on stage',
99
+ setting: 'What is on stage',
100
+ script: 'What happens',
101
+ stage: 'Where it plays',
102
+ audience: 'Who may watch',
103
+ rehearsal: 'Its rehearsal',
104
+ };
105
+ /**
106
+ * The checks that are not the browser's: made by agentspecs when `loop`
107
+ * loads the scene. Said here so that no second set of them is written.
108
+ */
109
+ export const AGENTSPECS_OWN = [
110
+ "A rehearsal's recording, which is a file beside the scenes.",
111
+ ];
112
+ const idOf = (ref) => (ref ?? '').split(':')[0];
113
+ /** As Python writes a string in a sentence (`repr`): in single quotes. */
114
+ const quoted = (value) => `'${value}'`;
115
+ const isServer = (member) => Boolean(member.server);
116
+ const personaName = (member) => member.persona?.name ?? '';
117
+ /** The entry agentspecs reads: the one said, else the initiator, else the first of the cast. */
118
+ export function entryOf(spec) {
119
+ if (spec.entry) {
120
+ return spec.entry;
121
+ }
122
+ const initiator = spec.cast.find(member => member.role === 'initiator');
123
+ return initiator?.member ?? spec.cast[0]?.member ?? '';
124
+ }
125
+ /**
126
+ * Why the cast makes no team, in agentspecs' sentence, or undefined. The
127
+ * order is the order Python builds the team in: its supervisor, then each
128
+ * member of the cast, then the team over them — and the first that fails is
129
+ * the only one said.
130
+ */
131
+ function castMakesNoTeam(spec) {
132
+ const cast = spec.cast;
133
+ const entry = entryOf(spec);
134
+ const front = cast.find(member => member.member === entry);
135
+ if (!front) {
136
+ // Raised as a scene's own refusal, not under *Its cast makes no team*.
137
+ return undefined;
138
+ }
139
+ const said = (why) => `Its cast makes no team: ${why}`;
140
+ // The supervisor: the member the audience talks to, as the team's front.
141
+ const name = personaName(front) || front.member;
142
+ if (front.ref && front.app) {
143
+ return said(`supervisor ${quoted(name)} is an agent (\`ref\`) or an application (\`app\`), not both`);
144
+ }
145
+ if (!front.ref && !front.app) {
146
+ return said(`supervisor ${quoted(name)} needs a \`ref\` into the agent catalogue, an \`app\` from the application catalogue, or a \`model\` and \`instructions\` of its own`);
147
+ }
148
+ // Each member of the cast, in its order.
149
+ for (const member of cast) {
150
+ const given = [member.ref, member.app, member.server].filter(Boolean).length;
151
+ if (given > 1) {
152
+ return said(`member ${quoted(member.member)} is an agent (\`ref\`), an application (\`app\`) or a server (\`server\`), not two of them`);
153
+ }
154
+ if (given === 0 && !personaName(member) && !member.brief) {
155
+ return said(`member ${quoted(member.member)} needs a \`ref\` into the agent catalogue, an \`app\` from the application catalogue, a \`server\` from the MCP server catalogue, or enough of its own definition (\`name\`, \`goal\`) to stand alone`);
156
+ }
157
+ if (member.server && (member.talksTo ?? []).length) {
158
+ return said(`member ${quoted(member.member)} is a server: it answers over MCP and asks nobody`);
159
+ }
160
+ }
161
+ // The team over them: every link.
162
+ const seen = new Set(cast.map(member => member.member));
163
+ const servers = new Set(cast.filter(isServer).map(member => member.member));
164
+ for (const member of cast) {
165
+ for (const link of member.talksTo ?? []) {
166
+ if (!seen.has(link.member)) {
167
+ return said(`member ${quoted(member.member)} talks to ${quoted(link.member)}, which is not a member of team ${quoted(spec.id)}`);
168
+ }
169
+ if (link.member === member.member) {
170
+ return said(`member ${quoted(member.member)} talks to itself`);
171
+ }
172
+ if (link.over === 'mcp' && !servers.has(link.member)) {
173
+ return said(`member ${quoted(member.member)} talks to ${quoted(link.member)} over mcp, which is not a server: an agent or an application is asked over a2a`);
174
+ }
175
+ if (link.over !== 'mcp' && servers.has(link.member)) {
176
+ return said(`member ${quoted(member.member)} talks to ${quoted(link.member)} over ${link.over}, which is a server: a server is asked over mcp`);
177
+ }
178
+ }
179
+ }
180
+ return undefined;
181
+ }
182
+ /** A member's application: the catalogue's, else one of the person's own. */
183
+ function appOf(ref, own) {
184
+ if (!ref) {
185
+ return undefined;
186
+ }
187
+ const id = idOf(ref);
188
+ const catalogued = APP_CATALOGUE[id];
189
+ return (catalogued ??
190
+ (own && Object.prototype.hasOwnProperty.call(own, id) ? own[id] : undefined));
191
+ }
192
+ /** Whether a member reaches a system: through its application's connection, or a link over MCP. */
193
+ function reaches(member, cast, serverId, own) {
194
+ const app = appOf(member.app, own);
195
+ if (app &&
196
+ app.connections.some(connection => idOf(connection.server) === serverId)) {
197
+ return true;
198
+ }
199
+ return (member.talksTo ?? []).some(link => {
200
+ const asked = cast.find(other => other.member === link.member);
201
+ return (link.over === 'mcp' &&
202
+ asked !== undefined &&
203
+ idOf(asked.server) === serverId);
204
+ });
205
+ }
206
+ /** A system of the setting by its id, its reference or the name the audience reads, whatever the case. */
207
+ function systemNamed(spec, name) {
208
+ const wanted = name.trim().toLowerCase();
209
+ return spec.setting.systems.find(system => {
210
+ const shown = (system.as || idOf(system.server)).toLowerCase();
211
+ return (wanted === idOf(system.server) ||
212
+ wanted === system.server.toLowerCase() ||
213
+ wanted === shown);
214
+ });
215
+ }
216
+ /** What a system is read as: `as`, or its id. */
217
+ const systemName = (system) => system.as || idOf(system.server);
218
+ /** What is wrong with one move of a beat, in agentspecs' sentences. */
219
+ function moveProblems(spec, beatId, move, links, own) {
220
+ const where = `Beat '${beatId}':`;
221
+ const mover = spec.cast.find(member => member.member === move.who);
222
+ if (!mover) {
223
+ return [`${where} '${move.who}' moves, and is not in the cast.`];
224
+ }
225
+ if (isServer(mover)) {
226
+ return [
227
+ `${where} '${move.who}' moves, and a system answers tool by tool: it asks nobody.`,
228
+ ];
229
+ }
230
+ if (!move.asks) {
231
+ return [];
232
+ }
233
+ const asked = spec.cast.find(member => member.member === move.asks);
234
+ let system = systemNamed(spec, move.asks);
235
+ if (asked && isServer(asked)) {
236
+ system = system ?? { server: asked.server, as: '', holds: '' };
237
+ }
238
+ if (system) {
239
+ if (move.over === 'a2a') {
240
+ return [
241
+ `${where} '${move.who}' asks '${move.asks}' over a2a, a system: a system is asked over mcp.`,
242
+ ];
243
+ }
244
+ if (!reaches(mover, spec.cast, idOf(system.server), own)) {
245
+ return [
246
+ `${where} '${move.who}' asks '${move.asks}' (${idOf(system.server)}), which it reaches through no connection.`,
247
+ ];
248
+ }
249
+ if (move.tool) {
250
+ const why = toolProblem(mover, personaNameOf(resolvedCast(spec.cast, own), mover.member), idOf(system.server), move.tool, move.does, own);
251
+ return why
252
+ ? [`${where} '${move.who}' asks '${move.asks}' for ${why}`]
253
+ : [];
254
+ }
255
+ return [];
256
+ }
257
+ if (move.over === 'mcp') {
258
+ return [
259
+ `${where} '${move.who}' asks '${move.asks}' over mcp, which is not a system of the scene.`,
260
+ ];
261
+ }
262
+ if (!asked) {
263
+ return [
264
+ `${where} '${move.who}' asks '${move.asks}', which is not in the cast.`,
265
+ ];
266
+ }
267
+ if (!links.has(`${move.who}\u0000${move.asks}`)) {
268
+ return [
269
+ `${where} '${move.who}' asks '${move.asks}', which the team does not have it talk to.`,
270
+ ];
271
+ }
272
+ return [];
273
+ }
274
+ /** What the transcript calls the audience (agentspecs' `AUDIENCE`). */
275
+ const AUDIENCE = 'You';
276
+ const ASK = /^([^→:]+?)\s*→\s*([^:]+?)(?::\s*(.+))?$/;
277
+ const ANSWER = /^([^→:]+?):\s*(.+)$/;
278
+ /**
279
+ * A line of a rehearsal's shape, as agentspecs reads it (`parse_line`):
280
+ * *Sales → Accounting* (an ask), *Accounting → Odoo: odoo_*\** (a tool),
281
+ * *Accounting: a table* (an answer).
282
+ */
283
+ export function transcriptLineOf(text) {
284
+ const asked = ASK.exec(text.trim());
285
+ if (asked) {
286
+ return {
287
+ who: asked[1].trim(),
288
+ whom: asked[2].trim(),
289
+ detail: (asked[3] ?? '').trim(),
290
+ };
291
+ }
292
+ const answered = ANSWER.exec(text.trim());
293
+ if (answered) {
294
+ return { who: answered[1].trim(), whom: '', detail: answered[2].trim() };
295
+ }
296
+ return undefined;
297
+ }
298
+ /** Every name a rehearsal's line may use, to the id it stands for. */
299
+ function namesOf(spec) {
300
+ // Every name the transcript may use, to the id it stands for (`_names`).
301
+ const names = new Map([[AUDIENCE, AUDIENCE]]);
302
+ for (const member of spec.cast) {
303
+ names.set(member.member, member.member);
304
+ if (personaName(member)) {
305
+ names.set(personaName(member), member.member);
306
+ }
307
+ }
308
+ for (const system of spec.setting.systems) {
309
+ const id = idOf(system.server);
310
+ names.set(id, id);
311
+ names.set(system.server, id);
312
+ names.set(systemName(system), id);
313
+ names.set(systemName(system).toLowerCase(), id);
314
+ }
315
+ return names;
316
+ }
317
+ /** A member's persona name in the cast, as agentspecs reads it (`_persona_name`). */
318
+ function personaNameOf(cast, memberId) {
319
+ const said = cast.find(member => member.member === memberId);
320
+ return said ? (said.persona?.name ?? '') : memberId;
321
+ }
322
+ /**
323
+ * Why a tool of a system is not one a member may use, or undefined — in
324
+ * agentspecs' words (`_tool_problem`): a tool its application's connections
325
+ * to that server do not reach, a tool the server does not offer, or one it
326
+ * offers for something other than what the move asks of it. What a server
327
+ * offers is the catalogue's (`SERVER_ACTIONS`), read with the same pattern
328
+ * matching and the same classes as a call's rules (`rules.ts`).
329
+ */
330
+ function toolProblem(member, name, serverId, tool, does, own) {
331
+ const app = appOf(member.app, own);
332
+ if (app) {
333
+ const connections = app.connections.filter(connection => idOf(connection.server) === serverId);
334
+ const reached = (only) => !only?.length || only.some(pattern => matchesPattern(tool, pattern));
335
+ if (connections.length &&
336
+ !connections.some(connection => reached(connection.only))) {
337
+ return `'${tool}', a tool no connection of ${name} offers.`;
338
+ }
339
+ }
340
+ const server = Object.prototype.hasOwnProperty.call(SERVER_ACTIONS, serverId)
341
+ ? SERVER_ACTIONS[serverId]
342
+ : undefined;
343
+ const declared = Object.keys(server?.tools ?? {});
344
+ if (declared.length &&
345
+ !declared.some(known => matchesPattern(known, tool) || matchesPattern(tool, known))) {
346
+ return `'${tool}', which ${serverId} does not offer.`;
347
+ }
348
+ if (does && server && declared.includes(tool)) {
349
+ const classes = classesOf(`${serverId}.${tool}`, {});
350
+ if (classes.length && !classes.includes(does)) {
351
+ return `'${tool}' to ${does}, and it ${classes.join(', ')}s.`;
352
+ }
353
+ }
354
+ return undefined;
355
+ }
356
+ /** What to show a person of a team member: its name, what it references, or its id (`display_name`). */
357
+ const displayName = (member) => member.name ||
358
+ idOf(member.ref || member.app || member.server || '') ||
359
+ member.id;
360
+ /**
361
+ * The cast a named team puts on stage, as its members (`cast_of`): each
362
+ * with its persona filled — the cast's name, else its application's, else
363
+ * what to show a person; the cast's face, else its application's.
364
+ */
365
+ function castOfTeam(spec, team) {
366
+ if (!team) {
367
+ return spec.cast;
368
+ }
369
+ return team.agents.map((member) => {
370
+ const said = spec.cast.find(one => one.member === member.id);
371
+ const app = member.app ? APP_CATALOGUE[idOf(member.app)] : undefined;
372
+ return {
373
+ member: member.id,
374
+ app: member.app ?? '',
375
+ ref: member.ref ?? '',
376
+ server: member.server ?? '',
377
+ role: member.role ?? '',
378
+ ...(member.runsIn ? { runsIn: member.runsIn } : {}),
379
+ ...(member.talksTo?.length
380
+ ? {
381
+ talksTo: member.talksTo.map(link => ({
382
+ member: link.member,
383
+ over: link.over,
384
+ })),
385
+ }
386
+ : {}),
387
+ persona: {
388
+ name: said?.persona?.name || (app ? app.name : displayName(member)),
389
+ face: said?.persona?.face || (app ? app.emoji : ''),
390
+ line: said?.persona?.line ?? '',
391
+ },
392
+ brief: said?.brief || member.goal || '',
393
+ };
394
+ });
395
+ }
396
+ /**
397
+ * The cast with every persona resolved (`cast_of`), for the names a
398
+ * rehearsal may use and the faces a scene may not wear: a member of an
399
+ * inline cast that says no name is read as its application's.
400
+ */
401
+ function resolvedCast(cast, own) {
402
+ return cast.map(member => {
403
+ const app = appOf(member.app, own);
404
+ return {
405
+ ...member,
406
+ persona: {
407
+ ...member.persona,
408
+ name: member.persona?.name ||
409
+ (app
410
+ ? app.name
411
+ : idOf(member.ref || member.app || member.server) || member.member),
412
+ face: member.persona?.face || (app?.emoji ?? ''),
413
+ },
414
+ };
415
+ });
416
+ }
417
+ /**
418
+ * What is wrong with the shape of the scene, as agentspecs reads it
419
+ * (`SceneSpec._stages_somebody`), said as `parse_scene` says it — the
420
+ * scene's id, then the sentence. The first stops, as the reader stops.
421
+ */
422
+ export function sceneShapeProblem(spec) {
423
+ const said = (says, section) => ({
424
+ says: `${spec.id}: ${says}`,
425
+ section,
426
+ });
427
+ if (!spec.team && !spec.cast.length) {
428
+ return said('the scene stages nobody: it names a `team`, or writes its `cast`', 'cast');
429
+ }
430
+ const seen = new Set();
431
+ for (const member of spec.cast) {
432
+ if (seen.has(member.member)) {
433
+ return said(`the cast names '${member.member}' twice`, 'cast');
434
+ }
435
+ seen.add(member.member);
436
+ }
437
+ const beats = new Set();
438
+ for (const beat of spec.script) {
439
+ if (beats.has(beat.id)) {
440
+ return said(`the script has two beats named '${beat.id}'`, 'script');
441
+ }
442
+ beats.add(beat.id);
443
+ }
444
+ // Where each member stands: fractions of the box (`StagePosition`).
445
+ for (const [memberId, at] of Object.entries(spec.stage.positions)) {
446
+ for (const [axis, value] of [
447
+ ['x', at.x],
448
+ ['y', at.y],
449
+ ]) {
450
+ if (value > 1) {
451
+ return said(`stage.positions.${memberId}.${axis}: Input should be less than or equal to 1`, 'stage');
452
+ }
453
+ if (value < 0) {
454
+ return said(`stage.positions.${memberId}.${axis}: Input should be greater than or equal to 0`, 'stage');
455
+ }
456
+ }
457
+ }
458
+ return undefined;
459
+ }
460
+ /**
461
+ * What stops a scene from being played, each in agentspecs' sentence with
462
+ * the section it is about; empty when nothing does.
463
+ */
464
+ export function sceneCheck(spec, own, { written = false, } = {}) {
465
+ // 1. The shape of the scene, as it is read (`parse_scene`): the first stops.
466
+ const shape = sceneShapeProblem(spec);
467
+ if (shape) {
468
+ return [shape];
469
+ }
470
+ // 2. The team on stage.
471
+ if (spec.team) {
472
+ const team = getTeamSpec(idOf(spec.team));
473
+ if (!team) {
474
+ return [
475
+ {
476
+ says: `There is no team named ${quoted(spec.team)}.`,
477
+ section: 'cast',
478
+ },
479
+ ];
480
+ }
481
+ // A scene that names a team says how each member is played, and nothing
482
+ // of what each is: the team says that (`scene_problems`). The editor's text
483
+ // is written with an inline cast and no team, so this speaks only of a
484
+ // team a person wrote — never of a catalogue scene read resolved.
485
+ const ids = new Set(team.agents.map(member => member.id));
486
+ const castProblems = [];
487
+ for (const said of written ? spec.cast : []) {
488
+ if (!ids.has(said.member)) {
489
+ castProblems.push({
490
+ says: `The cast names '${said.member}', which is not a member of the team '${team.id}'.`,
491
+ section: 'cast',
492
+ });
493
+ }
494
+ else if (Boolean(said.app || said.ref || said.server) ||
495
+ said.role != null ||
496
+ said.runsIn != null ||
497
+ Boolean(said.talksTo?.length)) {
498
+ castProblems.push({
499
+ says: `The cast member '${said.member}' is the team's: it says its persona and its brief, nothing of what it is.`,
500
+ section: 'cast',
501
+ });
502
+ }
503
+ }
504
+ return [
505
+ ...castProblems,
506
+ ...sceneOverTeam(spec, castOfTeam(spec, team), own),
507
+ ];
508
+ }
509
+ const entry = entryOf(spec);
510
+ if (!spec.cast.some(member => member.member === entry)) {
511
+ return [
512
+ {
513
+ says: `The scene enters at '${entry}', which is not in its cast.`,
514
+ section: 'cast',
515
+ },
516
+ ];
517
+ }
518
+ const noTeam = castMakesNoTeam(spec);
519
+ if (noTeam) {
520
+ return [{ says: noTeam, section: 'cast' }];
521
+ }
522
+ return sceneOverTeam(spec, spec.cast, own);
523
+ }
524
+ /** The scene read over the team on stage: everything after `team_of` passed. */
525
+ function sceneOverTeam(spec, cast, own) {
526
+ const problems = [];
527
+ const say = (section, says) => problems.push({ says, section });
528
+ const ids = new Set(cast.map(member => member.member));
529
+ const entry = entryOf(spec);
530
+ if (spec.entry && !ids.has(spec.entry)) {
531
+ say('cast', `The scene enters at '${spec.entry}', which is not in its cast.`);
532
+ }
533
+ const front = cast.find(member => member.member === entry);
534
+ if (front && isServer(front)) {
535
+ say('cast', `The scene enters at '${entry}', a system: the audience talks to an agent.`);
536
+ }
537
+ const resolved = resolvedCast(cast, own);
538
+ const faces = resolved.map(member => member.persona?.face).filter(Boolean);
539
+ if (spec.emoji && faces.includes(spec.emoji)) {
540
+ say('scene', `The scene wears ${spec.emoji}, a member's face: a scene has a face of its own.`);
541
+ }
542
+ // The setting.
543
+ for (const system of spec.setting.systems) {
544
+ const id = idOf(system.server);
545
+ if (!MCP_SERVER_LIBRARY[id]) {
546
+ say('setting', `There is no MCP server named ${quoted(system.server)}.`);
547
+ }
548
+ else if (!cast.some(member => reaches(member, cast, id, own) || idOf(member.server) === id)) {
549
+ say('setting', `The system '${systemName(system)}' (${id}) is on stage, and no member of the cast reaches it.`);
550
+ }
551
+ }
552
+ for (const ref of spec.setting.frames ?? []) {
553
+ if (!FRAME_CATALOGUE[idOf(ref)]) {
554
+ say('setting', `There is no Frame named ${quoted(ref)}.`);
555
+ }
556
+ }
557
+ // The script.
558
+ const links = new Set(cast.flatMap(member => (member.talksTo ?? []).map(link => `${member.member}\u0000${link.member}`)));
559
+ const beats = new Set(spec.script.map(beat => beat.id));
560
+ for (const beat of spec.script) {
561
+ let answered = false;
562
+ const moves = [
563
+ ...beat.moves,
564
+ ...(beat.branch ?? []).flatMap(branch => branch.moves ?? []),
565
+ ];
566
+ for (const move of moves) {
567
+ for (const says of moveProblems({ ...spec, cast: cast }, beat.id, move, links, own)) {
568
+ say('script', says);
569
+ }
570
+ answered = answered || move.who === entry;
571
+ }
572
+ if (!answered) {
573
+ say('script', `Beat '${beat.id}': its cue is answered by nobody — no move is the entry's ('${entry}').`);
574
+ }
575
+ for (const branch of beat.branch ?? []) {
576
+ if (branch.then && !beats.has(branch.then)) {
577
+ say('script', `Beat '${beat.id}': its branch goes on to '${branch.then}', which is no beat.`);
578
+ }
579
+ }
580
+ }
581
+ // The stage directions.
582
+ for (const memberId of Object.keys(spec.stage.positions)) {
583
+ if (!ids.has(memberId)) {
584
+ say('stage', `The stage places '${memberId}', which is not in the cast.`);
585
+ }
586
+ }
587
+ if (spec.stage.opensFirst && !ids.has(spec.stage.opensFirst)) {
588
+ say('stage', `The balloon of '${spec.stage.opensFirst}' opens first, and it is not in the cast.`);
589
+ }
590
+ // The audience, and where the scene plays.
591
+ for (const memberId of Object.keys(spec.deployment.addresses)) {
592
+ const member = cast.find(item => item.member === memberId);
593
+ if (!member) {
594
+ say('stage', `An address is read for '${memberId}', which is not in the cast.`);
595
+ }
596
+ else if (member.runsIn === 'browser') {
597
+ say('stage', `'${memberId}' runs in the browser: it has no address.`);
598
+ }
599
+ }
600
+ if (spec.audience.who === 'visitors') {
601
+ for (const member of cast) {
602
+ if (!isServer(member) &&
603
+ member.runsIn === 'runtime' &&
604
+ !(member.member in spec.deployment.addresses)) {
605
+ say('audience', `Visitors may watch, and '${member.member}' runs on a runtime with no address: deployment.addresses names none for it.`);
606
+ }
607
+ }
608
+ }
609
+ // The rehearsal.
610
+ const names = namesOf({ ...spec, cast: resolved });
611
+ const systemIds = new Set(spec.setting.systems.map(system => idOf(system.server)));
612
+ const rehearsed = new Set();
613
+ for (const rehearsal of spec.rehearsal.beats) {
614
+ if (!beats.has(rehearsal.beat)) {
615
+ say('rehearsal', `The rehearsal names the beat '${rehearsal.beat}', which is not in the script.`);
616
+ }
617
+ if (rehearsed.has(rehearsal.beat)) {
618
+ say('rehearsal', `The rehearsal names the beat '${rehearsal.beat}' twice.`);
619
+ }
620
+ rehearsed.add(rehearsal.beat);
621
+ for (const text of rehearsal.lines ?? []) {
622
+ const line = transcriptLineOf(text);
623
+ if (!line) {
624
+ // A line that is neither an ask nor an answer is refused as it is read.
625
+ say('rehearsal', `${spec.id}: the line '${text}' is neither an ask (\`A → B\`) nor an answer (\`A: …\`)`);
626
+ continue;
627
+ }
628
+ for (const name of [line.who, line.whom]) {
629
+ if (name && !names.has(name)) {
630
+ say('rehearsal', `The rehearsal of '${rehearsal.beat}' names '${name}', which is not on stage.`);
631
+ }
632
+ }
633
+ // A tool a member is expected to ask a system for (`_tool_problem`).
634
+ const whom = names.get(line.whom) ?? '';
635
+ const asker = resolved.find(member => member.member === (names.get(line.who) ?? ''));
636
+ if (systemIds.has(whom) && line.detail && asker) {
637
+ const why = toolProblem(asker, personaNameOf(resolved, asker.member), whom, line.detail, null, own);
638
+ if (why) {
639
+ say('rehearsal', `The rehearsal of '${rehearsal.beat}' expects ${why}`);
640
+ }
641
+ }
642
+ }
643
+ }
644
+ return problems;
645
+ }
646
+ /** What stops a scene from being played, in agentspecs' sentences; empty when nothing does. */
647
+ export const sceneProblems = (spec, own) => sceneCheck(spec, own).map(problem => problem.says);
648
+ /** The refusals of one section of the spec: the stage's, for S-09's own panel. */
649
+ export const problemsOfSections = (problems, sections) => problems.filter(problem => sections.includes(problem.section));
@@ -0,0 +1,8 @@
1
+ import { type SceneProblem } from './sceneChecks';
2
+ /**
3
+ * What `loop` says of a scene's shape — every issue, after the scene's id —
4
+ * filed under the part of the scene its first issue is about; nothing when
5
+ * the shape is right. `data` is the text read as plain data, in agentspecs'
6
+ * spelling.
7
+ */
8
+ export declare function sceneShapeSays(data: unknown): SceneProblem | undefined;