@amalgm/agents 0.1.1 → 0.1.3
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/PURPOSE.md +12 -2
- package/README.md +34 -0
- package/dist/bin/rest.js +7 -2
- package/dist/bundles/create.d.ts +13 -0
- package/dist/bundles/create.js +125 -0
- package/dist/bundles/graph-records.d.ts +10 -0
- package/dist/bundles/graph-records.js +61 -0
- package/dist/bundles/graph.d.ts +6 -0
- package/dist/bundles/graph.js +121 -0
- package/dist/bundles/install.d.ts +6 -0
- package/dist/bundles/install.js +64 -0
- package/dist/bundles/types.d.ts +97 -0
- package/dist/bundles/types.js +1 -0
- package/dist/bundles/util.d.ts +14 -0
- package/dist/bundles/util.js +56 -0
- package/dist/bundles/validate.d.ts +2 -0
- package/dist/bundles/validate.js +92 -0
- package/dist/config/import.d.ts +14 -0
- package/dist/config/import.js +59 -0
- package/dist/config/native.d.ts +2 -0
- package/dist/config/native.js +179 -0
- package/dist/config/schema.d.ts +3 -0
- package/dist/config/schema.js +135 -0
- package/dist/config/store.d.ts +9 -0
- package/dist/config/store.js +56 -0
- package/dist/config/types.d.ts +55 -0
- package/dist/config/types.js +1 -0
- package/dist/http/bundle-routes.d.ts +2 -0
- package/dist/http/bundle-routes.js +67 -0
- package/dist/http/config-routes.d.ts +2 -0
- package/dist/http/config-routes.js +46 -0
- package/dist/http/server.js +5 -1
- package/dist/http-types.d.ts +5 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +9 -0
- package/dist/mcp/server.js +1 -1
- package/docs/ENGINE_INTEGRATION.md +3 -3
- package/docs/REST.md +25 -0
- package/package.json +1 -1
package/PURPOSE.md
CHANGED
|
@@ -13,6 +13,10 @@ and UI. This package stores references to those products and delegates
|
|
|
13
13
|
execution to an injected driver; it does not duplicate their state.
|
|
14
14
|
It also discovers installed agent skills across the canonical project and
|
|
15
15
|
harness roots so every transport presents the same available skill catalog.
|
|
16
|
+
Rich harness configuration is stored inside the same immutable definition and
|
|
17
|
+
projected onto its executable fields; it is not a second agent registry.
|
|
18
|
+
Portable bundle-v2 documents close an agent's subagent, skill, and tool graph;
|
|
19
|
+
hosts supply explicit ports for resources owned by other products.
|
|
16
20
|
|
|
17
21
|
## Primitives
|
|
18
22
|
|
|
@@ -37,11 +41,17 @@ harness roots so every transport presents the same available skill catalog.
|
|
|
37
41
|
none is a second implementation.
|
|
38
42
|
7. An unchanged current definition is idempotent; every actual change creates
|
|
39
43
|
a new immutable revision, even when its content matches an earlier revision.
|
|
44
|
+
8. Agent configuration and its executable projection change atomically in one
|
|
45
|
+
immutable revision; native imports never ingest credentials or MCP config.
|
|
46
|
+
9. A portable bundle is accepted only when its flat graph is derivable from
|
|
47
|
+
its canonical records and every element is reachable from a declared head.
|
|
40
48
|
|
|
41
49
|
## Consequences
|
|
42
50
|
|
|
43
51
|
The package needs no cloud command inbox, scheduler, trigger relay, Chat
|
|
44
52
|
record, or general event-routing system. Engine and other hosts may call the
|
|
45
53
|
SDK whenever they need to route work, but routing is outside the Agents
|
|
46
|
-
product.
|
|
47
|
-
|
|
54
|
+
product. Bundle ports call Apps, Automations, and Toolbox public APIs; Agents
|
|
55
|
+
never imports their stores or duplicates their lifecycle behavior. Internal
|
|
56
|
+
revisions, ordered events, idempotency, and cancellation exist only to make
|
|
57
|
+
the behaviors above durable and predictable.
|
package/README.md
CHANGED
|
@@ -83,6 +83,40 @@ The scanner reads the same canonical project, shared, Codex, Claude Code, and
|
|
|
83
83
|
OpenCode roots as Engine. Hosts inject `skillRoots` when they need a selected
|
|
84
84
|
project or an isolated home.
|
|
85
85
|
|
|
86
|
+
Rich harness configuration remains part of the agent's immutable definition:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { updateAgentConfig } from '@amalgm/agents';
|
|
90
|
+
|
|
91
|
+
const { config } = updateAgentConfig(agents, 'reviewer', {
|
|
92
|
+
instructions: 'Review against the measured contract.',
|
|
93
|
+
skills: [{ name: 'parity', content: 'Run the parity meter.' }],
|
|
94
|
+
hooks: [{ event: 'Stop', type: 'command', command: 'npm test' }],
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The REST adapter also serves `/agent-config`, `/agent-config/get`,
|
|
99
|
+
`/agent-config/update`, and `/agent-config/import-native`. Native import reads
|
|
100
|
+
only instructions, skills, command hooks, and agent markdown from an injected
|
|
101
|
+
home; credentials and MCP configuration never enter Agents.
|
|
102
|
+
|
|
103
|
+
Portable bundle-v2 documents use the same SDK law as the REST surface:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const { bundle, preview } = await createAgentBundle(
|
|
107
|
+
agents,
|
|
108
|
+
{ agentIds: ['reviewer'] },
|
|
109
|
+
{ port: hostResourcePort },
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
await installAgentBundle(agents, bundle, { port: hostResourcePort });
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Agents closes recursive subagents, skills, tools, and the canonical flat
|
|
116
|
+
graph. The injected port exports and installs Apps, Automations, and Toolbox
|
|
117
|
+
records through their owning SDKs. A missing port or graph mismatch fails
|
|
118
|
+
explicitly; dependencies are never omitted to make a bundle look portable.
|
|
119
|
+
|
|
86
120
|
## CLI
|
|
87
121
|
|
|
88
122
|
```bash
|
package/dist/bin/rest.js
CHANGED
|
@@ -6,12 +6,17 @@ import { fatal } from './fatal.js';
|
|
|
6
6
|
async function main() {
|
|
7
7
|
const args = parseArgs(process.argv.slice(2));
|
|
8
8
|
if (args.flags.has('help')) {
|
|
9
|
-
process.stdout.write('Usage: amalgm-agents-rest [--host 127.0.0.1] [--port 4317] [--token value]\n');
|
|
9
|
+
process.stdout.write('Usage: amalgm-agents-rest [--host 127.0.0.1] [--port 4317] [--token value] [--native-home dir]\n');
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
12
12
|
const agents = await openAgents(args);
|
|
13
13
|
const token = flag(args, 'token') || process.env.AMALGM_AGENTS_TOKEN;
|
|
14
|
-
const
|
|
14
|
+
const nativeHomeDir = flag(args, 'native-home') || process.env.AMALGM_NATIVE_HOME;
|
|
15
|
+
const rest = createRestServer({
|
|
16
|
+
agents,
|
|
17
|
+
...(token ? { token } : {}),
|
|
18
|
+
...(nativeHomeDir ? { nativeHomeDir } : {}),
|
|
19
|
+
});
|
|
15
20
|
const address = await rest.listen(Number(flag(args, 'port') || 4317), flag(args, 'host') || '127.0.0.1');
|
|
16
21
|
process.stderr.write(`amalgm-agents REST listening on http://${address.address}:${address.port}\n`);
|
|
17
22
|
const close = () => void rest.close().then(() => agents.close()).catch(fatal);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Agents } from '../agents.js';
|
|
2
|
+
import type { InstalledSkill } from '../skills/scanner.js';
|
|
3
|
+
import type { AgentBundle, AgentBundlePort, BundleCreateInput } from './types.js';
|
|
4
|
+
export interface CreateBundleOptions {
|
|
5
|
+
port?: AgentBundlePort;
|
|
6
|
+
installedSkills?: InstalledSkill[];
|
|
7
|
+
bundleId?: string;
|
|
8
|
+
now?: () => string;
|
|
9
|
+
}
|
|
10
|
+
export declare function createAgentBundle(agents: Agents, input: BundleCreateInput, options?: CreateBundleOptions): Promise<{
|
|
11
|
+
bundle: AgentBundle;
|
|
12
|
+
preview: Record<string, unknown>;
|
|
13
|
+
}>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { configFromAgent } from '../config/store.js';
|
|
2
|
+
import { buildBundleGraph } from './graph.js';
|
|
3
|
+
import { collectSkillRecords } from './graph-records.js';
|
|
4
|
+
import { BUNDLE_KIND, BUNDLE_SCHEMA_VERSION, addUnique, bundleId, emptyRequirements, mergeRequirements, scrubSecrets, uniqueStrings, } from './util.js';
|
|
5
|
+
function hydratedSkill(skill, installed) {
|
|
6
|
+
if (skill.content)
|
|
7
|
+
return skill;
|
|
8
|
+
const sourcePath = typeof skill.source.path === 'string' ? skill.source.path : '';
|
|
9
|
+
const match = installed.find((item) => item.id === skill.id || item.name === skill.name
|
|
10
|
+
|| (sourcePath && item.source.path === sourcePath));
|
|
11
|
+
return match ? {
|
|
12
|
+
...skill,
|
|
13
|
+
name: match.name,
|
|
14
|
+
description: skill.description || match.description,
|
|
15
|
+
content: match.content || '',
|
|
16
|
+
source: match.source,
|
|
17
|
+
} : skill;
|
|
18
|
+
}
|
|
19
|
+
function collectAgents(agents, roots, installed) {
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
const entries = [];
|
|
22
|
+
const visit = (id) => {
|
|
23
|
+
if (!id || seen.has(id))
|
|
24
|
+
return;
|
|
25
|
+
seen.add(id);
|
|
26
|
+
const record = agents.getAgent(id);
|
|
27
|
+
if (!record)
|
|
28
|
+
throw new Error(`Agent not found: ${id}`);
|
|
29
|
+
const config = configFromAgent(record);
|
|
30
|
+
config.skills = config.skills.map((skill) => hydratedSkill(skill, installed));
|
|
31
|
+
entries.push({
|
|
32
|
+
sourceAgentId: record.id,
|
|
33
|
+
agent: {
|
|
34
|
+
name: record.definition.name,
|
|
35
|
+
description: record.definition.description,
|
|
36
|
+
driverId: record.definition.driver.id,
|
|
37
|
+
modelId: record.definition.model?.id || '',
|
|
38
|
+
modelSettings: record.definition.model?.settings || null,
|
|
39
|
+
},
|
|
40
|
+
config,
|
|
41
|
+
});
|
|
42
|
+
config.subagents.forEach((subagent) => visit(subagent.agentId));
|
|
43
|
+
};
|
|
44
|
+
roots.forEach(visit);
|
|
45
|
+
return entries;
|
|
46
|
+
}
|
|
47
|
+
function title(entries, external) {
|
|
48
|
+
const agent = entries[0]?.agent;
|
|
49
|
+
if (agent)
|
|
50
|
+
return { title: agent.name || 'Shared agent', description: agent.description || '' };
|
|
51
|
+
for (const records of [external.automations, external.apps, external.tools]) {
|
|
52
|
+
const first = records?.[0];
|
|
53
|
+
const summary = first && typeof first === 'object'
|
|
54
|
+
? (first.automation || first.app || first) : null;
|
|
55
|
+
if (summary)
|
|
56
|
+
return {
|
|
57
|
+
title: typeof summary.name === 'string' ? summary.name : 'Shared bundle',
|
|
58
|
+
description: typeof summary.description === 'string' ? summary.description : '',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return { title: 'Shared bundle', description: '' };
|
|
62
|
+
}
|
|
63
|
+
// Engine reference: amalgm-mcp/agent-bundles/export.js createBundle.
|
|
64
|
+
export async function createAgentBundle(agents, input, options = {}) {
|
|
65
|
+
const agentIds = uniqueStrings(input.agentIds);
|
|
66
|
+
const automationIds = uniqueStrings(input.automationIds);
|
|
67
|
+
const appIds = uniqueStrings(input.appIds);
|
|
68
|
+
const requestedToolIds = uniqueStrings(input.toolIds);
|
|
69
|
+
if (![agentIds, automationIds, appIds, requestedToolIds].some((ids) => ids.length)) {
|
|
70
|
+
throw new Error('At least one agent, automation, app, or tool is required');
|
|
71
|
+
}
|
|
72
|
+
const bundleAgents = collectAgents(agents, agentIds, options.installedSkills || []);
|
|
73
|
+
const agentToolIds = bundleAgents.flatMap((entry) => entry.config.loadout.toolIds);
|
|
74
|
+
const needsPort = automationIds.length || appIds.length || requestedToolIds.length || agentToolIds.length;
|
|
75
|
+
if (needsPort && !options.port)
|
|
76
|
+
throw new Error('A bundle resource port is required for tools, apps, or automations');
|
|
77
|
+
const external = options.port ? await options.port.exportResources({
|
|
78
|
+
automationIds, appIds, toolIds: uniqueStrings([...requestedToolIds, ...agentToolIds]),
|
|
79
|
+
}) : {};
|
|
80
|
+
const requires = mergeRequirements(emptyRequirements(), external.requires);
|
|
81
|
+
for (const entry of bundleAgents) {
|
|
82
|
+
addUnique(requires.runtimes, entry.agent.driverId);
|
|
83
|
+
addUnique(requires.auth, entry.agent.driverId);
|
|
84
|
+
}
|
|
85
|
+
const tools = (external.tools || []).map((tool) => {
|
|
86
|
+
if (tool.origin === 'system')
|
|
87
|
+
addUnique(requires.systemTools, String(tool.id || ''));
|
|
88
|
+
return scrubSecrets(tool, `tools.${String(tool.id || '')}`, requires);
|
|
89
|
+
});
|
|
90
|
+
const toolActions = (external.toolActions || []).map((action) => scrubSecrets(action, `toolActions.${String(action.id || '')}`, requires));
|
|
91
|
+
if (requires.missingTools.length)
|
|
92
|
+
throw new Error(`Tool not found: ${requires.missingTools.join(', ')}`);
|
|
93
|
+
const skills = collectSkillRecords(bundleAgents);
|
|
94
|
+
const hooks = bundleAgents.reduce((count, entry) => count + entry.config.hooks.length, 0);
|
|
95
|
+
const warnings = [
|
|
96
|
+
'Instructions, skill text, tool descriptions, and hook commands are shared as plain text.',
|
|
97
|
+
'Secrets pasted into free-text instructions or files are not automatically detected.',
|
|
98
|
+
...(hooks ? ['This bundle includes executable hook definitions. Review commands before importing.'] : []),
|
|
99
|
+
...(tools.length ? ['Tools can call external services or local commands. Reconnect credentials locally.'] : []),
|
|
100
|
+
...(external.warnings || []),
|
|
101
|
+
];
|
|
102
|
+
const named = title(bundleAgents, external);
|
|
103
|
+
const headToolIds = external.headToolIds || requestedToolIds;
|
|
104
|
+
const heads = [
|
|
105
|
+
...agentIds.map((id) => ({ type: 'agent', id })),
|
|
106
|
+
...automationIds.map((id) => ({ type: 'automation', id })),
|
|
107
|
+
...appIds.map((id) => ({ type: 'app', id })),
|
|
108
|
+
...headToolIds.map((id) => ({ type: 'tool', id })),
|
|
109
|
+
];
|
|
110
|
+
const summary = {
|
|
111
|
+
agents: bundleAgents.length, automations: external.automations?.length || 0,
|
|
112
|
+
apps: external.apps?.length || 0, tools: tools.length, toolActions: toolActions.length,
|
|
113
|
+
skills: skills.length, hooks, assets: 0,
|
|
114
|
+
};
|
|
115
|
+
const bundle = {
|
|
116
|
+
kind: BUNDLE_KIND, schemaVersion: BUNDLE_SCHEMA_VERSION,
|
|
117
|
+
bundleId: options.bundleId || bundleId(), createdAt: options.now?.() || new Date().toISOString(),
|
|
118
|
+
rootAgentId: agentIds[0] || null, heads, headIds: [], ...named,
|
|
119
|
+
agents: bundleAgents, automations: external.automations || [], apps: external.apps || [],
|
|
120
|
+
tools, toolActions, skills, assets: [], requires, warnings, summary, elements: [], edges: [],
|
|
121
|
+
};
|
|
122
|
+
Object.assign(bundle, buildBundleGraph(bundle));
|
|
123
|
+
const bytes = Buffer.byteLength(JSON.stringify(bundle));
|
|
124
|
+
return { bundle, preview: { bundleId: bundle.bundleId, ...named, summary, requires, warnings, bytes } };
|
|
125
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AgentBundle, BundleAgentEntry } from './types.js';
|
|
2
|
+
export declare function agentEntryId(entry: unknown): string;
|
|
3
|
+
export declare function automationEntryId(entry: unknown): string;
|
|
4
|
+
export declare function appEntryId(entry: unknown): string;
|
|
5
|
+
export declare function toolEntryId(entry: unknown): string;
|
|
6
|
+
export declare function actionEntryId(entry: unknown): string;
|
|
7
|
+
export declare function skillFingerprint(skill: unknown): string;
|
|
8
|
+
export declare function skillElementId(skill: unknown): string;
|
|
9
|
+
export declare function skillLabel(skill: unknown): string;
|
|
10
|
+
export declare function collectSkillRecords(agents: BundleAgentEntry[]): AgentBundle['skills'];
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { clean, digest, object } from './util.js';
|
|
2
|
+
export function agentEntryId(entry) {
|
|
3
|
+
const record = object(entry);
|
|
4
|
+
return clean(record?.sourceAgentId) || clean(object(record?.agent)?.sourceAgentId);
|
|
5
|
+
}
|
|
6
|
+
export function automationEntryId(entry) {
|
|
7
|
+
return clean(object(entry)?.sourceAutomationId);
|
|
8
|
+
}
|
|
9
|
+
export function appEntryId(entry) {
|
|
10
|
+
return clean(object(entry)?.sourceAppId);
|
|
11
|
+
}
|
|
12
|
+
export function toolEntryId(entry) {
|
|
13
|
+
return clean(object(entry)?.id);
|
|
14
|
+
}
|
|
15
|
+
export function actionEntryId(entry) {
|
|
16
|
+
return clean(object(entry)?.id);
|
|
17
|
+
}
|
|
18
|
+
export function skillFingerprint(skill) {
|
|
19
|
+
if (typeof skill === 'string')
|
|
20
|
+
return clean(skill) ? `string:${clean(skill)}` : '';
|
|
21
|
+
const record = object(skill);
|
|
22
|
+
if (!record)
|
|
23
|
+
return '';
|
|
24
|
+
if (clean(record.id))
|
|
25
|
+
return `id:${clean(record.id)}`;
|
|
26
|
+
if (clean(record.path))
|
|
27
|
+
return `path:${clean(record.path)}`;
|
|
28
|
+
const name = clean(record.name);
|
|
29
|
+
const content = clean(record.content);
|
|
30
|
+
if (name && content)
|
|
31
|
+
return `name-content:${name}:${content}`;
|
|
32
|
+
if (name)
|
|
33
|
+
return `name:${name}`;
|
|
34
|
+
if (content)
|
|
35
|
+
return `content:${content}`;
|
|
36
|
+
return `json:${JSON.stringify(skill)}`;
|
|
37
|
+
}
|
|
38
|
+
export function skillElementId(skill) {
|
|
39
|
+
const fingerprint = skillFingerprint(skill);
|
|
40
|
+
return fingerprint ? `skill:${digest(fingerprint)}` : '';
|
|
41
|
+
}
|
|
42
|
+
export function skillLabel(skill) {
|
|
43
|
+
if (typeof skill === 'string')
|
|
44
|
+
return clean(skill) || 'Skill';
|
|
45
|
+
const record = object(skill);
|
|
46
|
+
if (!record)
|
|
47
|
+
return 'Skill';
|
|
48
|
+
return clean(record.name) || clean(record.path).split('/').filter(Boolean).pop()
|
|
49
|
+
|| clean(record.id) || clean(record.content).slice(0, 60) || 'Skill';
|
|
50
|
+
}
|
|
51
|
+
export function collectSkillRecords(agents) {
|
|
52
|
+
const records = new Map();
|
|
53
|
+
for (const entry of agents) {
|
|
54
|
+
for (const skill of entry.config.skills) {
|
|
55
|
+
const key = skillFingerprint(skill);
|
|
56
|
+
if (key && !records.has(key))
|
|
57
|
+
records.set(key, JSON.parse(JSON.stringify(skill)));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return [...records.values()];
|
|
61
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { actionEntryId, agentEntryId, appEntryId, automationEntryId, skillElementId, skillLabel, toolEntryId, } from './graph-records.js';
|
|
2
|
+
import { clean, object, uniqueStrings } from './util.js';
|
|
3
|
+
const ORDER = {
|
|
4
|
+
agent: 0, automation: 1, app: 2, tool: 3, tool_action: 4, skill: 5,
|
|
5
|
+
};
|
|
6
|
+
function elementId(type, id) {
|
|
7
|
+
return `${type}:${id}`;
|
|
8
|
+
}
|
|
9
|
+
function graphRecords(bundle) {
|
|
10
|
+
return {
|
|
11
|
+
agent: new Map(bundle.agents.map((entry) => [agentEntryId(entry), entry])),
|
|
12
|
+
automation: new Map(bundle.automations.map((entry) => [automationEntryId(entry), entry])),
|
|
13
|
+
app: new Map(bundle.apps.map((entry) => [appEntryId(entry), entry])),
|
|
14
|
+
tool: new Map(bundle.tools.map((entry) => [toolEntryId(entry), entry])),
|
|
15
|
+
tool_action: new Map(bundle.toolActions.map((entry) => [actionEntryId(entry), entry])),
|
|
16
|
+
skill: new Map(bundle.skills.map((entry) => [skillElementId(entry), entry])),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
// Engine reference: amalgm-mcp/agent-bundles/graph.js buildFlatAgentBundleGraph.
|
|
20
|
+
export function buildBundleGraph(bundle) {
|
|
21
|
+
const records = graphRecords(bundle);
|
|
22
|
+
const elements = new Map();
|
|
23
|
+
const edges = new Map();
|
|
24
|
+
const addElement = (type, id, label) => {
|
|
25
|
+
const key = elementId(type, id);
|
|
26
|
+
elements.set(key, { id: key, type, label: label || id });
|
|
27
|
+
};
|
|
28
|
+
const addEdge = (fromType, fromId, toType, toId, kind) => {
|
|
29
|
+
const edge = { from: elementId(fromType, fromId), to: elementId(toType, toId), kind };
|
|
30
|
+
edges.set(`${edge.from}::${kind}::${edge.to}`, edge);
|
|
31
|
+
};
|
|
32
|
+
const requireRecord = (type, id, owner) => {
|
|
33
|
+
if (!records[type].has(id))
|
|
34
|
+
throw new Error(`${owner} references missing ${type.replace('_', ' ')}: ${id}`);
|
|
35
|
+
};
|
|
36
|
+
const addToolSelection = (ownerType, ownerId, selectedId) => {
|
|
37
|
+
if (records.tool_action.has(selectedId))
|
|
38
|
+
addEdge(ownerType, ownerId, 'tool_action', selectedId, 'uses-tool-action');
|
|
39
|
+
else {
|
|
40
|
+
requireRecord('tool', selectedId, `${ownerType} ${ownerId}`);
|
|
41
|
+
addEdge(ownerType, ownerId, 'tool', selectedId, 'uses-tool');
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
for (const entry of bundle.agents)
|
|
45
|
+
addElement('agent', agentEntryId(entry), entry.agent.name);
|
|
46
|
+
for (const entry of bundle.automations) {
|
|
47
|
+
addElement('automation', automationEntryId(entry), clean(object(entry.automation)?.name));
|
|
48
|
+
}
|
|
49
|
+
for (const entry of bundle.apps)
|
|
50
|
+
addElement('app', appEntryId(entry), clean(object(entry.app)?.name));
|
|
51
|
+
for (const entry of bundle.tools)
|
|
52
|
+
addElement('tool', toolEntryId(entry), clean(entry.name));
|
|
53
|
+
for (const entry of bundle.toolActions)
|
|
54
|
+
addElement('tool_action', actionEntryId(entry), clean(entry.name));
|
|
55
|
+
for (const skill of bundle.skills) {
|
|
56
|
+
const id = skillElementId(skill);
|
|
57
|
+
elements.set(id, { id, type: 'skill', label: skillLabel(skill) });
|
|
58
|
+
}
|
|
59
|
+
for (const entry of bundle.agents) {
|
|
60
|
+
const id = agentEntryId(entry);
|
|
61
|
+
for (const subagent of entry.config.subagents) {
|
|
62
|
+
if (!subagent.agentId)
|
|
63
|
+
continue;
|
|
64
|
+
requireRecord('agent', subagent.agentId, `agent ${id}`);
|
|
65
|
+
addEdge('agent', id, 'agent', subagent.agentId, 'uses-subagent');
|
|
66
|
+
}
|
|
67
|
+
for (const selectedId of entry.config.loadout.toolIds)
|
|
68
|
+
addToolSelection('agent', id, selectedId);
|
|
69
|
+
for (const skill of entry.config.skills) {
|
|
70
|
+
const skillId = skillElementId(skill);
|
|
71
|
+
if (!records.skill.has(skillId))
|
|
72
|
+
throw new Error(`agent ${id} references missing skill: ${skillLabel(skill)}`);
|
|
73
|
+
addEdge('agent', id, 'skill', skillId.slice('skill:'.length), 'uses-skill');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const entry of bundle.automations) {
|
|
77
|
+
const id = automationEntryId(entry);
|
|
78
|
+
for (const selectedId of uniqueStrings(entry.toolIds))
|
|
79
|
+
addToolSelection('automation', id, selectedId);
|
|
80
|
+
}
|
|
81
|
+
for (const entry of bundle.apps) {
|
|
82
|
+
const id = appEntryId(entry);
|
|
83
|
+
for (const toolId of uniqueStrings(entry.toolIds)) {
|
|
84
|
+
requireRecord('tool', toolId, `app ${id}`);
|
|
85
|
+
addEdge('app', id, 'tool', toolId, 'provides-tool');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const tool of bundle.tools) {
|
|
89
|
+
const id = toolEntryId(tool);
|
|
90
|
+
const appId = clean(tool.appId);
|
|
91
|
+
if (appId) {
|
|
92
|
+
requireRecord('app', appId, `tool ${id}`);
|
|
93
|
+
addEdge('tool', id, 'app', appId, 'requires-app');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (const action of bundle.toolActions) {
|
|
97
|
+
const id = actionEntryId(action);
|
|
98
|
+
const toolId = clean(action.toolId);
|
|
99
|
+
requireRecord('tool', toolId, `tool action ${id}`);
|
|
100
|
+
addEdge('tool', toolId, 'tool_action', id, 'has-action');
|
|
101
|
+
addEdge('tool_action', id, 'tool', toolId, 'action-of');
|
|
102
|
+
const appId = clean(action.appId);
|
|
103
|
+
if (appId) {
|
|
104
|
+
requireRecord('app', appId, `tool action ${id}`);
|
|
105
|
+
addEdge('tool_action', id, 'app', appId, 'requires-app');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const headIds = bundle.heads.map((head) => {
|
|
109
|
+
const id = elementId(head.type, head.id);
|
|
110
|
+
if (!elements.has(id))
|
|
111
|
+
throw new Error(`bundle head references missing element: ${id}`);
|
|
112
|
+
return id;
|
|
113
|
+
});
|
|
114
|
+
return {
|
|
115
|
+
headIds,
|
|
116
|
+
elements: [...elements.values()].sort((left, right) => (ORDER[left.type] ?? 99) - (ORDER[right.type] ?? 99)
|
|
117
|
+
|| left.id.localeCompare(right.id)),
|
|
118
|
+
edges: [...edges.values()].sort((left, right) => left.from.localeCompare(right.from)
|
|
119
|
+
|| left.kind.localeCompare(right.kind) || left.to.localeCompare(right.to)),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Agents } from '../agents.js';
|
|
2
|
+
import type { AgentBundlePort, BundleInstallResult } from './types.js';
|
|
3
|
+
export declare function installAgentBundle(agents: Agents, input: unknown, options?: {
|
|
4
|
+
port?: AgentBundlePort;
|
|
5
|
+
authRef?: string;
|
|
6
|
+
}): Promise<BundleInstallResult>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { normalizeAgentConfig } from '../config/schema.js';
|
|
3
|
+
import { updateAgentConfig } from '../config/store.js';
|
|
4
|
+
import { validateAgentBundle } from './validate.js';
|
|
5
|
+
function uniqueName(agents, value) {
|
|
6
|
+
const base = value.trim() || 'Imported agent';
|
|
7
|
+
const names = new Set(agents.listAgents().map((item) => item.definition.name.toLowerCase()));
|
|
8
|
+
if (!names.has(base.toLowerCase()))
|
|
9
|
+
return base;
|
|
10
|
+
if (!names.has(`${base} (imported)`.toLowerCase()))
|
|
11
|
+
return `${base} (imported)`;
|
|
12
|
+
for (let index = 2; index < 1000; index += 1) {
|
|
13
|
+
const candidate = `${base} (imported ${index})`;
|
|
14
|
+
if (!names.has(candidate.toLowerCase()))
|
|
15
|
+
return candidate;
|
|
16
|
+
}
|
|
17
|
+
return `${base} (${crypto.randomUUID().slice(0, 8)})`;
|
|
18
|
+
}
|
|
19
|
+
// Engine reference: amalgm-mcp/agent-bundles/install.js installBundleAgents.
|
|
20
|
+
export async function installAgentBundle(agents, input, options = {}) {
|
|
21
|
+
const bundle = validateAgentBundle(input);
|
|
22
|
+
const hasExternal = bundle.automations.length || bundle.apps.length || bundle.tools.length;
|
|
23
|
+
if (hasExternal && !options.port)
|
|
24
|
+
throw new Error('A bundle resource port is required to install external resources');
|
|
25
|
+
const external = options.port ? await options.port.installResources(bundle) : {};
|
|
26
|
+
const idMap = new Map();
|
|
27
|
+
const created = [];
|
|
28
|
+
for (const entry of bundle.agents) {
|
|
29
|
+
const agent = agents.createAgent({
|
|
30
|
+
id: `imported-${crypto.randomUUID()}`,
|
|
31
|
+
name: uniqueName(agents, entry.agent.name),
|
|
32
|
+
description: entry.agent.description,
|
|
33
|
+
driver: { id: entry.agent.driverId, config: {} },
|
|
34
|
+
model: entry.agent.modelId ? { id: entry.agent.modelId, settings: entry.agent.modelSettings || {} } : null,
|
|
35
|
+
authRef: options.authRef || null,
|
|
36
|
+
instructions: entry.config.instructions,
|
|
37
|
+
resources: { files: [], skills: [], subagents: [] },
|
|
38
|
+
toolbox: { toolIds: entry.config.loadout.toolIds, actionIds: [] },
|
|
39
|
+
metadata: { installedFromBundle: bundle.bundleId },
|
|
40
|
+
});
|
|
41
|
+
idMap.set(entry.sourceAgentId, agent.id);
|
|
42
|
+
created.push({ sourceId: entry.sourceAgentId, agent, config: entry.config });
|
|
43
|
+
}
|
|
44
|
+
const installedAgents = created.map(({ agent, config }) => {
|
|
45
|
+
const rewritten = normalizeAgentConfig({
|
|
46
|
+
...config,
|
|
47
|
+
agentId: agent.id,
|
|
48
|
+
subagents: config.subagents.map((subagent) => ({
|
|
49
|
+
...subagent,
|
|
50
|
+
agentId: idMap.get(subagent.agentId) || '',
|
|
51
|
+
})).filter((subagent) => subagent.agentId),
|
|
52
|
+
});
|
|
53
|
+
return updateAgentConfig(agents, agent.id, rewritten).agent;
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
bundleId: bundle.bundleId,
|
|
58
|
+
rootAgentId: bundle.rootAgentId ? idMap.get(bundle.rootAgentId) || null : installedAgents[0]?.id || null,
|
|
59
|
+
idMap: Object.fromEntries(idMap),
|
|
60
|
+
installedAgents,
|
|
61
|
+
...external,
|
|
62
|
+
warnings: [...(bundle.warnings || []), ...(external.warnings || [])],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { AgentConfig } from '../config/types.js';
|
|
2
|
+
import type { AgentRecord, JsonObject } from '../types.js';
|
|
3
|
+
export type BundleHeadType = 'agent' | 'automation' | 'app' | 'tool';
|
|
4
|
+
export interface BundleHead {
|
|
5
|
+
type: BundleHeadType;
|
|
6
|
+
id: string;
|
|
7
|
+
}
|
|
8
|
+
export interface BundleAgentEntry {
|
|
9
|
+
sourceAgentId: string;
|
|
10
|
+
agent: {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
driverId: string;
|
|
14
|
+
modelId: string;
|
|
15
|
+
modelSettings: JsonObject | null;
|
|
16
|
+
};
|
|
17
|
+
config: AgentConfig;
|
|
18
|
+
}
|
|
19
|
+
export interface BundleGraphElement {
|
|
20
|
+
id: string;
|
|
21
|
+
type: string;
|
|
22
|
+
label: string;
|
|
23
|
+
}
|
|
24
|
+
export interface BundleGraphEdge {
|
|
25
|
+
from: string;
|
|
26
|
+
to: string;
|
|
27
|
+
kind: string;
|
|
28
|
+
}
|
|
29
|
+
export interface BundleRequirements {
|
|
30
|
+
runtimes: string[];
|
|
31
|
+
auth: string[];
|
|
32
|
+
secrets: string[];
|
|
33
|
+
bindings: string[];
|
|
34
|
+
systemTools: string[];
|
|
35
|
+
systemActions: string[];
|
|
36
|
+
missingTools: string[];
|
|
37
|
+
}
|
|
38
|
+
export interface AgentBundle {
|
|
39
|
+
kind: 'amalgm.bundle';
|
|
40
|
+
schemaVersion: 2;
|
|
41
|
+
bundleId: string;
|
|
42
|
+
createdAt: string;
|
|
43
|
+
rootAgentId: string | null;
|
|
44
|
+
heads: BundleHead[];
|
|
45
|
+
headIds: string[];
|
|
46
|
+
title: string;
|
|
47
|
+
description: string;
|
|
48
|
+
agents: BundleAgentEntry[];
|
|
49
|
+
automations: JsonObject[];
|
|
50
|
+
apps: JsonObject[];
|
|
51
|
+
tools: JsonObject[];
|
|
52
|
+
toolActions: JsonObject[];
|
|
53
|
+
skills: JsonObject[];
|
|
54
|
+
assets: JsonObject[];
|
|
55
|
+
requires: BundleRequirements;
|
|
56
|
+
warnings: string[];
|
|
57
|
+
summary: JsonObject;
|
|
58
|
+
elements: BundleGraphElement[];
|
|
59
|
+
edges: BundleGraphEdge[];
|
|
60
|
+
}
|
|
61
|
+
export interface BundleExternalExport {
|
|
62
|
+
headToolIds?: string[];
|
|
63
|
+
automations?: JsonObject[];
|
|
64
|
+
apps?: JsonObject[];
|
|
65
|
+
tools?: JsonObject[];
|
|
66
|
+
toolActions?: JsonObject[];
|
|
67
|
+
requires?: Partial<BundleRequirements>;
|
|
68
|
+
warnings?: string[];
|
|
69
|
+
}
|
|
70
|
+
export interface BundleExternalInstall {
|
|
71
|
+
installedAutomations?: JsonObject[];
|
|
72
|
+
installedApps?: JsonObject[];
|
|
73
|
+
installedTools?: JsonObject[];
|
|
74
|
+
warnings?: string[];
|
|
75
|
+
}
|
|
76
|
+
export interface AgentBundlePort {
|
|
77
|
+
exportResources(input: {
|
|
78
|
+
automationIds: string[];
|
|
79
|
+
appIds: string[];
|
|
80
|
+
toolIds: string[];
|
|
81
|
+
}): Promise<BundleExternalExport> | BundleExternalExport;
|
|
82
|
+
installResources(bundle: AgentBundle): Promise<BundleExternalInstall> | BundleExternalInstall;
|
|
83
|
+
}
|
|
84
|
+
export interface BundleCreateInput {
|
|
85
|
+
agentIds?: string[];
|
|
86
|
+
automationIds?: string[];
|
|
87
|
+
appIds?: string[];
|
|
88
|
+
toolIds?: string[];
|
|
89
|
+
}
|
|
90
|
+
export interface BundleInstallResult extends BundleExternalInstall {
|
|
91
|
+
ok: true;
|
|
92
|
+
bundleId: string;
|
|
93
|
+
rootAgentId: string | null;
|
|
94
|
+
idMap: Record<string, string>;
|
|
95
|
+
installedAgents: AgentRecord[];
|
|
96
|
+
warnings: string[];
|
|
97
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { JsonObject, JsonValue } from '../types.js';
|
|
2
|
+
import type { BundleRequirements } from './types.js';
|
|
3
|
+
export declare const BUNDLE_KIND: "amalgm.bundle";
|
|
4
|
+
export declare const BUNDLE_SCHEMA_VERSION: 2;
|
|
5
|
+
export declare function clean(value: unknown): string;
|
|
6
|
+
export declare function object(value: unknown): JsonObject | null;
|
|
7
|
+
export declare function clone<T>(value: T): T;
|
|
8
|
+
export declare function uniqueStrings(value: unknown): string[];
|
|
9
|
+
export declare function addUnique(target: string[], value: string): void;
|
|
10
|
+
export declare function bundleId(): string;
|
|
11
|
+
export declare function digest(value: unknown): string;
|
|
12
|
+
export declare function emptyRequirements(): BundleRequirements;
|
|
13
|
+
export declare function mergeRequirements(target: BundleRequirements, incoming?: Partial<BundleRequirements>): BundleRequirements;
|
|
14
|
+
export declare function scrubSecrets(value: JsonValue, path: string, requires: BundleRequirements): JsonValue;
|