@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.
- package/README.md +1 -1
- package/lib/chat/base/ChatBase.js +3 -2
- package/lib/config/AgentConfiguration.d.ts +1 -1
- package/lib/loop/apps/AppRenderer.d.ts +24 -0
- package/lib/loop/apps/AppRenderer.js +101 -0
- package/lib/loop/apps/appspec.d.ts +69 -0
- package/lib/loop/apps/appspec.js +566 -0
- package/lib/loop/apps/checks.d.ts +21 -0
- package/lib/loop/apps/checks.js +556 -0
- package/lib/loop/apps/index.d.ts +10 -0
- package/lib/loop/apps/index.js +14 -0
- package/lib/loop/apps/rules.d.ts +64 -0
- package/lib/loop/apps/rules.js +289 -0
- package/lib/loop/apps/yaml.d.ts +17 -0
- package/lib/loop/apps/yaml.js +185 -0
- package/lib/loop/embed/LoopEmbed.d.ts +6 -1
- package/lib/loop/embed/LoopEmbed.js +3 -2
- package/lib/loop/index.d.ts +1 -0
- package/lib/loop/index.js +3 -0
- package/lib/loop/plugins/chat/ChatView.js +9 -4
- package/lib/loop/plugins/chat/index.d.ts +10 -0
- package/lib/loop/plugins/chat/index.js +1 -0
- package/lib/loop/presets.d.ts +6 -0
- package/lib/loop/presets.js +2 -1
- package/lib/loop/shell/LoopWorkspace.d.ts +14 -0
- package/lib/loop/shell/LoopWorkspace.js +20 -6
- package/lib/loop/shell/index.d.ts +1 -1
- package/lib/loop/shell/index.js +1 -1
- package/lib/loop/shell/useWorkspaceFullScreen.d.ts +17 -4
- package/lib/loop/shell/useWorkspaceFullScreen.js +23 -6
- package/lib/specs/actions.d.ts +30 -0
- package/lib/specs/actions.js +616 -0
- package/lib/specs/agents/agents.d.ts +1 -0
- package/lib/specs/agents/agents.js +338 -284
- package/lib/specs/apps.d.ts +25 -0
- package/lib/specs/apps.js +1064 -0
- package/lib/specs/cogs.d.ts +19 -0
- package/lib/specs/cogs.js +736 -0
- package/lib/specs/events.d.ts +6 -1
- package/lib/specs/events.js +144 -1
- package/lib/specs/frames.d.ts +19 -0
- package/lib/specs/frames.js +444 -0
- package/lib/specs/gates.d.ts +22 -0
- package/lib/specs/gates.js +177 -0
- package/lib/specs/guards.d.ts +26 -0
- package/lib/specs/guards.js +930 -0
- package/lib/specs/index.d.ts +10 -0
- package/lib/specs/index.js +10 -0
- package/lib/specs/mcpServers.js +2 -2
- package/lib/specs/memory.js +4 -0
- package/lib/specs/modelProviders.d.ts +22 -0
- package/lib/specs/modelProviders.js +102 -0
- package/lib/specs/models.d.ts +31 -0
- package/lib/specs/models.js +215 -0
- package/lib/specs/ops.d.ts +15 -0
- package/lib/specs/ops.js +1272 -0
- package/lib/specs/outputs.js +2 -2
- package/lib/specs/skills.js +1 -1
- package/lib/specs/teams/teams.js +18 -18
- package/lib/specs/tracks.d.ts +15 -0
- package/lib/specs/tracks.js +87 -0
- package/lib/specs/uiPlugins.d.ts +16 -0
- package/lib/specs/uiPlugins.js +40 -0
- package/lib/types/agentspecs.d.ts +453 -2
- package/lib/types/memory.d.ts +2 -0
- package/lib/types/models.d.ts +52 -0
- package/package.json +2 -1
- package/scripts/check-cloudflare-models.py +122 -0
- package/scripts/codegen/agentspecs_clone.py +53 -0
- package/scripts/codegen/compose.py +65 -0
- package/scripts/codegen/generate_agents.py +14 -4
- package/scripts/codegen/generate_apps.py +454 -0
- package/scripts/codegen/generate_cogs.py +308 -0
- package/scripts/codegen/generate_frames.py +278 -0
- package/scripts/codegen/generate_memory.py +2 -0
- package/scripts/codegen/generate_model_providers.py +246 -0
- package/scripts/codegen/generate_models.py +135 -4
- package/scripts/codegen/generate_ops.py +388 -0
- package/scripts/codegen/generate_sandbox_agents.py +1 -1
- package/scripts/codegen/generate_ui_plugins.py +226 -0
- 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;
|
package/lib/loop/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
package/lib/loop/presets.d.ts
CHANGED
|
@@ -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
|
package/lib/loop/presets.js
CHANGED
|
@@ -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
|