@datalayer/agent-runtimes 1.3.15 → 1.3.17

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 (81) hide show
  1. package/README.md +1 -1
  2. package/lib/chat/base/ChatBase.js +3 -2
  3. package/lib/config/AgentConfiguration.d.ts +1 -1
  4. package/lib/loop/apps/AppRenderer.d.ts +24 -0
  5. package/lib/loop/apps/AppRenderer.js +101 -0
  6. package/lib/loop/apps/appspec.d.ts +69 -0
  7. package/lib/loop/apps/appspec.js +566 -0
  8. package/lib/loop/apps/checks.d.ts +21 -0
  9. package/lib/loop/apps/checks.js +556 -0
  10. package/lib/loop/apps/index.d.ts +10 -0
  11. package/lib/loop/apps/index.js +14 -0
  12. package/lib/loop/apps/rules.d.ts +64 -0
  13. package/lib/loop/apps/rules.js +289 -0
  14. package/lib/loop/apps/yaml.d.ts +17 -0
  15. package/lib/loop/apps/yaml.js +185 -0
  16. package/lib/loop/embed/LoopEmbed.d.ts +6 -1
  17. package/lib/loop/embed/LoopEmbed.js +3 -2
  18. package/lib/loop/index.d.ts +1 -0
  19. package/lib/loop/index.js +3 -0
  20. package/lib/loop/plugins/chat/ChatView.js +9 -4
  21. package/lib/loop/plugins/chat/index.d.ts +10 -0
  22. package/lib/loop/plugins/chat/index.js +1 -0
  23. package/lib/loop/presets.d.ts +6 -0
  24. package/lib/loop/presets.js +2 -1
  25. package/lib/loop/shell/LoopWorkspace.d.ts +14 -0
  26. package/lib/loop/shell/LoopWorkspace.js +20 -6
  27. package/lib/loop/shell/index.d.ts +1 -1
  28. package/lib/loop/shell/index.js +1 -1
  29. package/lib/loop/shell/useWorkspaceFullScreen.d.ts +17 -4
  30. package/lib/loop/shell/useWorkspaceFullScreen.js +23 -6
  31. package/lib/specs/actions.d.ts +30 -0
  32. package/lib/specs/actions.js +616 -0
  33. package/lib/specs/agents/agents.d.ts +1 -0
  34. package/lib/specs/agents/agents.js +338 -284
  35. package/lib/specs/apps.d.ts +25 -0
  36. package/lib/specs/apps.js +1064 -0
  37. package/lib/specs/cogs.d.ts +19 -0
  38. package/lib/specs/cogs.js +736 -0
  39. package/lib/specs/events.d.ts +6 -1
  40. package/lib/specs/events.js +144 -1
  41. package/lib/specs/frames.d.ts +19 -0
  42. package/lib/specs/frames.js +444 -0
  43. package/lib/specs/gates.d.ts +22 -0
  44. package/lib/specs/gates.js +177 -0
  45. package/lib/specs/guards.d.ts +26 -0
  46. package/lib/specs/guards.js +930 -0
  47. package/lib/specs/index.d.ts +10 -0
  48. package/lib/specs/index.js +10 -0
  49. package/lib/specs/mcpServers.js +2 -2
  50. package/lib/specs/memory.js +4 -0
  51. package/lib/specs/modelProviders.d.ts +22 -0
  52. package/lib/specs/modelProviders.js +102 -0
  53. package/lib/specs/models.d.ts +31 -0
  54. package/lib/specs/models.js +215 -0
  55. package/lib/specs/ops.d.ts +15 -0
  56. package/lib/specs/ops.js +1272 -0
  57. package/lib/specs/outputs.js +2 -2
  58. package/lib/specs/skills.js +1 -1
  59. package/lib/specs/teams/teams.js +18 -18
  60. package/lib/specs/tracks.d.ts +15 -0
  61. package/lib/specs/tracks.js +87 -0
  62. package/lib/specs/uiPlugins.d.ts +16 -0
  63. package/lib/specs/uiPlugins.js +40 -0
  64. package/lib/types/agentspecs.d.ts +453 -2
  65. package/lib/types/memory.d.ts +2 -0
  66. package/lib/types/models.d.ts +52 -0
  67. package/package.json +2 -1
  68. package/scripts/check-cloudflare-models.py +122 -0
  69. package/scripts/codegen/agentspecs_clone.py +53 -0
  70. package/scripts/codegen/compose.py +65 -0
  71. package/scripts/codegen/generate_agents.py +14 -4
  72. package/scripts/codegen/generate_apps.py +454 -0
  73. package/scripts/codegen/generate_cogs.py +308 -0
  74. package/scripts/codegen/generate_frames.py +278 -0
  75. package/scripts/codegen/generate_memory.py +2 -0
  76. package/scripts/codegen/generate_model_providers.py +246 -0
  77. package/scripts/codegen/generate_models.py +135 -4
  78. package/scripts/codegen/generate_ops.py +388 -0
  79. package/scripts/codegen/generate_sandbox_agents.py +1 -1
  80. package/scripts/codegen/generate_ui_plugins.py +226 -0
  81. package/scripts/gen_workers.py +2 -2
