@datalayer/agent-runtimes 1.3.16 → 1.3.18
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/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 +574 -0
- package/lib/loop/apps/checks.d.ts +21 -0
- package/lib/loop/apps/checks.js +565 -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/index.d.ts +1 -0
- package/lib/loop/index.js +3 -0
- package/lib/specs/actions.d.ts +30 -0
- package/lib/specs/actions.js +616 -0
- package/lib/specs/apps.d.ts +25 -0
- package/lib/specs/apps.js +1072 -0
- package/lib/specs/cogs.d.ts +19 -0
- package/lib/specs/cogs.js +736 -0
- 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 +8 -0
- package/lib/specs/index.js +8 -0
- package/lib/specs/ops.d.ts +15 -0
- package/lib/specs/ops.js +1272 -0
- package/lib/specs/tracks.d.ts +15 -0
- package/lib/specs/tracks.js +87 -0
- package/lib/types/agentspecs.d.ts +442 -0
- package/package.json +2 -1
- package/scripts/codegen/agentspecs_clone.py +53 -0
- package/scripts/codegen/compose.py +65 -0
- package/scripts/codegen/generate_agents.py +8 -0
- 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_ops.py +388 -0
|
@@ -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
|
+
}
|
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';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action Classes.
|
|
3
|
+
*
|
|
4
|
+
* What every tool does to the world: read, write, send, buy, delete, publish.
|
|
5
|
+
* A tool with no class is unknown, and unknown is the most restricted.
|
|
6
|
+
*
|
|
7
|
+
* This file is AUTO-GENERATED from YAML specifications.
|
|
8
|
+
* DO NOT EDIT MANUALLY - run 'make specs' to regenerate.
|
|
9
|
+
*/
|
|
10
|
+
import type { ActionClass, AppBehaviour, AppEscalation, ServerActionsSpec } from '../types/agentspecs';
|
|
11
|
+
/** The classes of action, from the one that changes nothing. */
|
|
12
|
+
export declare const ACTION_CLASSES: ActionClass[];
|
|
13
|
+
/** What each tool of the tools catalogue does, by id. */
|
|
14
|
+
export declare const TOOL_ACTIONS: Record<string, ActionClass[]>;
|
|
15
|
+
/**
|
|
16
|
+
* What each MCP server's tools do, by server id. `checked` is the day the
|
|
17
|
+
* names were read off the running server; absent when nobody looked.
|
|
18
|
+
*/
|
|
19
|
+
export declare const SERVER_ACTIONS: Record<string, ServerActionsSpec>;
|
|
20
|
+
/**
|
|
21
|
+
* What each application of the catalogue does about each classed tool of the
|
|
22
|
+
* servers it connects to, as agentspecs decides it. What the rules engine has
|
|
23
|
+
* to reproduce.
|
|
24
|
+
*/
|
|
25
|
+
export declare const APP_BEHAVIOURS: Record<string, Record<string, AppBehaviour>>;
|
|
26
|
+
/**
|
|
27
|
+
* Where what a tool is asked changes what an application does about it: by
|
|
28
|
+
* application and tool, each condition and the behaviour when it holds.
|
|
29
|
+
*/
|
|
30
|
+
export declare const APP_ESCALATIONS: Record<string, Record<string, AppEscalation[]>>;
|