@datalayer/agent-runtimes 1.3.86 → 1.3.87

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.
@@ -20,7 +20,7 @@
20
20
  * values the rows, top to bottom.
21
21
  *
22
22
  * A member's connections — the MCP servers it reaches, Odoo for Accounting —
23
- * hang under it, each a node half a member's size with the server's mark
23
+ * stand to its right, each a node half a member's size with the server's mark
24
24
  * (`SpecMark`), linked to it by an edge of its own. That edge is still until
25
25
  * the member calls one of the connection's tools: then it flows toward the
26
26
  * connection for as long as the call runs ({@link A2ATeamCall}), with the
@@ -59,7 +59,7 @@ export type A2ATeamGraphMember = {
59
59
  onToggle?: () => void;
60
60
  /** It was sent away, or called back. */
61
61
  onAway?: (away: boolean) => void;
62
- /** The MCP servers it reaches, drawn under it (`teamConnectionsOf`). */
62
+ /** The MCP servers it reaches, drawn to its right (`teamConnectionsOf`). */
63
63
  connections?: A2ATeamConnection[];
64
64
  /**
65
65
  * How its balloon shows what it says (LOOP T-23): only what it says or
@@ -25,9 +25,15 @@ const WIDTH = NODE_WIDTH * 2 + GAP;
25
25
  const ROW_GAP = 48;
26
26
  /** A connection's node: half a member's. */
27
27
  const CONNECTION_WIDTH = NODE_WIDTH / 2;
28
- /** Between a member and its connections. */
28
+ /** Between a member and its connections, to its right. */
29
29
  const CONNECTION_GAP = 44;
30
30
  const CONNECTIONS_APART = 16;
31
+ /** Where a member's connections start: right of it. */
32
+ const CONNECTION_X = NODE_WIDTH + CONNECTION_GAP;
33
+ /** Below the character's middle, so the A2A edge leaving it passes above. */
34
+ const CONNECTION_DROP = 12;
35
+ /** What a member's connections take beside it. */
36
+ const CONNECTIONS_BESIDE = CONNECTION_GAP + CONNECTION_WIDTH;
31
37
  /** Where the edge leaves and reaches a member: the middle of its character. */
32
38
  const HANDLE = (size) => ({
33
39
  opacity: 0,
@@ -118,7 +124,7 @@ const Moving = createContext({
118
124
  move: () => undefined,
119
125
  reset: () => undefined,
120
126
  });
121
- /** A connection's node id: under its member. */
127
+ /** A connection's node id: beside its member. */
122
128
  const connectionNodeId = (member, connection) => `${member}/${connection}`;
123
129
  const MemberNode = memo(function MemberNode({ data, }) {
124
130
  const { id, side, size } = data;
@@ -282,7 +288,7 @@ const MemberNode = memo(function MemberNode({ data, }) {
282
288
  stayPut: true,
283
289
  // Named as the member, not its character: `Sales (in your
284
290
  // browser)`; the character describes it.
285
- agentName: member.name, label: memberLabel(member) })) }), _jsxs(Text, { sx: { fontWeight: 600, textAlign: 'center' }, children: [member.emoji ? `${member.emoji} ` : '', member.name] }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', textAlign: 'center' }, "data-member-where": "", children: member.where }), _jsx(Handle, { id: MEMBER_HANDLES.outRight, type: "source", position: Position.Right, style: HANDLE(size), isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.outLeft, type: "source", position: Position.Left, style: HANDLE(size), isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.outBottom, type: "source", position: Position.Bottom, style: { ...HANDLE(size), top: size }, isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.outTop, type: "source", position: Position.Top, style: { ...HANDLE(size), top: 0 }, isConnectable: false }), member.connections?.length ? (_jsx(Handle, { id: MEMBER_HANDLES.connections, type: "source", position: Position.Bottom, style: { ...HANDLE(size), top: 'auto', bottom: 0 }, isConnectable: false })) : null] }));
291
+ agentName: member.name, label: memberLabel(member) })) }), _jsxs(Text, { sx: { fontWeight: 600, textAlign: 'center' }, children: [member.emoji ? `${member.emoji} ` : '', member.name] }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', textAlign: 'center' }, "data-member-where": "", children: member.where }), _jsx(Handle, { id: MEMBER_HANDLES.outRight, type: "source", position: Position.Right, style: HANDLE(size), isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.outLeft, type: "source", position: Position.Left, style: HANDLE(size), isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.outBottom, type: "source", position: Position.Bottom, style: { ...HANDLE(size), top: size }, isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.outTop, type: "source", position: Position.Top, style: { ...HANDLE(size), top: 0 }, isConnectable: false }), member.connections?.length ? (_jsx(Handle, { id: MEMBER_HANDLES.connections, type: "source", position: Position.Right, style: { ...HANDLE(size), left: 'auto', right: 0 }, isConnectable: false })) : null] }));
286
292
  });
287
293
  /** A member's accessible name: its name, and where it runs. */