@@ -0,0 +1,289 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * What an application does when its agent calls a tool.
7
+ *
8
+ * A rule is written in a person's words — *ask me before it sends anything* —
9
+ * and applies to what a tool does: a class of action (read, write, send, buy,
10
+ * delete, publish) or named tools. The classes are the catalogue's
11
+ * (`specs/actions`), written on every tool by somebody who looked; a tool
12
+ * nobody classed is unknown, and unknown is the most restricted.
13
+ *
14
+ * The decision, in order:
15
+ *
16
+ * 1. a tool of a server the application is not connected to, or that its
17
+ * connection leaves out (`only`), is left to the person: an application
18
+ * reaches nothing it does not name;
19
+ * 2. a tool that can act, on a connection that only reads, is left to the person;
20
+ * 3. a rule that names the tool decides what the tool does of its own — and
21
+ * what the arguments of the call make it do *besides* is still decided by
22
+ * its class: *label a message: do it* does not become *trash it: do it*;
23
+ * 4. a tool nobody classed is left to the person;
24
+ * 5. otherwise each of its classes is decided by the rule on that class — or,
25
+ * with no rule, reading is done and anything that acts waits for a person —
26
+ * and the most restricted wins.
27
+ *
28
+ * The same decision is written in Python, in `agentspecs.apps.behaviour_for`
29
+ * and in `agent_runtimes.loop.apps.rules`. `APP_BEHAVIOURS`, generated from
30
+ * agentspecs, is what all three have to agree on. The server enforces; this
31
+ * one shows the decision before it is made.
32
+ *
33
+ * Pure: nothing here calls a tool or renders.
34
+ *
35
+ * @module loop/apps/rules
36
+ */
37
+ import { SERVER_ACTIONS, TOOL_ACTIONS } from '../../specs/actions';
38
+ /** The four behaviours, from the freest to the most restricted. */
39
+ export const BEHAVIOURS = [
40
+ 'do_it',
41
+ 'if_asked',
42
+ 'ask_first',
43
+ 'leave_to_me',
44
+ ];
45
+ /**
46
+ * What an application does about a class no rule of its own covers. Reading
47
+ * needs no rule; anything that acts waits for a person: never `do_it`.
48
+ */
49
+ export const DEFAULT_BEHAVIOURS = {
50
+ read: 'do_it',
51
+ write: 'ask_first',
52
+ send: 'ask_first',
53
+ buy: 'ask_first',
54
+ delete: 'ask_first',
55
+ publish: 'ask_first',
56
+ };
57
+ const SERVER_TOOL = /^([A-Za-z0-9_-]+)(?::\d+(?:\.\d+)*)?\.([A-Za-z_][A-Za-z0-9_-]*)$/;
58
+ const own = (record, key) => Object.prototype.hasOwnProperty.call(record, key) ? record[key] : undefined;
59
+ /** The id of a reference, `id` or `id:version`. */
60
+ const idOf = (ref) => {
61
+ const at = ref.lastIndexOf(':');
62
+ return at > 0 && ref.slice(at + 1).includes('.') ? ref.slice(0, at) : ref;
63
+ };
64
+ /** A tool reference as [server, tool]: `tavily.tavily_search`, or a tool id alone. */
65
+ export function splitRef(ref) {
66
+ const matched = SERVER_TOOL.exec(ref);
67
+ return matched ? [matched[1], matched[2]] : [undefined, idOf(ref)];
68
+ }
69
+ /** Whether a tool name is a pattern: it stands for several. */
70
+ export const isPattern = (name) => /[*?]/.test(name);
71
+ /**
72
+ * Whether a name matches a pattern: `*` is any run of characters, `?` any one.
73
+ *
74
+ * Nothing else is special — no bracket expressions — and case counts: the
75
+ * same pattern means the same thing here and in Python.
76
+ */
77
+ export function matchesPattern(name, pattern) {
78
+ const expression = Array.from(pattern, character => character === '*'
79
+ ? '[\\s\\S]*'
80
+ : character === '?'
81
+ ? '[\\s\\S]'
82
+ : character.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('');
83
+ return new RegExp(`^${expression}$`).test(name);
84
+ }
85
+ /**
86
+ * Whether a value is one every reader compares the same way: a word, true or
87
+ * false, or a number that is finite and — when it is whole — held exactly.
88
+ * Beyond `Number.MAX_SAFE_INTEGER` two different integers are one number
89
+ * here and two in Python.
90
+ */
91
+ export const isComparable = (value) => typeof value === 'string' ||
92
+ typeof value === 'boolean' ||
93
+ (typeof value === 'number' &&
94
+ Number.isFinite(value) &&
95
+ (!Number.isInteger(value) || Number.isSafeInteger(value)));
96
+ /** Whether an argument's value is the one a condition names; words whatever their case. */
97
+ const same = (value, wanted) => typeof value === 'string' && typeof wanted === 'string'
98
+ ? value.trim().toLowerCase() === wanted.trim().toLowerCase()
99
+ : isComparable(value) && isComparable(wanted) && value === wanted;
100
+ /** Whether the arguments of a call make a condition true. */
101
+ export function conditionHolds(condition, args) {
102
+ if (!Object.prototype.hasOwnProperty.call(args, condition.argument)) {
103
+ return false;
104
+ }
105
+ const value = args[condition.argument];
106
+ if (condition.equals?.some(wanted => same(value, wanted))) {
107
+ return true;
108
+ }
109
+ if (condition.includes?.length) {
110
+ const values = Array.isArray(value) ? value : [value];
111
+ return values.some(item => condition.includes.some(wanted => same(item, wanted)));
112
+ }
113
+ return false;
114
+ }
115
+ /** What answers for a tool of a server: its own classes, and its conditions. */
116
+ function entryOf(server, name) {
117
+ const actions = own(SERVER_ACTIONS, server);
118
+ if (!actions) {
119
+ return [[], []];
120
+ }
121
+ const exact = own(actions.tools, name);
122
+ if (exact) {
123
+ return [[...exact], own(actions.conditions, name) ?? []];
124
+ }
125
+ for (const [pattern, classes] of Object.entries(actions.tools)) {
126
+ if (isPattern(pattern) && matchesPattern(name, pattern)) {
127
+ return [[...classes], own(actions.conditions, pattern) ?? []];
128
+ }
129
+ }
130
+ return [[...actions.default], []];
131
+ }
132
+ /**
133
+ * The classes of a tool, by reference; empty when nobody classed it.
134
+ *
135
+ * With the arguments of a call, the classes of that call. Without them,
136
+ * everything the tool can do: nobody said what it is asked.
137
+ */
138
+ export function classesOf(ref, args) {
139
+ const [server, name] = splitRef(ref);
140
+ if (server === undefined) {
141
+ return [...(own(TOOL_ACTIONS, name) ?? [])];
142
+ }
143
+ const [classes, conditions] = entryOf(server, name);
144
+ for (const condition of conditions) {
145
+ if (args === undefined || conditionHolds(condition, args)) {
146
+ for (const item of condition.classes) {
147
+ if (!classes.includes(item)) {
148
+ classes.push(item);
149
+ }
150
+ }
151
+ }
152
+ }
153
+ return classes;
154
+ }
155
+ /** Whether a tool only reads. An unknown tool — no class — does not. */
156
+ export const isReadOnly = (classes) => classes.length > 0 && classes.every(item => item === 'read');
157
+ /** The most restricted of several behaviours. */
158
+ export const strictest = (behaviours) => behaviours.reduce((worst, behaviour) => BEHAVIOURS.indexOf(behaviour) > BEHAVIOURS.indexOf(worst)
159
+ ? behaviour
160
+ : worst);
161
+ const connectionTo = (app, server) => app.connections.find(connection => idOf(connection.server) === server);
162
+ const reaches = (connection, tool) => connection.only.length === 0 ||
163
+ connection.only.some(pattern => matchesPattern(tool, pattern));
164
+ const isClass = (target) => Object.prototype.hasOwnProperty.call(DEFAULT_BEHAVIOURS, target);
165
+ /** What a rule applies to, versions aside. */
166
+ const normal = (target) => {
167
+ if (isClass(target)) {
168
+ return target;
169
+ }
170
+ const [server, name] = splitRef(target);
171
+ return server !== undefined ? `${server}.${name}` : name;
172
+ };
173
+ /** What the rules on classes decide for each of these classes. */
174
+ const byClasses = (app, classes) => {
175
+ const byClass = {};
176
+ for (const rule of app.rules) {
177
+ for (const target of rule.appliesTo) {
178
+ if (isClass(target)) {
179
+ byClass[target] = rule.behaviour;
180
+ }
181
+ }
182
+ }
183
+ return classes.map(item => byClass[item] ?? DEFAULT_BEHAVIOURS[item]);
184
+ };
185
+ /**
186
+ * What an application does when its agent calls a tool.
187
+ *
188
+ * `tool` is `server.tool` for a tool of an MCP server, or the id of a tool
189
+ * of the catalogue. Its classes are the catalogue's, unless given.
190
+ */
191
+ export function behaviourFor(app, tool, options = {}) {
192
+ const [server, name] = splitRef(tool);
193
+ let ownClasses;
194
+ let besides = [];
195
+ let possible;
196
+ if (options.classes) {
197
+ ownClasses = possible = [...options.classes];
198
+ }
199
+ else {
200
+ possible = classesOf(tool);
201
+ ownClasses = classesOf(tool, {});
202
+ const now = options.arguments === undefined
203
+ ? possible
204
+ : classesOf(tool, options.arguments);
205
+ besides = now.filter(item => !ownClasses.includes(item));
206
+ }
207
+ if (server !== undefined) {
208
+ const connection = connectionTo(app, server);
209
+ if (!connection || !reaches(connection, name)) {
210
+ return 'leave_to_me';
211
+ }
212
+ if (connection.access === 'read' &&
213
+ possible.some(item => item !== 'read')) {
214
+ return 'leave_to_me';
215
+ }
216
+ }
217
+ const wanted = server !== undefined ? `${server}.${name}` : name;
218
+ for (const rule of app.rules) {
219
+ if (rule.appliesTo.some(target => !isClass(target) && normal(target) === wanted)) {
220
+ return strictest([rule.behaviour, ...byClasses(app, besides)]);
221
+ }
222
+ }
223
+ if (ownClasses.length === 0 && besides.length === 0) {
224
+ return 'leave_to_me';
225
+ }
226
+ return strictest(byClasses(app, [...ownClasses, ...besides]));
227
+ }
228
+ /**
229
+ * What the application does about every classed tool of the servers it
230
+ * connects to, each in its plain use — no argument that makes it do more; see
231
+ * {@link toolEscalations} for those. A server that classes its tools by a
232
+ * pattern is reported by that pattern.
233
+ */
234
+ export function toolBehaviours(app) {
235
+ const behaviours = {};
236
+ for (const connection of app.connections) {
237
+ const server = idOf(connection.server);
238
+ const actions = own(SERVER_ACTIONS, server);
239
+ if (!actions) {
240
+ continue;
241
+ }
242
+ for (const [name, found] of Object.entries(actions.tools)) {
243
+ const ref = `${server}.${name}`;
244
+ if (!isPattern(name)) {
245
+ behaviours[ref] = behaviourFor(app, ref, { arguments: {} });
246
+ }
247
+ else if (connection.access === 'read' &&
248
+ found.some(item => item !== 'read')) {
249
+ behaviours[ref] = 'leave_to_me';
250
+ }
251
+ else {
252
+ behaviours[ref] =
253
+ found.length > 0 ? strictest(byClasses(app, found)) : 'leave_to_me';
254
+ }
255
+ }
256
+ }
257
+ return behaviours;
258
+ }
259
+ /** Where what a tool is asked changes what the application does about it. */
260
+ export function toolEscalations(app) {
261
+ const escalations = {};
262
+ for (const connection of app.connections) {
263
+ const server = idOf(connection.server);
264
+ const actions = own(SERVER_ACTIONS, server);
265
+ if (!actions) {
266
+ continue;
267
+ }
268
+ for (const [name, conditions] of Object.entries(actions.conditions)) {
269
+ if (isPattern(name)) {
270
+ continue;
271
+ }
272
+ const ref = `${server}.${name}`;
273
+ const plain = behaviourFor(app, ref, { arguments: {} });
274
+ for (const condition of conditions) {
275
+ const values = condition.includes?.length
276
+ ? condition.includes
277
+ : (condition.equals ?? []);
278
+ const value = condition.includes?.length ? [values[0]] : values[0];
279
+ const then = behaviourFor(app, ref, {
280
+ arguments: { [condition.argument]: value },
281
+ });
282
+ if (then !== plain) {
283
+ (escalations[ref] ??= []).push({ ...condition, behaviour: then });
284
+ }
285
+ }
286
+ }
287
+ }
288
+ return escalations;
289
+ }
@@ -0,0 +1,17 @@
1
+ import type { AppSpec } from '../../types/agentspecs';
2
+ import { type ParsedAppspec } from './appspec';
3
+ /**
4
+ * An application from its YAML file.
5
+ *
6
+ * What YAML itself refuses is reported with its line, before anything the
7
+ * spec has to say; the application returned is whole either way.
8
+ */
9
+ export declare function readAppspecYaml(text: string): ParsedAppspec;
10
+ /**
11
+ * An application as its YAML file.
12
+ *
13
+ * Into `previous` when there is one and it reads: what did not change keeps
14
+ * its comments, its order and its style. Afresh otherwise, as the canonical
15
+ * document. Either way the file reads back as the application.
16
+ */
17
+ export declare function writeAppspecYaml(app: AppSpec, previous?: string): string;
@@ -0,0 +1,185 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * The Appspec as a YAML file a person also edits.
7
+ *
8
+ * An application's file is reviewed like code: it has comments, an order
9
+ * somebody chose, a way of quoting. When the Canvas changes one field and
10
+ * writes the file back, all of that has to still be there — a comment that
11
+ * disappears on a save is a reason never to open the Canvas again.
12
+ *
13
+ * {@link writeAppspecYaml} therefore does not write the application afresh
14
+ * when there is a file already: it writes it **into** that file. What did not
15
+ * change keeps its node — its comment, its style, its place — and only what
16
+ * changed is replaced. Two things are the writer's own and are
17
+ * settled by the first save: one space before a comment at the end of a
18
+ * line, and long text folded at eighty columns. A file written once is
19
+ * given back untouched when nothing changed. An item of a list is followed by what identifies it
20
+ * (a connection's server, a rule's action, a component's id), so that a
21
+ * comment stays with its item when the list is reordered or grows.
22
+ *
23
+ * With no file to write into, the document is the canonical one
24
+ * (`dumpAppspec`): the spec's keys in the spec's order.
25
+ *
26
+ * Pure: nothing here reads or writes a disk.
27
+ *
28
+ * @module loop/apps/yaml
29
+ */
30
+ import { Document, isMap, isScalar, isSeq, parseDocument, } from 'yaml';
31
+ import { dumpAppspec, parseAppspec } from './appspec';
32
+ const isData = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
33
+ /**
34
+ * How the file is written. Long text is folded at eighty columns, and a
35
+ * list written on one line stays `[a, b]`.
36
+ */
37
+ const WRITING = {
38
+ indent: 2,
39
+ lineWidth: 80,
40
+ flowCollectionPadding: false,
41
+ };
42
+ /**
43
+ * An application from its YAML file.
44
+ *
45
+ * What YAML itself refuses is reported with its line, before anything the
46
+ * spec has to say; the application returned is whole either way.
47
+ */
48
+ export function readAppspecYaml(text) {
49
+ const document = parseDocument(text, { prettyErrors: true });
50
+ if (document.errors.length > 0) {
51
+ const parsed = parseAppspec({});
52
+ return {
53
+ app: parsed.app,
54
+ problems: document.errors.map(error => {
55
+ const at = error.linePos?.[0];
56
+ const where = at ? `Line ${at.line}: ` : '';
57
+ return `${where}${error.message.split('\n')[0]}`;
58
+ }),
59
+ };
60
+ }
61
+ return parseAppspec(document.toJS());
62
+ }
63
+ /** What identifies an item of a list, when its items are mappings. */
64
+ const IDENTITIES = ['id', 'server', 'action', 'space', 'name', 'label', 'ask'];
65
+ /** The key every item of both lists has, with a different value each — or none. */
66
+ function identityOf(items, nodes) {
67
+ const ofNode = (node, key) => isMap(node) ? node.get(key) : undefined;
68
+ return IDENTITIES.find(key => {
69
+ const values = items.map(item => (isData(item) ? item[key] : undefined));
70
+ const existing = nodes.map(node => ofNode(node, key));
71
+ const distinct = (list) => list.every(value => typeof value === 'string') &&
72
+ new Set(list).size === list.length;
73
+ return items.length > 0 && distinct(values) && distinct(existing);
74
+ });
75
+ }
76
+ /** A value as a node of a document, as the document would write it new. */
77
+ const nodeOf = (document, value) => document.createNode(value);
78
+ /**
79
+ * A node saying a value: the node itself when it already does, changed in
80
+ * place where it can be, and a new one only when it is not the same kind of
81
+ * thing any more.
82
+ *
83
+ * `before` is what the file said at this place, in the canonical form: a key
84
+ * the canonical form leaves out both before and now — a default written out
85
+ * by its author, `enabled: true`, `access: read` — is kept as it was written.
86
+ */
87
+ function merged(document, node, value, before) {
88
+ if (isData(value) && isMap(node)) {
89
+ mergeMap(document, node, value, isData(before) ? before : {});
90
+ return node;
91
+ }
92
+ if (Array.isArray(value) && isSeq(node)) {
93
+ mergeSeq(document, node, value, Array.isArray(before) ? before : []);
94
+ return node;
95
+ }
96
+ if (!isData(value) && !Array.isArray(value) && isScalar(node)) {
97
+ const scalar = node;
98
+ if (scalar.value !== value) {
99
+ scalar.value = value;
100
+ }
101
+ return scalar;
102
+ }
103
+ return nodeOf(document, value);
104
+ }
105
+ const keyOf = (pair) => isScalar(pair.key) ? String(pair.key.value) : String(pair.key);
106
+ const has = (data, key) => Object.prototype.hasOwnProperty.call(data, key);
107
+ /**
108
+ * A mapping made to say `value`: its own pairs kept, changed, removed or
109
+ * joined.
110
+ *
111
+ * A pair is removed only when the application stopped saying it: when the
112
+ * canonical form had it before and has it no more. One the canonical form
113
+ * never had — a default spelled out, a key this reader does not know — stays
114
+ * as it was written.
115
+ *
116
+ * A key that is new goes after the key the canonical form puts just before
117
+ * it, among those that are there, wherever the person put that one; first
118
+ * when nothing comes before it. The canonical form writes its keys in the
119
+ * spec's order, at every depth.
120
+ */
121
+ function mergeMap(document, map, value, before) {
122
+ map.items = map.items.filter(pair => {
123
+ const key = keyOf(pair);
124
+ return has(value, key) || !has(before, key);
125
+ });
126
+ const order = Object.keys(value);
127
+ for (const [key, item] of Object.entries(value)) {
128
+ const pair = map.items.find(existing => keyOf(existing) === key);
129
+ if (pair) {
130
+ pair.value = merged(document, pair.value, item, before[key]);
131
+ continue;
132
+ }
133
+ const created = document.createPair(key, item);
134
+ const rank = order.indexOf(key);
135
+ let after = -1;
136
+ let best = -1;
137
+ map.items.forEach((existing, index) => {
138
+ const other = order.indexOf(keyOf(existing));
139
+ if (other >= 0 && other < rank && other > best) {
140
+ best = other;
141
+ after = index;
142
+ }
143
+ });
144
+ map.items.splice(after + 1, 0, created);
145
+ }
146
+ }
147
+ /** A list made to say `value`: an item keeps its node when it can be told which it was. */
148
+ function mergeSeq(document, seq, value, before) {
149
+ const nodes = seq.items;
150
+ const identity = identityOf(value, nodes);
151
+ if (identity) {
152
+ const byIdentity = new Map(nodes.map(node => [node.get(identity), node]));
153
+ const beforeByIdentity = new Map(before.filter(isData).map(item => [item[identity], item]));
154
+ seq.items = value.map(item => {
155
+ const key = item[identity];
156
+ const existing = byIdentity.get(key);
157
+ return existing
158
+ ? merged(document, existing, item, beforeByIdentity.get(key))
159
+ : nodeOf(document, item);
160
+ });
161
+ return;
162
+ }
163
+ // Nothing tells the items apart: they are followed by their place.
164
+ seq.items = value.map((item, index) => index < nodes.length
165
+ ? merged(document, nodes[index], item, before[index])
166
+ : nodeOf(document, item));
167
+ }
168
+ /**
169
+ * An application as its YAML file.
170
+ *
171
+ * Into `previous` when there is one and it reads: what did not change keeps
172
+ * its comments, its order and its style. Afresh otherwise, as the canonical
173
+ * document. Either way the file reads back as the application.
174
+ */
175
+ export function writeAppspecYaml(app, previous) {
176
+ const data = dumpAppspec(app);
177
+ if (previous !== undefined && previous.trim() !== '') {
178
+ const document = parseDocument(previous);
179
+ if (document.errors.length === 0 && isMap(document.contents)) {
180
+ mergeMap(document, document.contents, data, dumpAppspec(parseAppspec(document.toJS()).app));
181
+ return document.toString(WRITING);
182
+ }
183
+ }
184
+ return new Document(data).toString(WRITING);
185
+ }
@@ -29,6 +29,11 @@ export type LoopEmbedProps = LoopPresetOptions & {
29
29
  chatHeaderActions?: ReactNode;
30
30
  /** The host's own controls for the workspace header row. */
31
31
  headerActions?: ReactNode;
32
+ /**
33
+ * An element in the host's own bar to render the header's controls in,
34
+ * instead of a header row. See `LoopWorkspaceProps.headerContainer`.
35
+ */
36
+ headerContainer?: HTMLElement | null;
32
37
  /**
33
38
  * Draw the workspace inside a window frame with this title bar.
34
39
  *
@@ -53,5 +58,5 @@ export type LoopEmbedProps = LoopPresetOptions & {
53
58
  */
54
59
  plugins?: PluginRef[];
55
60
  };
56
- export declare function LoopEmbed({ agentId, showHeader, chatHeaderActions, headerActions, frameTitle, frameHeight, plugins, ...preset }: LoopEmbedProps): React.JSX.Element;
61
+ export declare function LoopEmbed({ agentId, showHeader, chatHeaderActions, headerActions, headerContainer, frameTitle, frameHeight, plugins, ...preset }: LoopEmbedProps): React.JSX.Element;
57
62
  export default LoopEmbed;
@@ -30,7 +30,7 @@ import { internalQueryClient } from '../../utils';
30
30
  import { buildLoopReactor, LoopWorkspace } from '../shell';
31
31
  import { loopPlugins } from '../presets';
32
32
  import { WindowFrame } from '../plugins/window-frame';
33
- export function LoopEmbed({ agentId = '', showHeader = false, chatHeaderActions, headerActions, frameTitle, frameHeight, plugins = [], ...preset }) {
33
+ export function LoopEmbed({ agentId = '', showHeader = false, chatHeaderActions, headerActions, headerContainer, frameTitle, frameHeight, plugins = [], ...preset }) {
34
34
  /*
35
35
  * The latest `firstPromptHook`, kept in a ref rather than closed over
36
36
  * directly.
@@ -69,6 +69,7 @@ export function LoopEmbed({ agentId = '', showHeader = false, chatHeaderActions,
69
69
  preset.chatHeaderButtons,
70
70
  preset.promptPlacement,
71
71
  preset.autoFocusPrompt,
72
+ preset.suggestionLabels,
72
73
  preset.fullScreenTopOffset,
73
74
  preset.showAgentVariants,
74
75
  preset.agentSummary,
@@ -119,7 +120,7 @@ export function LoopEmbed({ agentId = '', showHeader = false, chatHeaderActions,
119
120
  * view and the workspace draws "No view is available yet".
120
121
  */
121
122
  useReactor(reactor);
122
- const shell = (_jsx(LoopWorkspace, { serverUrl: preset.serverUrl ?? '', agentId: agentId, reactor: reactor, manageReactor: false, showViewSelector: preset.showViewSelector, showHeader: showHeader, chatHeaderActions: chatHeaderActions, headerActions: headerActions }));
123
+ const shell = (_jsx(LoopWorkspace, { serverUrl: preset.serverUrl ?? '', agentId: agentId, reactor: reactor, manageReactor: false, showViewSelector: preset.showViewSelector, showHeader: showHeader, chatHeaderActions: chatHeaderActions, headerActions: headerActions, headerContainer: headerContainer }));
123
124
  return (_jsx(QueryClientProvider, { client: internalQueryClient, children: frameTitle !== undefined ? (_jsx(WindowFrame, { title: frameTitle, height: frameHeight, children: shell })) : (_jsx(Box, { sx: { height: '100%', minHeight: 0 }, children: shell })) }));
124
125
  }
125
126
  export default LoopEmbed;
@@ -66,3 +66,4 @@ export { LoopEmbed, type LoopEmbedProps } from './embed/LoopEmbed';
66
66
  export { SubagentActivityPlugin, SubagentPulse, SUBAGENT_ACTIVITY_PLUGIN_NAME, } from './plugins/subagent-activity';
67
67
  export { loopPlugins, type LoopPresetOptions } from './presets';
68
68
  export { DocumentExtension, NotebookExtension } from './extensions';
69
+ export { BEHAVIOURS as APP_BEHAVIOUR_ORDER, DEFAULT_BEHAVIOURS as APP_DEFAULT_BEHAVIOURS, behaviourFor, classesOf, isReadOnly, toolBehaviours, toolEscalations, type BehaviourOptions, type ToolArguments, APP_SCHEMA, APPSPEC_KEY_ORDER, DEFAULT_LAYOUTS as APP_DEFAULT_LAYOUTS, dumpAppspec, emptyAppspec, parseAppspec, AppRenderer, checkApp, checkAppspec, type AppCheck, defineAppPlugin, type AppRendererProps, readAppspecYaml, writeAppspecYaml, type ParsedAppspec, } from './apps';
package/lib/loop/index.js CHANGED
@@ -88,3 +88,6 @@ export { loopPlugins } from './presets';
88
88
  // The extensions that group them, for hosts that would rather install a
89
89
  // capability than assemble one.
90
90
  export { DocumentExtension, NotebookExtension } from './extensions';
91
+ // Applications: what one does when its agent calls a tool — the rules a
92
+ // person wrote, decided on what the tool does.
93
+ export { BEHAVIOURS as APP_BEHAVIOUR_ORDER, DEFAULT_BEHAVIOURS as APP_DEFAULT_BEHAVIOURS, behaviourFor, classesOf, isReadOnly, toolBehaviours, toolEscalations, APP_SCHEMA, APPSPEC_KEY_ORDER, DEFAULT_LAYOUTS as APP_DEFAULT_LAYOUTS, dumpAppspec, emptyAppspec, parseAppspec, AppRenderer, checkApp, checkAppspec, defineAppPlugin, readAppspecYaml, writeAppspecYaml, } from './apps';
@@ -37,7 +37,7 @@ import { agentRuntimeStore, useAgentRuntimeContextSnapshot, } from '../../../sto
37
37
  import { useIAMStore } from '../../../state';
38
38
  import { useConfig } from '../../../hooks/useConfig';
39
39
  import { useSkills, useSkillActions } from '../../../hooks/useSkills';
40
- import { AI_MODEL_CATALOGUE } from '../../../specs/models';
40
+ import { AI_MODEL_CATALOGUE, isChatModel } from '../../../specs/models';
41
41
  import { LoopAgentBlueprint, LoopAgentGate, LoopChatComposer, LoopChatExtras, LoopChatHeader, LoopChatLayout, LoopChatSuggestion, LoopChatSurface, LoopChatTurn, LoopFrontendTool, LoopSlots, canOpenView, onPromptFocusRequest, onSurfaceRequest, useLoopPromptStore, } from '../../core';
42
42
  import { turnWritersOf } from './turnState';
43
43
  import { isInactiveSurfaceContribution, orderToolContributions, toolsForChatView, } from './chatViewTools';
@@ -1153,7 +1153,8 @@ export default function ChatView({ workspace }) {
1153
1153
  const offeredModels = useMemo(() => catalogModels.length > 0
1154
1154
  ? catalogModels
1155
1155
  : Object.values(AI_MODEL_CATALOGUE)
1156
- .filter(model => model.available)
1156
+ // A typed-judgment model answers typed questions, not a chat.
1157
+ .filter(model => model.available && isChatModel(model))
1157
1158
  .map(model => ({
1158
1159
  id: model.id,
1159
1160
  name: model.name,
@@ -1475,7 +1476,9 @@ export default function ChatView({ workspace }) {
1475
1476
  // With the prompt on top the openers are already chips under
1476
1477
  // it; the empty state repeating them was the same three buttons
1477
1478
  // twice on one screen.
1478
- suggestions: topPrompt || layout ? [] : chatSuggestions,
1479
+ suggestions: topPrompt || layout || config?.suggestionLabels === false
1480
+ ? []
1481
+ : chatSuggestions,
1479
1482
  /*
1480
1483
  The title bar arrives as a plugin: the chat-header plugin
1481
1484
  contributes the component, this view hands it the assembled
@@ -1598,7 +1601,9 @@ export default function ChatView({ workspace }) {
1598
1601
  // through the extras channel — the A2UI examples draw their
1599
1602
  // surface here. Wins over the notebook surfaces in ChatBase.
1600
1603
  renderToolResult: chatExtras.renderToolResult, showTurnFooter: chatExtras.showTokenUsage ?? true, onContextSnapshot: handleContextSnapshot, onLoadingChange: handleLoadingChange, onItemsChange: handleMessagesChange, onDisplayItemsChange: handleDisplayItemsChange, enableStreaming: true }) }), besideChat && !promptHidden ? prompt : null] }) }));
1601
- const chipsEl = chatSuggestions.length > 0 ? (_jsx(Box, { "aria-label": "Suggested prompts", sx: { display: 'flex', flexWrap: 'wrap', gap: 2, px: 2, pb: '6px' }, children: chatSuggestions.map(item => (_jsx(Box, { as: "button", type: "button",
1604
+ // Off when the host asked for the menu alone: the composer's suggestions
1605
+ // control carries the same openers (`suggestions` above).
1606
+ const chipsEl = config?.suggestionLabels !== false && chatSuggestions.length > 0 ? (_jsx(Box, { "aria-label": "Suggested prompts", sx: { display: 'flex', flexWrap: 'wrap', gap: 2, px: 2, pb: '6px' }, children: chatSuggestions.map(item => (_jsx(Box, { as: "button", type: "button",
1602
1607
  // The whole request, where the chip shows only its summary.
1603
1608
  title: item.message, disabled: busy || chatDisabled, onClick: () => void handleSend(item.message), sx: {
1604
1609
  appearance: 'none',
@@ -105,6 +105,16 @@ export type ChatPluginConfig = {
105
105
  * them. Such a host passes `false` and lets the reader arrive at the prompt.
106
106
  */
107
107
  autoFocusPrompt: boolean;
108
+ /**
109
+ * Whether the openers are drawn as a row of labels by the composer.
110
+ *
111
+ * On by default: a person who has just opened the workspace sees what it is
112
+ * worth asking without opening anything. The same openers are always behind
113
+ * the composer's own suggestions menu, so a host with little room — a frame
114
+ * on a landing page, where the labels would be a row of the first screen —
115
+ * turns the labels off and loses nothing.
116
+ */
117
+ suggestionLabels: boolean;
108
118
  /**
109
119
  * Pixels to reserve at the top of the workspace when it goes full screen —
110
120
  * room for a host's own fixed header, drawn outside the workspace
@@ -48,6 +48,7 @@ export const ChatPlugin = definePlugin({
48
48
  hidePrompt: false,
49
49
  promptPlacement: 'bottom',
50
50
  autoFocusPrompt: true,
51
+ suggestionLabels: true,
51
52
  fullScreenTopOffset: 0,
52
53
  },
53
54
  displayName: 'Chat',
@@ -54,6 +54,12 @@ export type LoopPresetOptions = {
54
54
  * does not scroll the reader into the middle of the page.
55
55
  */
56
56
  autoFocusPrompt?: boolean;
57
+ /**
58
+ * Whether the openers show as labels by the composer. True by default; the
59
+ * composer's suggestions menu offers them either way. See
60
+ * `ChatPluginConfig.suggestionLabels`.
61
+ */
62
+ suggestionLabels?: boolean;
57
63
  /**
58
64
  * Pixels to reserve at the top of the workspace when it goes full screen,
59
65
  * for a host's own fixed header. Passed through to the chat plugin — see
@@ -52,7 +52,7 @@ import { LoopPageLayoutPlugin } from './plugins/page-layout';
52
52
  * be installed or signed into, and the switches a demonstration wants are off.
53
53
  */
54
54
  export function loopPlugins(options = {}) {
55
- const { serverUrl, target = 'browser', defaultEditor = 'notebook', showViewSelector = true, hideChatHeader = false, chatHeaderButtons = false, promptPlacement, autoFocusPrompt = true, fullScreenTopOffset = 0, showAgentVariants = false, agentSummary = true, teamId, teamPicker = true, pageLayout = false, pageLayoutTurnPanel = 'below', pageLayoutPrompt = 'docked', pageLayoutPromptAnchor = 'top', pageLayoutSize, pageLayoutTurnPanelFooter = 'full', localAgent, localAgentSpec, floatingPrompt = false, editorSelector = false, editors = true, graph = false, commandPalette = false, pluginsPanel = false, windowFrame = false, firstPromptHook, } = options;
55
+ const { serverUrl, target = 'browser', defaultEditor = 'notebook', showViewSelector = true, hideChatHeader = false, chatHeaderButtons = false, promptPlacement, autoFocusPrompt = true, suggestionLabels = true, fullScreenTopOffset = 0, showAgentVariants = false, agentSummary = true, teamId, teamPicker = true, pageLayout = false, pageLayoutTurnPanel = 'below', pageLayoutPrompt = 'docked', pageLayoutPromptAnchor = 'top', pageLayoutSize, pageLayoutTurnPanelFooter = 'full', localAgent, localAgentSpec, floatingPrompt = false, editorSelector = false, editors = true, graph = false, commandPalette = false, pluginsPanel = false, windowFrame = false, firstPromptHook, } = options;
56
56
  return [
57
57
  // The chat owns the editor beside it, so which one opens is its
58
58
  // configuration rather than the workspace's.
@@ -66,6 +66,7 @@ export function loopPlugins(options = {}) {
66
66
  headerButtons: { newChat: chatHeaderButtons, clear: chatHeaderButtons },
67
67
  promptPlacement: floatingPrompt ? 'floating' : promptPlacement,
68
68
  autoFocusPrompt,
69
+ suggestionLabels,
69
70
  fullScreenTopOffset,
70
71
  }),
71
72
  // The composer and the title bar are plugins of their own: the chat