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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.126",
3
+ "version": "0.1.127",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -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 byId = new Map();
187
+ const byKey = new Map();
112
188
  for (const entry of bundleAgents) {
113
189
  for (const skill of entry.config.skills || []) {
114
- if (!skill?.id) continue;
115
- byId.set(skill.id, cloneJson(skill));
190
+ const key = skillFingerprint(skill);
191
+ if (!key) continue;
192
+ byKey.set(key, cloneJson(skill));
116
193
  }
117
194
  }
118
- return [...byId.values()];
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
+ });