amalgm 0.1.126 → 0.1.127
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/package.json
CHANGED
|
@@ -9,6 +9,12 @@ const { defaultToolboxService } = require('../toolbox/service');
|
|
|
9
9
|
|
|
10
10
|
const BUNDLE_KIND = 'amalgm.agent.bundle';
|
|
11
11
|
const BUNDLE_SCHEMA_VERSION = 1;
|
|
12
|
+
const GRAPH_ELEMENT_ORDER = {
|
|
13
|
+
agent: 0,
|
|
14
|
+
tool: 1,
|
|
15
|
+
tool_action: 2,
|
|
16
|
+
skill: 3,
|
|
17
|
+
};
|
|
12
18
|
|
|
13
19
|
const SECRET_KEY_PATTERN = /(api[_-]?key|authorization|bearer|cookie|password|passwd|secret|token|credential|private[_-]?key)/i;
|
|
14
20
|
|
|
@@ -42,6 +48,76 @@ function bundleId() {
|
|
|
42
48
|
return `agent-bundle-${crypto.randomUUID()}`;
|
|
43
49
|
}
|
|
44
50
|
|
|
51
|
+
function stableDigest(value) {
|
|
52
|
+
return crypto.createHash('sha256')
|
|
53
|
+
.update(typeof value === 'string' ? value : JSON.stringify(value))
|
|
54
|
+
.digest('hex')
|
|
55
|
+
.slice(0, 16);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function graphElementId(type, id) {
|
|
59
|
+
return `${type}:${id}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function skillFingerprint(skill) {
|
|
63
|
+
if (typeof skill === 'string') {
|
|
64
|
+
const value = skill.trim();
|
|
65
|
+
return value ? `string:${value}` : '';
|
|
66
|
+
}
|
|
67
|
+
if (!isObject(skill)) return '';
|
|
68
|
+
const id = typeof skill.id === 'string' ? skill.id.trim() : '';
|
|
69
|
+
if (id) return `id:${id}`;
|
|
70
|
+
const path = typeof skill.path === 'string' ? skill.path.trim() : '';
|
|
71
|
+
if (path) return `path:${path}`;
|
|
72
|
+
const name = typeof skill.name === 'string' ? skill.name.trim() : '';
|
|
73
|
+
const content = typeof skill.content === 'string' ? skill.content.trim() : '';
|
|
74
|
+
if (name && content) return `name-content:${name}:${content}`;
|
|
75
|
+
if (name) return `name:${name}`;
|
|
76
|
+
if (content) return `content:${content}`;
|
|
77
|
+
return `json:${JSON.stringify(skill)}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function skillElementId(skill) {
|
|
81
|
+
const fingerprint = skillFingerprint(skill);
|
|
82
|
+
return fingerprint ? graphElementId('skill', stableDigest(fingerprint)) : '';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function skillLabel(skill) {
|
|
86
|
+
if (typeof skill === 'string') return skill.trim() || 'Skill';
|
|
87
|
+
if (!isObject(skill)) return 'Skill';
|
|
88
|
+
const id = typeof skill.id === 'string' ? skill.id.trim() : '';
|
|
89
|
+
const name = typeof skill.name === 'string' ? skill.name.trim() : '';
|
|
90
|
+
const path = typeof skill.path === 'string' ? skill.path.trim() : '';
|
|
91
|
+
if (name) return name;
|
|
92
|
+
if (path) return path.split('/').filter(Boolean).pop() || path;
|
|
93
|
+
if (id) return id;
|
|
94
|
+
const content = typeof skill.content === 'string' ? skill.content.trim() : '';
|
|
95
|
+
return content.slice(0, 60) || 'Skill';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function compareGraphElements(left, right) {
|
|
99
|
+
const order = (GRAPH_ELEMENT_ORDER[left.type] || 99) - (GRAPH_ELEMENT_ORDER[right.type] || 99);
|
|
100
|
+
if (order !== 0) return order;
|
|
101
|
+
return String(left.id).localeCompare(String(right.id));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function compareGraphEdges(left, right) {
|
|
105
|
+
return String(left.from).localeCompare(String(right.from))
|
|
106
|
+
|| String(left.kind).localeCompare(String(right.kind))
|
|
107
|
+
|| String(left.to).localeCompare(String(right.to));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function pushGraphElement(elements, element) {
|
|
111
|
+
if (!element?.id) return;
|
|
112
|
+
if (!elements.has(element.id)) elements.set(element.id, element);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function pushGraphEdge(edges, edge) {
|
|
116
|
+
if (!edge?.from || !edge?.to || !edge?.kind) return;
|
|
117
|
+
const key = `${edge.from}::${edge.kind}::${edge.to}`;
|
|
118
|
+
if (!edges.has(key)) edges.set(key, edge);
|
|
119
|
+
}
|
|
120
|
+
|
|
45
121
|
function portableAgent(agent) {
|
|
46
122
|
return {
|
|
47
123
|
sourceAgentId: agent.id,
|
|
@@ -108,14 +184,185 @@ function countHooks(bundleAgents) {
|
|
|
108
184
|
}
|
|
109
185
|
|
|
110
186
|
function collectSkillRecords(bundleAgents) {
|
|
111
|
-
const
|
|
187
|
+
const byKey = new Map();
|
|
112
188
|
for (const entry of bundleAgents) {
|
|
113
189
|
for (const skill of entry.config.skills || []) {
|
|
114
|
-
|
|
115
|
-
|
|
190
|
+
const key = skillFingerprint(skill);
|
|
191
|
+
if (!key) continue;
|
|
192
|
+
byKey.set(key, cloneJson(skill));
|
|
116
193
|
}
|
|
117
194
|
}
|
|
118
|
-
return [...
|
|
195
|
+
return [...byKey.values()];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function buildFlatAgentBundleGraph(bundle) {
|
|
199
|
+
const elements = new Map();
|
|
200
|
+
const edges = new Map();
|
|
201
|
+
const toolMap = new Map((Array.isArray(bundle.tools) ? bundle.tools : []).map((tool) => [tool.id, tool]));
|
|
202
|
+
const toolActionMap = new Map((Array.isArray(bundle.toolActions) ? bundle.toolActions : []).map((action) => [action.id, action]));
|
|
203
|
+
const systemToolIds = new Set(uniqueStrings(bundle.requires?.systemTools));
|
|
204
|
+
const missingToolIds = new Set(uniqueStrings(bundle.requires?.missingTools));
|
|
205
|
+
const skills = collectSkillRecords(bundle.agents || []);
|
|
206
|
+
|
|
207
|
+
for (const entry of bundle.agents || []) {
|
|
208
|
+
const currentAgentId = agentEntryId(entry);
|
|
209
|
+
if (!currentAgentId) continue;
|
|
210
|
+
pushGraphElement(elements, {
|
|
211
|
+
id: graphElementId('agent', currentAgentId),
|
|
212
|
+
type: 'agent',
|
|
213
|
+
label: entry.agent?.name || currentAgentId,
|
|
214
|
+
data: {
|
|
215
|
+
sourceAgentId: currentAgentId,
|
|
216
|
+
agent: cloneJson(entry.agent || {}),
|
|
217
|
+
config: cloneJson(normalizeAgentConfig(entry.config || {})),
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
for (const skill of skills) {
|
|
223
|
+
const elementId = skillElementId(skill);
|
|
224
|
+
if (!elementId) continue;
|
|
225
|
+
pushGraphElement(elements, {
|
|
226
|
+
id: elementId,
|
|
227
|
+
type: 'skill',
|
|
228
|
+
label: skillLabel(skill),
|
|
229
|
+
data: typeof skill === 'string' ? { value: skill } : cloneJson(skill),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const includedToolIds = new Set();
|
|
234
|
+
const includedActionIds = new Set();
|
|
235
|
+
for (const entry of bundle.agents || []) {
|
|
236
|
+
const currentAgentId = agentEntryId(entry);
|
|
237
|
+
if (!currentAgentId) continue;
|
|
238
|
+
const currentConfig = normalizeAgentConfig(entry.config || {});
|
|
239
|
+
|
|
240
|
+
for (const subagent of currentConfig.subagents || []) {
|
|
241
|
+
if (!subagent?.agentId) continue;
|
|
242
|
+
pushGraphEdge(edges, {
|
|
243
|
+
from: graphElementId('agent', currentAgentId),
|
|
244
|
+
to: graphElementId('agent', subagent.agentId),
|
|
245
|
+
kind: 'uses-subagent',
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
for (const selectedId of uniqueStrings(currentConfig.loadout?.toolIds)) {
|
|
250
|
+
const selectedAction = toolActionMap.get(selectedId);
|
|
251
|
+
if (selectedAction) {
|
|
252
|
+
includedActionIds.add(selectedAction.id);
|
|
253
|
+
includedToolIds.add(selectedAction.toolId);
|
|
254
|
+
pushGraphEdge(edges, {
|
|
255
|
+
from: graphElementId('agent', currentAgentId),
|
|
256
|
+
to: graphElementId('tool_action', selectedAction.id),
|
|
257
|
+
kind: 'uses-tool-action',
|
|
258
|
+
});
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
includedToolIds.add(selectedId);
|
|
263
|
+
pushGraphEdge(edges, {
|
|
264
|
+
from: graphElementId('agent', currentAgentId),
|
|
265
|
+
to: graphElementId('tool', selectedId),
|
|
266
|
+
kind: 'uses-tool',
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
for (const skill of currentConfig.skills || []) {
|
|
271
|
+
const targetId = skillElementId(skill);
|
|
272
|
+
if (!targetId) continue;
|
|
273
|
+
pushGraphEdge(edges, {
|
|
274
|
+
from: graphElementId('agent', currentAgentId),
|
|
275
|
+
to: targetId,
|
|
276
|
+
kind: 'uses-skill',
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
for (const toolId of includedToolIds) {
|
|
282
|
+
const tool = toolMap.get(toolId);
|
|
283
|
+
pushGraphElement(elements, {
|
|
284
|
+
id: graphElementId('tool', toolId),
|
|
285
|
+
type: 'tool',
|
|
286
|
+
label: tool?.name || toolId,
|
|
287
|
+
data: tool
|
|
288
|
+
? cloneJson(tool)
|
|
289
|
+
: {
|
|
290
|
+
id: toolId,
|
|
291
|
+
name: toolId,
|
|
292
|
+
origin: systemToolIds.has(toolId) ? 'system' : missingToolIds.has(toolId) ? 'missing' : 'unknown',
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
for (const action of Array.isArray(bundle.toolActions) ? bundle.toolActions : []) {
|
|
298
|
+
if (!includedToolIds.has(action.toolId)) continue;
|
|
299
|
+
includedActionIds.add(action.id);
|
|
300
|
+
pushGraphElement(elements, {
|
|
301
|
+
id: graphElementId('tool_action', action.id),
|
|
302
|
+
type: 'tool_action',
|
|
303
|
+
label: action.name || action.id,
|
|
304
|
+
data: cloneJson(action),
|
|
305
|
+
});
|
|
306
|
+
pushGraphEdge(edges, {
|
|
307
|
+
from: graphElementId('tool_action', action.id),
|
|
308
|
+
to: graphElementId('tool', action.toolId),
|
|
309
|
+
kind: 'action-of',
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const firstAgentId = agentEntryId(bundle.agents?.[0]);
|
|
314
|
+
const rootAgentId = typeof bundle.rootAgentId === 'string' && bundle.rootAgentId.trim()
|
|
315
|
+
? bundle.rootAgentId.trim()
|
|
316
|
+
: firstAgentId;
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
headIds: rootAgentId ? [graphElementId('agent', rootAgentId)] : [],
|
|
320
|
+
elements: [...elements.values()].sort(compareGraphElements),
|
|
321
|
+
edges: [...edges.values()].sort(compareGraphEdges),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function assertClosedFlatBundleGraph(bundle) {
|
|
326
|
+
if (!Array.isArray(bundle.headIds) || bundle.headIds.length === 0) {
|
|
327
|
+
throw new Error('bundle graph must include at least one head');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const elementIds = new Set();
|
|
331
|
+
for (const element of Array.isArray(bundle.elements) ? bundle.elements : []) {
|
|
332
|
+
if (!isObject(element) || typeof element.id !== 'string' || !element.id.trim()) {
|
|
333
|
+
throw new Error('bundle graph elements must include non-empty ids');
|
|
334
|
+
}
|
|
335
|
+
if (elementIds.has(element.id)) {
|
|
336
|
+
throw new Error(`bundle graph contains duplicate element id: ${element.id}`);
|
|
337
|
+
}
|
|
338
|
+
elementIds.add(element.id);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
for (const headId of bundle.headIds) {
|
|
342
|
+
if (typeof headId !== 'string' || !elementIds.has(headId)) {
|
|
343
|
+
throw new Error(`bundle graph head is missing element: ${headId}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
for (const edge of Array.isArray(bundle.edges) ? bundle.edges : []) {
|
|
348
|
+
if (!isObject(edge) || typeof edge.from !== 'string' || typeof edge.to !== 'string' || typeof edge.kind !== 'string') {
|
|
349
|
+
throw new Error('bundle graph edges must include from, to, and kind');
|
|
350
|
+
}
|
|
351
|
+
if (!elementIds.has(edge.from)) throw new Error(`bundle graph edge source is missing element: ${edge.from}`);
|
|
352
|
+
if (!elementIds.has(edge.to)) throw new Error(`bundle graph edge target is missing element: ${edge.to}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function withBundleGraph(bundle) {
|
|
357
|
+
const graph = buildFlatAgentBundleGraph(bundle);
|
|
358
|
+
const nextBundle = {
|
|
359
|
+
...bundle,
|
|
360
|
+
headIds: graph.headIds,
|
|
361
|
+
elements: graph.elements,
|
|
362
|
+
edges: graph.edges,
|
|
363
|
+
};
|
|
364
|
+
assertClosedFlatBundleGraph(nextBundle);
|
|
365
|
+
return nextBundle;
|
|
119
366
|
}
|
|
120
367
|
|
|
121
368
|
function collectToolRecords(toolIds, requires) {
|
|
@@ -210,7 +457,7 @@ function createAgentBundle(agentId, options = {}) {
|
|
|
210
457
|
...(tools.length > 0 ? ['Tools can call external services or local commands. Recipients should reconnect credentials on their own machine.'] : []),
|
|
211
458
|
];
|
|
212
459
|
|
|
213
|
-
const bundle = {
|
|
460
|
+
const bundle = withBundleGraph({
|
|
214
461
|
kind: BUNDLE_KIND,
|
|
215
462
|
schemaVersion: BUNDLE_SCHEMA_VERSION,
|
|
216
463
|
bundleId: options.bundleId || bundleId(),
|
|
@@ -233,7 +480,7 @@ function createAgentBundle(agentId, options = {}) {
|
|
|
233
480
|
hooks,
|
|
234
481
|
assets: 0,
|
|
235
482
|
},
|
|
236
|
-
};
|
|
483
|
+
});
|
|
237
484
|
|
|
238
485
|
const bytes = Buffer.byteLength(JSON.stringify(bundle));
|
|
239
486
|
return {
|
|
@@ -330,7 +577,7 @@ function validateBundle(bundle) {
|
|
|
330
577
|
throw new Error('bundle must include at least one agent');
|
|
331
578
|
}
|
|
332
579
|
assertClosedAgentBundleGraph(bundle);
|
|
333
|
-
return bundle;
|
|
580
|
+
return withBundleGraph(bundle);
|
|
334
581
|
}
|
|
335
582
|
|
|
336
583
|
function installTools(bundle) {
|
|
@@ -83,6 +83,27 @@ test('agent bundle includes recursive subagents, selected user tools, and warnin
|
|
|
83
83
|
assert.deepEqual(bundle.agents.map((entry) => entry.sourceAgentId), [root.id, child.id]);
|
|
84
84
|
assert.deepEqual(bundle.tools.map((tool) => tool.id), ['acme.cli']);
|
|
85
85
|
assert.deepEqual(bundle.toolActions.map((action) => action.id), ['acme.cli.run']);
|
|
86
|
+
assert.deepEqual(bundle.headIds, ['agent:bundle-root']);
|
|
87
|
+
assert.equal(bundle.elements.some((element) => element.id === 'agent:bundle-root' && element.type === 'agent'), true);
|
|
88
|
+
assert.equal(bundle.elements.some((element) => element.id === 'agent:bundle-child' && element.type === 'agent'), true);
|
|
89
|
+
assert.equal(bundle.elements.some((element) => element.id === 'tool:acme.cli' && element.type === 'tool'), true);
|
|
90
|
+
assert.equal(bundle.elements.some((element) => element.id === 'tool:browser' && element.type === 'tool'), true);
|
|
91
|
+
assert.equal(bundle.elements.some((element) => element.id === 'tool_action:acme.cli.run' && element.type === 'tool_action'), true);
|
|
92
|
+
assert.equal(bundle.edges.some((edge) => (
|
|
93
|
+
edge.from === 'agent:bundle-root'
|
|
94
|
+
&& edge.to === 'agent:bundle-child'
|
|
95
|
+
&& edge.kind === 'uses-subagent'
|
|
96
|
+
)), true);
|
|
97
|
+
assert.equal(bundle.edges.some((edge) => (
|
|
98
|
+
edge.from === 'agent:bundle-root'
|
|
99
|
+
&& edge.to === 'tool:acme.cli'
|
|
100
|
+
&& edge.kind === 'uses-tool'
|
|
101
|
+
)), true);
|
|
102
|
+
assert.equal(bundle.edges.some((edge) => (
|
|
103
|
+
edge.from === 'agent:bundle-child'
|
|
104
|
+
&& edge.to === 'tool:browser'
|
|
105
|
+
&& edge.kind === 'uses-tool'
|
|
106
|
+
)), true);
|
|
86
107
|
assert.equal(bundle.skills.length, 1);
|
|
87
108
|
assert.equal(bundle.summary.hooks, 1);
|
|
88
109
|
assert.equal(bundle.requires.systemTools.includes('browser'), true);
|
|
@@ -134,6 +155,7 @@ test('installing an agent bundle creates local copies and rewrites subagent refs
|
|
|
134
155
|
assert.equal(installedRootConfig.subagents.length, 1);
|
|
135
156
|
assert.notEqual(installedRootConfig.subagents[0].agentId, child.id);
|
|
136
157
|
assert.equal(Boolean(resolveAgent(installedRootConfig.subagents[0].agentId)), true);
|
|
158
|
+
assert.deepEqual(bundle.headIds, ['agent:install-source-root']);
|
|
137
159
|
});
|
|
138
160
|
|
|
139
161
|
test('installing an agent bundle rejects missing shared subagents', () => {
|
|
@@ -165,3 +187,31 @@ test('installing an agent bundle rejects missing shared subagents', () => {
|
|
|
165
187
|
/missing shared subagents: missing-child/,
|
|
166
188
|
);
|
|
167
189
|
});
|
|
190
|
+
|
|
191
|
+
test('installing an agent bundle regenerates stale flat graph fields from canonical agent arrays', () => {
|
|
192
|
+
const result = installAgentBundle({
|
|
193
|
+
kind: 'amalgm.agent.bundle',
|
|
194
|
+
schemaVersion: 1,
|
|
195
|
+
rootAgentId: 'graph-root',
|
|
196
|
+
title: 'Graph root',
|
|
197
|
+
agents: [{
|
|
198
|
+
sourceAgentId: 'graph-root',
|
|
199
|
+
agent: {
|
|
200
|
+
name: 'Graph root',
|
|
201
|
+
baseHarnessId: 'codex',
|
|
202
|
+
baseModelId: 'openai/gpt-5.5',
|
|
203
|
+
},
|
|
204
|
+
config: {
|
|
205
|
+
instructions: 'Root',
|
|
206
|
+
loadout: { toolIds: [] },
|
|
207
|
+
subagents: [],
|
|
208
|
+
},
|
|
209
|
+
}],
|
|
210
|
+
headIds: ['agent:graph-root'],
|
|
211
|
+
elements: [{ id: 'agent:graph-root', type: 'agent', label: 'Graph root', data: {} }],
|
|
212
|
+
edges: [{ from: 'agent:graph-root', to: 'tool:missing', kind: 'uses-tool' }],
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
assert.equal(result.ok, true);
|
|
216
|
+
assert.equal(Boolean(resolveAgent(result.rootAgentId)), true);
|
|
217
|
+
});
|