288
294
  export function memberLabel(member) {
@@ -314,7 +320,7 @@ const ConnectionNode = memo(function ConnectionNode({ id, data, }) {
314
320
  gap: '2px',
315
321
  cursor: 'default',
316
322
  pointerEvents: 'all',
317
- }, title: connection.name, "data-team-connection": connection.id, "data-connection-busy": tool ? 'true' : 'false', children: [_jsx(Handle, { type: "target", position: Position.Top, style: { ...HANDLE(0), top: 0 }, isConnectable: false }), _jsx(Box, { as: "button", type: "button", className: tool ? 'a2a-team-connection-busy' : undefined, "aria-label": connectionLabel(connection, owner?.name), "aria-expanded": told, "aria-controls": detailsId, "data-connection-figure": "", onClick: () => setTold(shown => !shown), onKeyDown: (event) => {
323
+ }, title: connection.name, "data-team-connection": connection.id, "data-connection-busy": tool ? 'true' : 'false', children: [_jsx(Handle, { type: "target", position: Position.Left, style: { ...HANDLE(size), left: 0 }, isConnectable: false }), _jsx(Box, { as: "button", type: "button", className: tool ? 'a2a-team-connection-busy' : undefined, "aria-label": connectionLabel(connection, owner?.name), "aria-expanded": told, "aria-controls": detailsId, "data-connection-figure": "", onClick: () => setTold(shown => !shown), onKeyDown: (event) => {
318
324
  if (event.key === 'Escape' && told) {
319
325
  event.stopPropagation();
320
326
  setTold(false);
@@ -419,11 +425,19 @@ export const CallEdge = memo(function CallEdge({ id, sourceX, sourceY, targetX,
419
425
  });
420
426
  const NODE_TYPES = { member: MemberNode, connection: ConnectionNode };
421
427
  const EDGE_TYPES = { a2a: LinkEdge, mcp: CallEdge };
422
- /** Where a member's connections sit: under it, side by side, centred. */
423
- function connectionsX(memberX, count) {
424
- const row = count * CONNECTION_WIDTH + (count - 1) * CONNECTIONS_APART;
425
- const left = memberX + (NODE_WIDTH - row) / 2;
426
- return Array.from({ length: count }, (_, at) => left + at * (CONNECTION_WIDTH + CONNECTIONS_APART));
428
+ /** Where a member's connections sit: right of it, one under another from its character's middle. */
429
+ function connectionsY(memberY, size, count, connectionHeight) {
430
+ const top = memberY + size / 2 + CONNECTION_DROP;
431
+ return Array.from({ length: count }, (_, at) => top + at * (connectionHeight + CONNECTIONS_APART));
432
+ }
433
+ /** How far down a member's connections reach, from its top; none, 0. */
434
+ function connectionsReach(size, count, connectionHeight) {
435
+ return count
436
+ ? size / 2 +
437
+ CONNECTION_DROP +
438
+ count * connectionHeight +
439
+ (count - 1) * CONNECTIONS_APART
440
+ : 0;
427
441
  }
428
442
  /** What a member's call says, on its edge and to a screen reader. */
429
443
  export function callWords(member, connection, tool) {
@@ -502,7 +516,11 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
502
516
  // Where the members stand, as columns and rows, and the width that takes.
503
517
  const peerIds = peers.map(one => one.id);
504
518
  const placing = placeMembers(entry.id, peerIds, positions);
505
- const drawn = Math.max(WIDTH, placing.columns * NODE_WIDTH + (placing.columns - 1) * GAP);
519
+ const hasConnections = members.some(member => member.connections?.length);
520
+ // The last column's connections sit beside it, inside the graph.
521
+ const drawn = Math.max(WIDTH, placing.columns * NODE_WIDTH +
522
+ (placing.columns - 1) * GAP +
523
+ (hasConnections ? CONNECTIONS_BESIDE : 0));
506
524
  const viewport = useMemo(() => teamViewport(width || drawn, drawn), [width, drawn]);
507
525
  // The members' height: the character, its name and where it runs.
508
526
  const memberHeight = size + 56;
@@ -516,14 +534,13 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
516
534
  connections: (member.connections ?? []).map(connection => connection.id),
517
535
  }));
518
536
  const placedKey = JSON.stringify(placed);
519
- const hasConnections = placed.some(({ connections }) => connections.length);
520
537
  // A notebook in a balloon: more room above the members.
521
538
  const room = members.some(member => member.persona.notebook)
522
539
  ? Math.max(NOTEBOOK_BALLOON_ROOM, balloonRoom)
523
540
  : balloonRoom;
524
- // Under a member: its connections, when any member has some.
525
- const below = hasConnections ? CONNECTION_GAP + connectionHeight : 0;
526
- const rowHeight = memberHeight + below;
541
+ // Beside a member, its connections may reach below it: the row takes them.
542
+ const reach = Math.max(0, ...placed.map(({ connections }) => connectionsReach(size, connections.length, connectionHeight)));
543
+ const rowHeight = Math.max(memberHeight, reach);
527
544
  const total = room + placing.rows * rowHeight + (placing.rows - 1) * ROW_GAP;
528
545
  const height = Math.ceil(total * viewport.zoom);
529
546
  // Where each member is laid out, before it is moved.
@@ -537,14 +554,14 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
537
554
  // Where the members were moved, kept inside the graph's box.
538
555
  const boundsOf = (id) => {
539
556
  const layout = layoutOf(id);
540
- const under = placed.find(({ member }) => member === id)?.connections.length
541
- ? CONNECTION_GAP + connectionHeight
542
- : 0;
557
+ const count = placed.find(({ member }) => member === id)?.connections.length ?? 0;
558
+ const beside = count ? CONNECTIONS_BESIDE : 0;
559
+ const tall = Math.max(memberHeight, connectionsReach(size, count, connectionHeight));
543
560
  return {
544
561
  minDx: -layout.x,
545
- maxDx: drawn - NODE_WIDTH - layout.x,
562
+ maxDx: drawn - NODE_WIDTH - beside - layout.x,
546
563
  minDy: -layout.y,
547
- maxDy: total - layout.y - memberHeight - under,
564
+ maxDy: total - layout.y - tall,
548
565
  };
549
566
  };
550
567
  const [moved, setMoved] = useState(() => Object.fromEntries(members.map(member => [
@@ -597,13 +614,13 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
597
614
  },
598
615
  // A member's connections move with it.
599
616
  ...connections.map((connection, at) => {
600
- const xs = connectionsX(x, connections.length);
617
+ const ys = connectionsY(y, size, connections.length, connectionHeight);
601
618
  return {
602
619
  id: connectionNodeId(id, connection),
603
620
  type: 'connection',
604
621
  position: {
605
- x: xs[at],
606
- y: y + memberHeight + CONNECTION_GAP,
622
+ x: x + CONNECTION_X,
623
+ y: ys[at],
607
624
  },
608
625
  width: CONNECTION_WIDTH,
609
626
  height: connectionHeight,
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The scene spec's JSON Schema (`loop.scene/v1`), generated from agentspecs by
3
+ * `scripts/codegen/generate_scenes.py`. Do not edit.
4
+ *
5
+ * @module specs/sceneSchema
6
+ */
7
+ import type { JsonSchema } from './appspecSchema';
8
+ export declare const SCENE_SCHEMA: JsonSchema;
@@ -0,0 +1,852 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ export const SCENE_SCHEMA = {
6
+ $defs: {
7
+ ActionClass: {
8
+ description: 'What a tool does to the world.',
9
+ enum: ['read', 'write', 'send', 'buy', 'delete', 'publish'],
10
+ title: 'ActionClass',
11
+ type: 'string',
12
+ },
13
+ AnswerKind: {
14
+ description: 'What kind of answer comes back, and what the page shows.',
15
+ enum: ['words', 'table', 'chart', 'notebook', 'map', 'file', 'image'],
16
+ title: 'AnswerKind',
17
+ type: 'string',
18
+ },
19
+ AppVerified: {
20
+ additionalProperties: false,
21
+ description: 'What was verified, and how, each in a sentence a person reads (LOOP E-14).\n\nAn example says it on its card and on its page: what was tried live,\nwhat runs on recorded data instead, and what is not verified yet. Said\nby whoever tried it; nothing here is computed.',
22
+ properties: {
23
+ live: {
24
+ description: 'What was tried live, where and when',
25
+ items: {
26
+ type: 'string',
27
+ },
28
+ title: 'Live',
29
+ type: 'array',
30
+ },
31
+ recorded: {
32
+ description: 'What runs on recorded data, not on live calls',
33
+ items: {
34
+ type: 'string',
35
+ },
36
+ title: 'Recorded',
37
+ type: 'array',
38
+ },
39
+ unverified: {
40
+ description: 'What is not verified yet',
41
+ items: {
42
+ type: 'string',
43
+ },
44
+ title: 'Unverified',
45
+ type: 'array',
46
+ },
47
+ },
48
+ title: 'AppVerified',
49
+ type: 'object',
50
+ },
51
+ Inspector: {
52
+ description: 'The inspectors a page may offer under a scene.',
53
+ enum: ['agent', 'tools', 'a2a', 'notebook', 'cost'],
54
+ title: 'Inspector',
55
+ type: 'string',
56
+ },
57
+ Pace: {
58
+ description: 'How fast a beat plays for the audience.',
59
+ enum: ['quick', 'steady', 'slow'],
60
+ title: 'Pace',
61
+ type: 'string',
62
+ },
63
+ RehearsalBeat: {
64
+ additionalProperties: false,
65
+ description: "What a beat's transcript must look like.",
66
+ properties: {
67
+ beat: {
68
+ description: 'The beat, by id',
69
+ title: 'Beat',
70
+ type: 'string',
71
+ },
72
+ lines: {
73
+ description: 'The shape, in order: `Sales → Accounting`, `Accounting → Odoo: odoo_accounting_*`, `Accounting: a table`',
74
+ items: {
75
+ type: 'string',
76
+ },
77
+ title: 'Lines',
78
+ type: 'array',
79
+ },
80
+ must_say: {
81
+ description: 'Words that must appear',
82
+ items: {
83
+ type: 'string',
84
+ },
85
+ title: 'Must Say',
86
+ type: 'array',
87
+ },
88
+ must_not_say: {
89
+ description: 'Words that must not',
90
+ items: {
91
+ type: 'string',
92
+ },
93
+ title: 'Must Not Say',
94
+ type: 'array',
95
+ },
96
+ within: {
97
+ default: '',
98
+ description: 'The time the beat may take',
99
+ pattern: '^(?:[1-9]\\d*(?:ms|s|m|h))?$',
100
+ title: 'Within',
101
+ type: 'string',
102
+ },
103
+ },
104
+ required: ['beat'],
105
+ title: 'RehearsalBeat',
106
+ type: 'object',
107
+ },
108
+ SceneAudience: {
109
+ additionalProperties: false,
110
+ description: 'Who may watch and ask, and what an ask may cost.',
111
+ properties: {
112
+ who: {
113
+ $ref: '#/$defs/Watchers',
114
+ default: 'signed-in',
115
+ description: '`visitors`, `signed-in`, or `nobody`',
116
+ },
117
+ ceiling_per_ask: {
118
+ default: 0,
119
+ description: "What one ask may cost, in USD; the site's when 0",
120
+ minimum: 0,
121
+ title: 'Ceiling Per Ask',
122
+ type: 'number',
123
+ },
124
+ asks_a_day: {
125
+ default: 0,
126
+ description: "How many asks a visitor gets a day; the site's when 0",
127
+ minimum: 0,
128
+ title: 'Asks A Day',
129
+ type: 'integer',
130
+ },
131
+ },
132
+ title: 'SceneAudience',
133
+ type: 'object',
134
+ },
135
+ SceneBeat: {
136
+ additionalProperties: false,
137
+ description: 'One beat of the script.',
138
+ properties: {
139
+ id: {
140
+ description: 'Its id: what the rehearsal and a branch name',
141
+ title: 'Id',
142
+ type: 'string',
143
+ },
144
+ cue: {
145
+ $ref: '#/$defs/SceneCue',
146
+ description: 'What starts it',
147
+ },
148
+ narration: {
149
+ default: '',
150
+ description: 'One line for the audience, read in the transcript',
151
+ title: 'Narration',
152
+ type: 'string',
153
+ },
154
+ moves: {
155
+ description: 'Who asks whom over what, in order',
156
+ items: {
157
+ $ref: '#/$defs/SceneMove',
158
+ },
159
+ title: 'Moves',
160
+ type: 'array',
161
+ },
162
+ expect: {
163
+ description: 'What should happen, in words',
164
+ title: 'Expect',
165
+ type: 'string',
166
+ },
167
+ shows: {
168
+ description: 'What the page shows: the answers to the audience when unsaid',
169
+ items: {
170
+ $ref: '#/$defs/AnswerKind',
171
+ },
172
+ title: 'Shows',
173
+ type: 'array',
174
+ },
175
+ pace: {
176
+ anyOf: [
177
+ {
178
+ $ref: '#/$defs/Pace',
179
+ },
180
+ {
181
+ type: 'null',
182
+ },
183
+ ],
184
+ default: null,
185
+ description: "Its pace; the stage's when unsaid",
186
+ },
187
+ branch: {
188
+ description: 'What it does instead, on a decision',
189
+ items: {
190
+ $ref: '#/$defs/SceneBranch',
191
+ },
192
+ title: 'Branch',
193
+ type: 'array',
194
+ },
195
+ },
196
+ required: ['id', 'cue', 'expect'],
197
+ title: 'SceneBeat',
198
+ type: 'object',
199
+ },
200
+ SceneBranch: {
201
+ additionalProperties: false,
202
+ description: 'What a beat does instead when a decision holds.',
203
+ properties: {
204
+ decision: {
205
+ description: 'When, in words: *the books hold no open invoice*',
206
+ title: 'Decision',
207
+ type: 'string',
208
+ },
209
+ expect: {
210
+ default: '',
211
+ description: 'What happens then, in words',
212
+ title: 'Expect',
213
+ type: 'string',
214
+ },
215
+ moves: {
216
+ description: 'The moves then, when they differ',
217
+ items: {
218
+ $ref: '#/$defs/SceneMove',
219
+ },
220
+ title: 'Moves',
221
+ type: 'array',
222
+ },
223
+ then: {
224
+ default: '',
225
+ description: 'The beat that follows, by id, when the script goes on elsewhere',
226
+ title: 'Then',
227
+ type: 'string',
228
+ },
229
+ },
230
+ required: ['decision'],
231
+ title: 'SceneBranch',
232
+ type: 'object',
233
+ },
234
+ SceneCastMember: {
235
+ additionalProperties: false,
236
+ description: 'A member of the cast.\n\nOn a staged team, it names a member of the team (`member`) and says only\nhow it appears here (`persona`) and what it is for in this scene\n(`brief`). Written inline — a scene with no `team` — it is the member:\nwhat it is (`app`, `ref` or `server`), its `role`, where it `runs_in`\nand whom it `talks_to`, as a team member says them.',
237
+ properties: {
238
+ member: {
239
+ description: 'Its id: in the team, or in this cast',
240
+ title: 'Member',
241
+ type: 'string',
242
+ },
243
+ app: {
244
+ default: '',
245
+ description: 'Inline: the application it is, `id` or `id:version`',
246
+ title: 'App',
247
+ type: 'string',
248
+ },
249
+ ref: {
250
+ default: '',
251
+ description: 'Inline: the agent it is, `id` or `id:version`',
252
+ title: 'Ref',
253
+ type: 'string',
254
+ },
255
+ server: {
256
+ default: '',
257
+ description: 'Inline: the MCP server it is, a system of the scene',
258
+ title: 'Server',
259
+ type: 'string',
260
+ },
261
+ role: {
262
+ anyOf: [
263
+ {
264
+ $ref: '#/$defs/TeamRole',
265
+ },
266
+ {
267
+ type: 'null',
268
+ },
269
+ ],
270
+ default: null,
271
+ description: 'Inline: what it is for, structurally',
272
+ },
273
+ runs_in: {
274
+ anyOf: [
275
+ {
276
+ $ref: '#/$defs/TeamPlace',
277
+ },
278
+ {
279
+ type: 'null',
280
+ },
281
+ ],
282
+ default: null,
283
+ description: 'Inline: `browser` or `runtime`',
284
+ },
285
+ talks_to: {
286
+ description: 'Inline: whom it asks, and over what',
287
+ items: {
288
+ $ref: '#/$defs/TeamLink',
289
+ },
290
+ title: 'Talks To',
291
+ type: 'array',
292
+ },
293
+ persona: {
294
+ $ref: '#/$defs/ScenePersona',
295
+ description: 'How it appears on stage',
296
+ },
297
+ brief: {
298
+ default: '',
299
+ description: "What it is for in this scene, over its application's instructions",
300
+ title: 'Brief',
301
+ type: 'string',
302
+ },
303
+ },
304
+ required: ['member'],
305
+ title: 'SceneCastMember',
306
+ type: 'object',
307
+ },
308
+ SceneCue: {
309
+ additionalProperties: false,
310
+ description: 'What starts a beat: something the audience says, a schedule, or an event.',
311
+ properties: {
312
+ say: {
313
+ default: '',
314
+ description: "An opener the audience may say: the entry's suggestion on the page",
315
+ title: 'Say',
316
+ type: 'string',
317
+ },
318
+ schedule: {
319
+ default: '',
320
+ description: 'A schedule, when the beat starts by itself',
321
+ title: 'Schedule',
322
+ type: 'string',
323
+ },
324
+ event: {
325
+ default: '',
326
+ description: 'An event, when something happening starts it',
327
+ title: 'Event',
328
+ type: 'string',
329
+ },
330
+ },
331
+ title: 'SceneCue',
332
+ type: 'object',
333
+ },
334
+ SceneDeployment: {
335
+ additionalProperties: false,
336
+ description: 'Where the scene plays.',
337
+ properties: {
338
+ account: {
339
+ default: '',
340
+ description: 'Under which account the scene plays: `demo`',
341
+ title: 'Account',
342
+ type: 'string',
343
+ },
344
+ page: {
345
+ default: '',
346
+ description: "The page it plays on: the entry's address",
347
+ title: 'Page',
348
+ type: 'string',
349
+ },
350
+ addresses: {
351
+ additionalProperties: {
352
+ type: 'string',
353
+ },
354
+ description: 'For each member on a runtime, the variable its address is read from at build',
355
+ title: 'Addresses',
356
+ type: 'object',
357
+ },
358
+ },
359
+ title: 'SceneDeployment',
360
+ type: 'object',
361
+ },
362
+ SceneMove: {
363
+ additionalProperties: false,
364
+ description: 'One member asking another over a protocol, or answering the audience.',
365
+ properties: {
366
+ who: {
367
+ description: 'The member moving, by its id in the cast',
368
+ title: 'Who',
369
+ type: 'string',
370
+ },
371
+ asks: {
372
+ default: '',
373
+ description: 'Whom: a member or a system; nobody when it answers the audience',
374
+ title: 'Asks',
375
+ type: 'string',
376
+ },
377
+ over: {
378
+ anyOf: [
379
+ {
380
+ $ref: '#/$defs/TeamProtocol',
381
+ },
382
+ {
383
+ type: 'null',
384
+ },
385
+ ],
386
+ default: null,
387
+ description: '`a2a` to a member, `mcp` to a system',
388
+ },
389
+ what: {
390
+ default: '',
391
+ description: 'What it asks for, or what it answers, in words',
392
+ title: 'What',
393
+ type: 'string',
394
+ },
395
+ tool: {
396
+ default: '',
397
+ description: 'Over `mcp`: the tool, by name or pattern (`odoo_accounting_*`)',
398
+ title: 'Tool',
399
+ type: 'string',
400
+ },
401
+ does: {
402
+ anyOf: [
403
+ {
404
+ $ref: '#/$defs/ActionClass',
405
+ },
406
+ {
407
+ type: 'null',
408
+ },
409
+ ],
410
+ default: null,
411
+ description: 'The kind of tool: `read`, `write`, …',
412
+ },
413
+ answers: {
414
+ anyOf: [
415
+ {
416
+ $ref: '#/$defs/AnswerKind',
417
+ },
418
+ {
419
+ type: 'null',
420
+ },
421
+ ],
422
+ default: null,
423
+ description: 'What kind of answer comes back',
424
+ },
425
+ },
426
+ required: ['who'],
427
+ title: 'SceneMove',
428
+ type: 'object',
429
+ },
430
+ ScenePersona: {
431
+ additionalProperties: false,
432
+ description: 'How a member appears to the audience, in this scene.',
433
+ properties: {
434
+ name: {
435
+ default: '',
436
+ description: "The name the audience sees; its application's when unsaid",
437
+ title: 'Name',
438
+ type: 'string',
439
+ },
440
+ face: {
441
+ default: '',
442
+ description: "One emoji; its application's when unsaid",
443
+ title: 'Face',
444
+ type: 'string',
445
+ },
446
+ line: {
447
+ default: '',
448
+ description: 'One line of *who I am here*',
449
+ title: 'Line',
450
+ type: 'string',
451
+ },
452
+ },
453
+ title: 'ScenePersona',
454
+ type: 'object',
455
+ },
456
+ SceneRecording: {
457
+ additionalProperties: false,
458
+ description: 'A transcript recorded once, played when the scene cannot play live (LOOP H-08).',
459
+ properties: {
460
+ path: {
461
+ description: 'Its file, beside the scenes: `sales-and-accounting/recording.json`',
462
+ title: 'Path',
463
+ type: 'string',
464
+ },
465
+ taken: {
466
+ default: '',
467
+ description: 'When it was recorded',
468
+ title: 'Taken',
469
+ type: 'string',
470
+ },
471
+ note: {
472
+ default: '',
473
+ description: 'What the audience is told: *recorded on 2026-10-07*',
474
+ title: 'Note',
475
+ type: 'string',
476
+ },
477
+ },
478
+ required: ['path'],
479
+ title: 'SceneRecording',
480
+ type: 'object',
481
+ },
482
+ SceneRehearsal: {
483
+ additionalProperties: false,
484
+ description: "The scene's tests: a passing rehearsal is the gallery's *Live*.",
485
+ properties: {
486
+ beats: {
487
+ description: "Each beat's expected shape",
488
+ items: {
489
+ $ref: '#/$defs/RehearsalBeat',
490
+ },
491
+ title: 'Beats',
492
+ type: 'array',
493
+ },
494
+ within: {
495
+ default: '',
496
+ description: 'The time the whole scene may take',
497
+ pattern: '^(?:[1-9]\\d*(?:ms|s|m|h))?$',
498
+ title: 'Within',
499
+ type: 'string',
500
+ },
501
+ recording: {
502
+ anyOf: [
503
+ {
504
+ $ref: '#/$defs/SceneRecording',
505
+ },
506
+ {
507
+ type: 'null',
508
+ },
509
+ ],
510
+ default: null,
511
+ description: 'A recording, for when it cannot play live',
512
+ },
513
+ verified: {
514
+ $ref: '#/$defs/AppVerified',
515
+ description: 'What was verified live, what runs on recorded data, and what is not verified yet',
516
+ },
517
+ },
518
+ title: 'SceneRehearsal',
519
+ type: 'object',
520
+ },
521
+ SceneSetting: {
522
+ additionalProperties: false,
523
+ description: 'The stage: what is on it, and what the audience is told.',
524
+ properties: {
525
+ systems: {
526
+ description: 'The systems the cast reaches',
527
+ items: {
528
+ $ref: '#/$defs/SceneSystem',
529
+ },
530
+ title: 'Systems',
531
+ type: 'array',
532
+ },
533
+ frames: {
534
+ description: "The Frames the scene plays under, over the team's",
535
+ items: {
536
+ type: 'string',
537
+ },
538
+ title: 'Frames',
539
+ type: 'array',
540
+ },
541
+ contents: {
542
+ description: 'The data in play: documents and datasets',
543
+ items: {
544
+ type: 'string',
545
+ },
546
+ title: 'Contents',
547
+ type: 'array',
548
+ },
549
+ period: {
550
+ default: '',
551
+ description: 'When the scene plays, in words: *March 2026*',
552
+ title: 'Period',
553
+ type: 'string',
554
+ },
555
+ language: {
556
+ default: '',
557
+ description: 'The language it plays in, `en`',
558
+ pattern: '^(?:[a-z]{2,3}(?:-[A-Z]{2})?)?$',
559
+ title: 'Language',
560
+ type: 'string',
561
+ },
562
+ assumes: {
563
+ default: '',
564
+ description: 'What the audience is told before it starts, one line',
565
+ title: 'Assumes',
566
+ type: 'string',
567
+ },
568
+ },
569
+ title: 'SceneSetting',
570
+ type: 'object',
571
+ },
572
+ SceneStage: {
573
+ additionalProperties: false,
574
+ description: 'Directions for the page.',
575
+ properties: {
576
+ positions: {
577
+ additionalProperties: {
578
+ $ref: '#/$defs/StagePosition',
579
+ },
580
+ description: 'Where each member stands',
581
+ title: 'Positions',
582
+ type: 'object',
583
+ },
584
+ opens_first: {
585
+ default: '',
586
+ description: "Whose balloon opens first; the entry's when unsaid",
587
+ title: 'Opens First',
588
+ type: 'string',
589
+ },
590
+ transcript: {
591
+ $ref: '#/$defs/SceneTranscript',
592
+ description: 'What the transcript shows',
593
+ },
594
+ inspectors: {
595
+ description: 'The inspectors offered',
596
+ items: {
597
+ $ref: '#/$defs/Inspector',
598
+ },
599
+ title: 'Inspectors',
600
+ type: 'array',
601
+ },
602
+ rests_after: {
603
+ default: '',
604
+ description: 'How long the scene plays before it rests',
605
+ pattern: '^(?:[1-9]\\d*(?:ms|s|m|h))?$',
606
+ title: 'Rests After',
607
+ type: 'string',
608
+ },
609
+ pace: {
610
+ $ref: '#/$defs/Pace',
611
+ default: 'steady',
612
+ description: 'The pace of the beats that say none',
613
+ },
614
+ },
615
+ title: 'SceneStage',
616
+ type: 'object',
617
+ },
618
+ SceneSystem: {
619
+ additionalProperties: false,
620
+ description: 'A system on stage: an MCP server of the catalogue the cast reaches.',
621
+ properties: {
622
+ server: {
623
+ description: 'An MCP server of the catalogue, `id` or `id:version`',
624
+ title: 'Server',
625
+ type: 'string',
626
+ },
627
+ as: {
628
+ default: '',
629
+ description: 'The name the audience reads on the graph and in the transcript',
630
+ title: 'As',
631
+ type: 'string',
632
+ },
633
+ holds: {
634
+ default: '',
635
+ description: "What it holds in this scene: *Datalayer's own books, March 2026*",
636
+ title: 'Holds',
637
+ type: 'string',
638
+ },
639
+ },
640
+ required: ['server'],
641
+ title: 'SceneSystem',
642
+ type: 'object',
643
+ },
644
+ SceneTranscript: {
645
+ additionalProperties: false,
646
+ description: 'What the transcript shows.',
647
+ properties: {
648
+ tools: {
649
+ default: true,
650
+ description: 'Whether tool lines are shown',
651
+ title: 'Tools',
652
+ type: 'boolean',
653
+ },
654
+ narration: {
655
+ default: true,
656
+ description: "Whether the beats' narration is read in it",
657
+ title: 'Narration',
658
+ type: 'boolean',
659
+ },
660
+ withhold: {
661
+ description: 'Words withheld: credentials, ids, …',
662
+ items: {
663
+ $ref: '#/$defs/Withheld',
664
+ },
665
+ title: 'Withhold',
666
+ type: 'array',
667
+ },
668
+ },
669
+ title: 'SceneTranscript',
670
+ type: 'object',
671
+ },
672
+ StagePosition: {
673
+ additionalProperties: false,
674
+ description: 'Where a member stands: fractions of the box, left to right and top to bottom.',
675
+ properties: {
676
+ x: {
677
+ maximum: 1,
678
+ minimum: 0,
679
+ title: 'X',
680
+ type: 'number',
681
+ },
682
+ y: {
683
+ maximum: 1,
684
+ minimum: 0,
685
+ title: 'Y',
686
+ type: 'number',
687
+ },
688
+ },
689
+ required: ['x', 'y'],
690
+ title: 'StagePosition',
691
+ type: 'object',
692
+ },
693
+ TeamLink: {
694
+ description: 'One member asking another, directly, over a protocol.\n\nDistinct from `depends_on`, which orders work: a link says who may ask\nwhom while the work runs, in either order and as often as it needs.',
695
+ properties: {
696
+ member: {
697
+ description: 'The member asked, by its id in this team',
698
+ title: 'Member',
699
+ type: 'string',
700
+ },
701
+ over: {
702
+ $ref: '#/$defs/TeamProtocol',
703
+ default: 'a2a',
704
+ description: 'The protocol it is asked over: `a2a` to an agent or an application, `mcp` to a server',
705
+ },
706
+ },
707
+ required: ['member'],
708
+ title: 'TeamLink',
709
+ type: 'object',
710
+ },
711
+ TeamPlace: {
712
+ description: "Where a member's loop turns.\n\nA team of applications can be split between the person's page and a\nruntime: what reads the person's screen and talks to them turns in their\nbrowser, what reaches a company's systems turns on a runtime, where its\nconnections and its credentials are.",
713
+ enum: ['browser', 'runtime'],
714
+ title: 'TeamPlace',
715
+ type: 'string',
716
+ },
717
+ TeamProtocol: {
718
+ description: 'How one member talks to another.',
719
+ enum: ['a2a', 'mcp'],
720
+ title: 'TeamProtocol',
721
+ type: 'string',
722
+ },
723
+ TeamRole: {
724
+ description: 'What a member is *for*, structurally.\n\nDeliberately about position in the work rather than about subject matter: a\nteam of analysts and a team of reviewers have the same shape, and a runtime\nscheduling them cares which member opens the work and which closes it, not\nwhat the work is about.',
725
+ enum: [
726
+ 'coordinator',
727
+ 'initiator',
728
+ 'contributor',
729
+ 'reviewer',
730
+ 'finalizer',
731
+ ],
732
+ title: 'TeamRole',
733
+ type: 'string',
734
+ },
735
+ Watchers: {
736
+ description: 'Who may watch and ask.',
737
+ enum: ['visitors', 'signed-in', 'nobody'],
738
+ title: 'Watchers',
739
+ type: 'string',
740
+ },
741
+ Withheld: {
742
+ description: 'What the transcript does not show.',
743
+ enum: ['credentials', 'ids', 'addresses', 'amounts'],
744
+ title: 'Withheld',
745
+ type: 'string',
746
+ },
747
+ },
748
+ additionalProperties: false,
749
+ description: 'Specification for a scene: a team, staged.',
750
+ properties: {
751
+ schema: {
752
+ description: 'The version of the spec itself',
753
+ title: 'Schema',
754
+ const: 'loop.scene/v1',
755
+ default: 'loop.scene/v1',
756
+ },
757
+ id: {
758
+ description: 'Unique scene identifier',
759
+ title: 'Id',
760
+ type: 'string',
761
+ },
762
+ version: {
763
+ default: '0.0.1',
764
+ description: 'Scene version',
765
+ title: 'Version',
766
+ type: 'string',
767
+ },
768
+ name: {
769
+ description: "Display name: the tab's",
770
+ title: 'Name',
771
+ type: 'string',
772
+ },
773
+ description: {
774
+ default: '',
775
+ description: 'What happens in it, in a sentence',
776
+ title: 'Description',
777
+ type: 'string',
778
+ },
779
+ tags: {
780
+ items: {
781
+ type: 'string',
782
+ },
783
+ title: 'Tags',
784
+ type: 'array',
785
+ },
786
+ icon: {
787
+ default: 'people',
788
+ description: 'Icon identifier',
789
+ title: 'Icon',
790
+ type: 'string',
791
+ },
792
+ emoji: {
793
+ default: '👀',
794
+ description: 'Its face: one emoji, drawn before its name',
795
+ title: 'Emoji',
796
+ type: 'string',
797
+ },
798
+ team: {
799
+ default: '',
800
+ description: 'The team it stages, `id` or `id:version`',
801
+ title: 'Team',
802
+ type: 'string',
803
+ },
804
+ entry: {
805
+ default: '',
806
+ description: "The member the audience talks to; the team's entry when unsaid",
807
+ title: 'Entry',
808
+ type: 'string',
809
+ },
810
+ cast: {
811
+ description: 'The cast: personas and briefs, or the members',
812
+ items: {
813
+ $ref: '#/$defs/SceneCastMember',
814
+ },
815
+ title: 'Cast',
816
+ type: 'array',
817
+ },
818
+ setting: {
819
+ $ref: '#/$defs/SceneSetting',
820
+ description: 'The stage',
821
+ },
822
+ script: {
823
+ description: 'The beats, in order',
824
+ items: {
825
+ $ref: '#/$defs/SceneBeat',
826
+ },
827
+ title: 'Script',
828
+ type: 'array',
829
+ },
830
+ stage: {
831
+ $ref: '#/$defs/SceneStage',
832
+ description: 'Directions for the page',
833
+ },
834
+ audience: {
835
+ $ref: '#/$defs/SceneAudience',
836
+ description: 'Who may watch and ask',
837
+ },
838
+ rehearsal: {
839
+ $ref: '#/$defs/SceneRehearsal',
840
+ description: 'Its tests',
841
+ },
842
+ deployment: {
843
+ $ref: '#/$defs/SceneDeployment',
844
+ description: 'Where it plays',
845
+ },
846
+ },
847
+ required: ['id', 'name'],
848
+ title: 'Scene spec',
849
+ type: 'object',
850
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
851
+ $id: 'https://agentspecs.datalayer.tech/schemas/loop.scene/v1.json',
852
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalayer/agent-runtimes",
3
- "version": "1.3.86",
3
+ "version": "1.3.87",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  ".",
@@ -7,7 +7,7 @@ Generate Python and TypeScript code from YAML environment variable specification
7
7
 
8
8
  Usage:
9
9
  python generate_envvars.py \\
10
- --specs-dir specs/envvars \\
10
+ --specs-dir specs/env-vars \\
11
11
  --python-output agent_runtimes/config/envvars.py \\
12
12
  --typescript-output src/config/envvars.ts
13
13
  """
@@ -16,6 +16,11 @@ filled from its application (``cast_of``) — its ``entry`` said, what each
16
16
  beat ``shows`` said, and what it names that is not enabled (``setup``).
17
17
  Empty optional lists are dropped, as ``generate_apps.py`` drops them.
18
18
 
19
+ The scene spec's JSON Schema is written beside the TypeScript catalogue as
20
+ ``sceneSchema.ts`` (``agentspecs.scenes.json_schema``), the way
21
+ ``generate_apps.py`` writes ``appspecSchema.ts``: what an editor of the text
22
+ completes and explains is what the spec accepts (LOOP S-01, A-03).
23
+
19
24
  Usage:
20
25
  python generate_scenes.py \\
21
26
  --specs-dir agentspecs/agentspecs/scenes \\
@@ -273,8 +278,41 @@ def generate_typescript_code(specs: list[dict[str, Any]]) -> str:
273
278
  return "\n".join(lines)
274
279
 
275
280
 
281
+ def generate_schema_typescript_code(specs_dir: Path) -> str:
282
+ """The scene spec's JSON Schema, for the editor of its text (LOOP S-01, A-03).
283
+
284
+ Taken from the agentspecs the catalogue is generated from, so that what
285
+ the editor completes and explains is what the spec accepts. The
286
+ ``JsonSchema`` type is ``appspecSchema.ts``'s.
287
+ """
288
+ scenes_module = import_from_clone(specs_dir, "scenes")
289
+ schema = json.dumps(
290
+ scenes_module.json_schema(), indent=2, ensure_ascii=False, sort_keys=False
291
+ )
292
+ return "\n".join(
293
+ [
294
+ "/*",
295
+ " * Copyright (c) 2025-2026 Datalayer, Inc.",
296
+ " * Distributed under the terms of the Modified BSD License.",
297
+ " */",
298
+ "",
299
+ "/**",
300
+ " * The scene spec's JSON Schema (`loop.scene/v1`), generated from agentspecs by",
301
+ " * `scripts/codegen/generate_scenes.py`. Do not edit.",
302
+ " *",
303
+ " * @module specs/sceneSchema",
304
+ " */",
305
+ "",
306
+ "import type { JsonSchema } from './appspecSchema';",
307
+ "",
308
+ f"export const SCENE_SCHEMA: JsonSchema = {schema};",
309
+ "",
310
+ ]
311
+ )
312
+
313
+
276
314
  def main() -> None:
277
- """Generate the scene catalogue."""
315
+ """Generate the scene catalogue, and the scene spec's JSON Schema."""
278
316
  parser = argparse.ArgumentParser(
279
317
  description="Generate Python and TypeScript code from YAML scene specifications"
280
318
  )
@@ -290,6 +328,10 @@ def main() -> None:
290
328
  for path, text in (
291
329
  (args.python_output, generate_python_code(specs)),
292
330
  (args.typescript_output, generate_typescript_code(specs)),
331
+ (
332
+ args.typescript_output.with_name("sceneSchema.ts"),
333
+ generate_schema_typescript_code(args.specs_dir),
334
+ ),
293
335
  ):
294
336
  path.parent.mkdir(parents=True, exist_ok=True)
295
337
  path.write_text(text)