@amalgm/agents 0.1.1 → 0.1.2
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 +4 -0
- package/README.md +17 -0
- package/dist/bin/rest.js +7 -2
- 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/config-routes.d.ts +2 -0
- package/dist/http/config-routes.js +46 -0
- package/dist/http/server.js +3 -1
- package/dist/http-types.d.ts +2 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/mcp/server.js +1 -1
- package/docs/REST.md +13 -0
- package/package.json +1 -1
package/PURPOSE.md
CHANGED
|
@@ -13,6 +13,8 @@ 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.
|
|
16
18
|
|
|
17
19
|
## Primitives
|
|
18
20
|
|
|
@@ -37,6 +39,8 @@ harness roots so every transport presents the same available skill catalog.
|
|
|
37
39
|
none is a second implementation.
|
|
38
40
|
7. An unchanged current definition is idempotent; every actual change creates
|
|
39
41
|
a new immutable revision, even when its content matches an earlier revision.
|
|
42
|
+
8. Agent configuration and its executable projection change atomically in one
|
|
43
|
+
immutable revision; native imports never ingest credentials or MCP config.
|
|
40
44
|
|
|
41
45
|
## Consequences
|
|
42
46
|
|
package/README.md
CHANGED
|
@@ -83,6 +83,23 @@ 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
|
+
|
|
86
103
|
## CLI
|
|
87
104
|
|
|
88
105
|
```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,14 @@
|
|
|
1
|
+
import type { Agents } from '../agents.js';
|
|
2
|
+
import type { AgentRecord } from '../types.js';
|
|
3
|
+
import type { AgentConfig } from './types.js';
|
|
4
|
+
export declare function importAgentConfig(agents: Agents, agentId: string, options?: {
|
|
5
|
+
homeDir?: string;
|
|
6
|
+
replace?: boolean;
|
|
7
|
+
}): {
|
|
8
|
+
config: AgentConfig;
|
|
9
|
+
importedAgents: {
|
|
10
|
+
created: boolean;
|
|
11
|
+
agent: AgentRecord;
|
|
12
|
+
}[];
|
|
13
|
+
sources: string[];
|
|
14
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { AgentError } from '../errors.js';
|
|
2
|
+
import { importNativeConfig } from './native.js';
|
|
3
|
+
import { normalizeAgentConfig, stableConfigId } from './schema.js';
|
|
4
|
+
import { configFromAgent, updateAgentConfig } from './store.js';
|
|
5
|
+
function mergeById(existing, incoming) {
|
|
6
|
+
return [...new Map([...existing, ...incoming].map((item) => [item.id, item])).values()];
|
|
7
|
+
}
|
|
8
|
+
function mergeSubagents(existing, incoming) {
|
|
9
|
+
return [...new Map([...existing, ...incoming].map((item) => [item.agentId, item])).values()];
|
|
10
|
+
}
|
|
11
|
+
function importedId(parent, name) {
|
|
12
|
+
return stableConfigId('imported-agent', `${parent.id}-${name || 'subagent'}`);
|
|
13
|
+
}
|
|
14
|
+
function ensureImportedAgent(agents, parent, native) {
|
|
15
|
+
const id = importedId(parent, native.name);
|
|
16
|
+
const existing = agents.getAgent(id) || agents.listAgents().find((item) => item.definition.name === native.name);
|
|
17
|
+
if (existing) {
|
|
18
|
+
updateAgentConfig(agents, existing.id, {
|
|
19
|
+
instructions: native.instructions || existing.definition.instructions,
|
|
20
|
+
});
|
|
21
|
+
return { agent: agents.getAgent(existing.id), created: false };
|
|
22
|
+
}
|
|
23
|
+
const agent = agents.createAgent({
|
|
24
|
+
id, name: native.name, description: native.description,
|
|
25
|
+
driver: { ...parent.definition.driver, id: native.driverId }, model: parent.definition.model,
|
|
26
|
+
authRef: parent.definition.authRef, instructions: native.instructions,
|
|
27
|
+
resources: { files: [], skills: [], subagents: [] },
|
|
28
|
+
toolbox: { toolIds: [], actionIds: [] }, workspace: parent.definition.workspace,
|
|
29
|
+
metadata: { importedFrom: native.source },
|
|
30
|
+
});
|
|
31
|
+
updateAgentConfig(agents, agent.id, { instructions: native.instructions });
|
|
32
|
+
return { agent: agents.getAgent(agent.id), created: true };
|
|
33
|
+
}
|
|
34
|
+
function mergedConfig(existing, imported, subagents, replace) {
|
|
35
|
+
if (replace)
|
|
36
|
+
return normalizeAgentConfig({ ...existing, ...imported, subagents }, existing);
|
|
37
|
+
return normalizeAgentConfig({
|
|
38
|
+
...existing,
|
|
39
|
+
instructions: existing.instructions || imported.instructions || '',
|
|
40
|
+
skills: mergeById(existing.skills, imported.skills || []),
|
|
41
|
+
hooks: mergeById(existing.hooks, imported.hooks || []),
|
|
42
|
+
subagents: mergeSubagents(existing.subagents, subagents),
|
|
43
|
+
}, existing);
|
|
44
|
+
}
|
|
45
|
+
// Engine reference: amalgm-mcp/agent-config/rest.js handleImportNative.
|
|
46
|
+
export function importAgentConfig(agents, agentId, options = {}) {
|
|
47
|
+
const parent = agents.getAgent(agentId);
|
|
48
|
+
if (!parent)
|
|
49
|
+
throw new AgentError('not_found', `Agent not found: ${agentId}`);
|
|
50
|
+
const imported = importNativeConfig(parent.definition.driver.id, options.homeDir);
|
|
51
|
+
const importedAgents = imported.agents.map((native) => ensureImportedAgent(agents, parent, native));
|
|
52
|
+
const references = importedAgents.map(({ agent }, index) => ({
|
|
53
|
+
id: stableConfigId('subagent', agent.id), agentId: agent.id,
|
|
54
|
+
name: agent.definition.name, description: agent.definition.description, enabled: true,
|
|
55
|
+
source: imported.agents[index]?.source || { kind: 'native-import' },
|
|
56
|
+
}));
|
|
57
|
+
const config = mergedConfig(configFromAgent(parent), imported.config, references, options.replace === true);
|
|
58
|
+
return { config: updateAgentConfig(agents, agentId, config).config, importedAgents, sources: imported.sources };
|
|
59
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { parseSkillFrontmatter } from '../skills/scanner.js';
|
|
5
|
+
import { stableConfigId } from './schema.js';
|
|
6
|
+
function readText(file, maxBytes = 128 * 1024) {
|
|
7
|
+
try {
|
|
8
|
+
const stat = fs.statSync(file);
|
|
9
|
+
return stat.isFile() && stat.size <= maxBytes ? fs.readFileSync(file, 'utf8') : '';
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return '';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function readJson(file) {
|
|
16
|
+
try {
|
|
17
|
+
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
18
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function directories(root) {
|
|
25
|
+
try {
|
|
26
|
+
return fs.readdirSync(root, { withFileTypes: true })
|
|
27
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
|
|
28
|
+
.map((entry) => path.join(root, entry.name));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function markdownFiles(root) {
|
|
35
|
+
try {
|
|
36
|
+
return fs.readdirSync(root, { withFileTypes: true }).flatMap((entry) => {
|
|
37
|
+
const file = path.join(root, entry.name);
|
|
38
|
+
if (entry.isDirectory() && !entry.name.startsWith('.'))
|
|
39
|
+
return markdownFiles(file);
|
|
40
|
+
return entry.isFile() && entry.name.toLowerCase().endsWith('.md') ? [file] : [];
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function skillsFrom(root, provider) {
|
|
48
|
+
return directories(root).flatMap((directory) => {
|
|
49
|
+
const file = path.join(directory, 'SKILL.md');
|
|
50
|
+
const content = readText(file);
|
|
51
|
+
if (!content)
|
|
52
|
+
return [];
|
|
53
|
+
const parsed = parseSkillFrontmatter(content);
|
|
54
|
+
const name = parsed.name || path.basename(directory);
|
|
55
|
+
return [{
|
|
56
|
+
id: stableConfigId('skill', `${provider}-${name}`), name,
|
|
57
|
+
description: parsed.description || '', content, enabled: true,
|
|
58
|
+
source: { kind: 'native-import', provider, path: file },
|
|
59
|
+
}];
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function agentsFrom(root, provider) {
|
|
63
|
+
return markdownFiles(root).flatMap((file) => {
|
|
64
|
+
const content = readText(file);
|
|
65
|
+
if (!content)
|
|
66
|
+
return [];
|
|
67
|
+
const parsed = parseSkillFrontmatter(content);
|
|
68
|
+
const end = content.startsWith('---\n') ? content.indexOf('\n---', 4) : -1;
|
|
69
|
+
const body = end >= 0 ? content.slice(end + 4).replace(/^\r?\n/, '').trim() : content.trim();
|
|
70
|
+
return [{
|
|
71
|
+
name: parsed.name || path.basename(file, path.extname(file)),
|
|
72
|
+
description: parsed.description || '', driverId: provider, instructions: body,
|
|
73
|
+
source: { kind: 'native-import', provider, path: file },
|
|
74
|
+
}];
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function appendHook(hooks, provider, sourcePath, event, value, matcher = '') {
|
|
78
|
+
const command = typeof value.command === 'string' ? value.command.trim() : '';
|
|
79
|
+
if (!command)
|
|
80
|
+
return;
|
|
81
|
+
const timeout = Number(value.timeout);
|
|
82
|
+
const phases = {
|
|
83
|
+
UserPromptSubmit: 'userSubmit', UserSubmit: 'userSubmit', Stop: 'responseComplete',
|
|
84
|
+
ResponseComplete: 'responseComplete',
|
|
85
|
+
};
|
|
86
|
+
hooks.push({
|
|
87
|
+
id: `${provider}:${sourcePath}:${event}:${hooks.length}`,
|
|
88
|
+
name: command.split(/\s+/).slice(0, 3).join(' '), enabled: true,
|
|
89
|
+
event, phase: phases[event] || event, matcher, type: 'command', command,
|
|
90
|
+
...(Number.isFinite(timeout) && timeout > 0 ? { timeout } : {}),
|
|
91
|
+
...(typeof value.statusMessage === 'string' && value.statusMessage.trim()
|
|
92
|
+
? { statusMessage: value.statusMessage.trim() } : {}),
|
|
93
|
+
source: { kind: 'native-import', provider, path: sourcePath },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function extractHooks(value, hooks, provider, sourcePath, event, matcher = '') {
|
|
97
|
+
if (typeof value === 'string') {
|
|
98
|
+
appendHook(hooks, provider, sourcePath, event, { command: value }, matcher);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (Array.isArray(value)) {
|
|
102
|
+
value.forEach((item) => extractHooks(item, hooks, provider, sourcePath, event, matcher));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (!value || typeof value !== 'object')
|
|
106
|
+
return;
|
|
107
|
+
const object = value;
|
|
108
|
+
const nextEvent = typeof object.event === 'string' ? object.event : event;
|
|
109
|
+
const nextMatcher = typeof object.matcher === 'string' ? object.matcher : matcher;
|
|
110
|
+
appendHook(hooks, provider, sourcePath, nextEvent, object, nextMatcher);
|
|
111
|
+
if (Array.isArray(object.hooks)) {
|
|
112
|
+
extractHooks(object.hooks, hooks, provider, sourcePath, nextEvent, nextMatcher);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function hooksFrom(files, provider) {
|
|
116
|
+
const hooks = [];
|
|
117
|
+
for (const file of files) {
|
|
118
|
+
const data = readJson(file);
|
|
119
|
+
const raw = data?.hooks ?? data;
|
|
120
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
121
|
+
continue;
|
|
122
|
+
for (const [event, value] of Object.entries(raw)) {
|
|
123
|
+
extractHooks(value, hooks, provider, file, event);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return hooks;
|
|
127
|
+
}
|
|
128
|
+
// Engine references: agent-config/importers/* and agents/hooks.js. Only
|
|
129
|
+
// instructions, skills, command hooks, and agent markdown cross this boundary.
|
|
130
|
+
export function importNativeConfig(driverId, homeDir = os.homedir()) {
|
|
131
|
+
if (driverId === 'codex') {
|
|
132
|
+
const root = path.join(homeDir, '.codex');
|
|
133
|
+
const instructions = readText(path.join(root, 'AGENTS.md'));
|
|
134
|
+
const hooksFile = path.join(root, 'hooks.json');
|
|
135
|
+
const skills = skillsFrom(path.join(root, 'skills'), driverId);
|
|
136
|
+
const hooks = hooksFrom([hooksFile], driverId);
|
|
137
|
+
return {
|
|
138
|
+
config: { ...(instructions ? { instructions } : {}), ...(skills.length ? { skills } : {}),
|
|
139
|
+
...(hooks.length ? { hooks } : {}) },
|
|
140
|
+
agents: [], sources: [path.join(root, 'AGENTS.md'), path.join(root, 'skills'), hooksFile],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (driverId === 'claude_code') {
|
|
144
|
+
const root = path.join(homeDir, '.claude');
|
|
145
|
+
const settings = ['settings.json', 'settings.local.json'].map((name) => path.join(root, name));
|
|
146
|
+
const instructions = readText(path.join(root, 'CLAUDE.md')) || readText(path.join(homeDir, 'CLAUDE.md'));
|
|
147
|
+
const configRoot = path.join(homeDir, '.config', 'claude');
|
|
148
|
+
const skills = [...skillsFrom(path.join(root, 'skills'), driverId),
|
|
149
|
+
...skillsFrom(path.join(configRoot, 'skills'), driverId)];
|
|
150
|
+
const hooks = hooksFrom([...settings, path.join(homeDir, '.claude.json')], driverId);
|
|
151
|
+
const agents = [...agentsFrom(path.join(root, 'agents'), driverId),
|
|
152
|
+
...agentsFrom(path.join(configRoot, 'agents'), driverId)];
|
|
153
|
+
return {
|
|
154
|
+
config: { ...(instructions ? { instructions } : {}), ...(skills.length ? { skills } : {}),
|
|
155
|
+
...(hooks.length ? { hooks } : {}) },
|
|
156
|
+
agents,
|
|
157
|
+
sources: [path.join(root, 'CLAUDE.md'), path.join(root, 'skills'), path.join(root, 'agents'), ...settings],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
if (driverId === 'opencode') {
|
|
161
|
+
const config = path.join(homeDir, '.config', 'opencode');
|
|
162
|
+
const dot = path.join(homeDir, '.opencode');
|
|
163
|
+
const data = readJson(path.join(config, 'opencode.json')) || readJson(path.join(config, 'config.json'))
|
|
164
|
+
|| readJson(path.join(dot, 'opencode.json')) || {};
|
|
165
|
+
const instructions = ['instructions', 'systemPrompt', 'prompt']
|
|
166
|
+
.map((key) => data[key]).find((value) => typeof value === 'string');
|
|
167
|
+
const instructionText = instructions || readText(path.join(dot, 'AGENTS.md'));
|
|
168
|
+
const skills = [...skillsFrom(path.join(config, 'skills'), driverId),
|
|
169
|
+
...skillsFrom(path.join(dot, 'skills'), driverId)];
|
|
170
|
+
return {
|
|
171
|
+
config: { ...(instructionText ? { instructions: instructionText } : {}),
|
|
172
|
+
...(skills.length ? { skills } : {}) },
|
|
173
|
+
agents: [...agentsFrom(path.join(config, 'agents'), driverId), ...agentsFrom(path.join(dot, 'agents'), driverId)],
|
|
174
|
+
sources: [path.join(config, 'opencode.json'), path.join(config, 'skills'), path.join(config, 'agents'),
|
|
175
|
+
path.join(dot, 'AGENTS.md'), path.join(dot, 'skills'), path.join(dot, 'agents')],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return { config: {}, agents: [], sources: [] };
|
|
179
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { isObject } from '../json.js';
|
|
3
|
+
function text(value) {
|
|
4
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
5
|
+
}
|
|
6
|
+
function clone(value, fallback) {
|
|
7
|
+
return isObject(value) ? JSON.parse(JSON.stringify(value)) : fallback;
|
|
8
|
+
}
|
|
9
|
+
function uniqueStrings(value) {
|
|
10
|
+
if (!Array.isArray(value))
|
|
11
|
+
return [];
|
|
12
|
+
return [...new Set(value.map(text).filter(Boolean))];
|
|
13
|
+
}
|
|
14
|
+
export function stableConfigId(prefix, seed) {
|
|
15
|
+
const clean = text(seed).toLowerCase().replace(/[^a-z0-9._-]+/g, '-')
|
|
16
|
+
.replace(/^-+|-+$/g, '').slice(0, 80);
|
|
17
|
+
return clean ? `${prefix}-${clean}` : `${prefix}-${crypto.randomUUID()}`;
|
|
18
|
+
}
|
|
19
|
+
function skill(value, index) {
|
|
20
|
+
if (typeof value === 'string') {
|
|
21
|
+
const name = text(value);
|
|
22
|
+
return name ? {
|
|
23
|
+
id: stableConfigId('skill', name), name, description: '', content: '', enabled: true,
|
|
24
|
+
source: { kind: 'amalgm' },
|
|
25
|
+
} : null;
|
|
26
|
+
}
|
|
27
|
+
if (!isObject(value))
|
|
28
|
+
return null;
|
|
29
|
+
const content = typeof value.content === 'string' ? value.content : '';
|
|
30
|
+
const name = text(value.name || value.id) || `Skill ${index + 1}`;
|
|
31
|
+
if (!name && !content)
|
|
32
|
+
return null;
|
|
33
|
+
return {
|
|
34
|
+
id: text(value.id) || stableConfigId('skill', name || content.slice(0, 80)),
|
|
35
|
+
name, description: typeof value.description === 'string' ? value.description : '', content,
|
|
36
|
+
enabled: value.enabled !== false, source: clone(value.source, { kind: 'amalgm' }),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function skills(value) {
|
|
40
|
+
if (!Array.isArray(value))
|
|
41
|
+
return [];
|
|
42
|
+
const byId = new Map();
|
|
43
|
+
value.forEach((item, index) => {
|
|
44
|
+
const normalized = skill(item, index);
|
|
45
|
+
if (normalized)
|
|
46
|
+
byId.set(normalized.id, normalized);
|
|
47
|
+
});
|
|
48
|
+
return [...byId.values()];
|
|
49
|
+
}
|
|
50
|
+
function hook(value, index) {
|
|
51
|
+
if (!isObject(value))
|
|
52
|
+
return null;
|
|
53
|
+
const command = text(value.command);
|
|
54
|
+
const type = text(value.type) || (command ? 'command' : 'native');
|
|
55
|
+
if (type === 'command' && !command)
|
|
56
|
+
return null;
|
|
57
|
+
const event = text(value.event || value.phase) || 'UserPromptSubmit';
|
|
58
|
+
const name = text(value.name || value.label) || command.split(/\s+/).slice(0, 3).join(' ') || event;
|
|
59
|
+
const timeout = Number(value.timeout);
|
|
60
|
+
return {
|
|
61
|
+
id: text(value.id) || stableConfigId('hook', `${event}-${name}-${index}`),
|
|
62
|
+
name, enabled: value.enabled !== false, event, phase: text(value.phase) || event,
|
|
63
|
+
matcher: typeof value.matcher === 'string' ? value.matcher : '', type,
|
|
64
|
+
...(command ? { command } : {}),
|
|
65
|
+
...(Number.isFinite(timeout) && timeout > 0 ? { timeout } : {}),
|
|
66
|
+
...(text(value.statusMessage) ? { statusMessage: text(value.statusMessage) } : {}),
|
|
67
|
+
source: clone(value.source, {
|
|
68
|
+
kind: value.source === 'native' ? 'native-import' : 'amalgm',
|
|
69
|
+
...(text(value.harnessId) ? { harnessId: text(value.harnessId) } : {}),
|
|
70
|
+
...(text(value.sourcePath) ? { path: text(value.sourcePath) } : {}),
|
|
71
|
+
}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function hooks(value) {
|
|
75
|
+
if (!Array.isArray(value))
|
|
76
|
+
return [];
|
|
77
|
+
const byId = new Map();
|
|
78
|
+
value.forEach((item, index) => {
|
|
79
|
+
const normalized = hook(item, index);
|
|
80
|
+
if (normalized)
|
|
81
|
+
byId.set(normalized.id, normalized);
|
|
82
|
+
});
|
|
83
|
+
return [...byId.values()];
|
|
84
|
+
}
|
|
85
|
+
function subagent(value, index) {
|
|
86
|
+
const raw = typeof value === 'string' ? { agentId: value } : value;
|
|
87
|
+
if (!isObject(raw))
|
|
88
|
+
return null;
|
|
89
|
+
const agentId = text(raw.agentId || raw.agent_id || raw.id);
|
|
90
|
+
if (!agentId)
|
|
91
|
+
return null;
|
|
92
|
+
return {
|
|
93
|
+
id: text(raw.id) || stableConfigId('subagent', `${agentId}-${index}`), agentId,
|
|
94
|
+
name: text(raw.name), description: typeof raw.description === 'string' ? raw.description : '',
|
|
95
|
+
enabled: raw.enabled !== false, source: clone(raw.source, { kind: 'amalgm' }),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function subagents(value) {
|
|
99
|
+
if (!Array.isArray(value))
|
|
100
|
+
return [];
|
|
101
|
+
const byAgent = new Map();
|
|
102
|
+
value.forEach((item, index) => {
|
|
103
|
+
const normalized = subagent(item, index);
|
|
104
|
+
if (normalized)
|
|
105
|
+
byAgent.set(normalized.agentId, normalized);
|
|
106
|
+
});
|
|
107
|
+
return [...byAgent.values()];
|
|
108
|
+
}
|
|
109
|
+
function loadout(value, fallback) {
|
|
110
|
+
const raw = isObject(value) ? value : {};
|
|
111
|
+
const prior = isObject(fallback) ? fallback : {};
|
|
112
|
+
const aliases = (source) => source.toolIds || source.selectedToolIds
|
|
113
|
+
|| source.selected || source.tools;
|
|
114
|
+
const explicit = ['toolIds', 'selectedToolIds', 'selected', 'tools']
|
|
115
|
+
.some((key) => Object.hasOwn(raw, key));
|
|
116
|
+
return { toolIds: uniqueStrings(explicit ? aliases(raw) : aliases(prior)) };
|
|
117
|
+
}
|
|
118
|
+
// Engine reference: amalgm-mcp/agent-config/schema.js.
|
|
119
|
+
export function normalizeAgentConfig(input = {}, fallback = {}) {
|
|
120
|
+
const raw = isObject(input) ? input : {};
|
|
121
|
+
const prior = isObject(fallback) ? fallback : {};
|
|
122
|
+
const agentId = text(raw.agentId || raw.agent_id || prior.agentId);
|
|
123
|
+
const instructions = raw.instructions ?? raw.systemPrompt;
|
|
124
|
+
const instructionText = isObject(instructions) && typeof instructions.text === 'string'
|
|
125
|
+
? instructions.text : instructions;
|
|
126
|
+
return {
|
|
127
|
+
version: 1, ...(agentId ? { agentId } : {}), runtimeHomeLayout: 'per-agent',
|
|
128
|
+
instructions: typeof instructionText === 'string'
|
|
129
|
+
? instructionText : typeof prior.instructions === 'string' ? prior.instructions : '',
|
|
130
|
+
skills: skills(raw.skills ?? prior.skills ?? []),
|
|
131
|
+
loadout: loadout(raw.loadout ?? raw.toolset ?? raw.tools, prior.loadout ?? prior.tools),
|
|
132
|
+
hooks: hooks(raw.hooks ?? prior.hooks ?? []),
|
|
133
|
+
subagents: subagents(raw.subagents ?? prior.subagents ?? []),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Agents } from '../agents.js';
|
|
2
|
+
import type { AgentRecord } from '../types.js';
|
|
3
|
+
import type { AgentConfig, AgentConfigInput } from './types.js';
|
|
4
|
+
export declare function configFromAgent(agent: AgentRecord): AgentConfig;
|
|
5
|
+
export declare function listAgentConfigs(agents: Agents): AgentConfig[];
|
|
6
|
+
export declare function updateAgentConfig(agents: Agents, agentId: string, patch: AgentConfigInput): {
|
|
7
|
+
agent: AgentRecord;
|
|
8
|
+
config: AgentConfig;
|
|
9
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { AgentError } from '../errors.js';
|
|
2
|
+
import { isObject } from '../json.js';
|
|
3
|
+
import { normalizeAgentConfig } from './schema.js';
|
|
4
|
+
const METADATA_KEY = 'agentConfig';
|
|
5
|
+
function configDocument(agent) {
|
|
6
|
+
const value = agent.definition.metadata[METADATA_KEY];
|
|
7
|
+
return isObject(value) ? value : {};
|
|
8
|
+
}
|
|
9
|
+
function same(left, right) {
|
|
10
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
11
|
+
}
|
|
12
|
+
// Engine reference: amalgm-mcp/agent-config/store.js. The immutable Agent
|
|
13
|
+
// revision is the store here; no second agent_configs database is introduced.
|
|
14
|
+
export function configFromAgent(agent) {
|
|
15
|
+
const stored = normalizeAgentConfig(configDocument(agent), {
|
|
16
|
+
agentId: agent.id,
|
|
17
|
+
instructions: agent.definition.instructions,
|
|
18
|
+
skills: agent.definition.resources.skills,
|
|
19
|
+
loadout: { toolIds: agent.definition.toolbox.toolIds },
|
|
20
|
+
subagents: agent.definition.resources.subagents,
|
|
21
|
+
});
|
|
22
|
+
const skillRefs = stored.skills.filter((item) => item.enabled).map((item) => item.name || item.id);
|
|
23
|
+
const subagentRefs = stored.subagents.filter((item) => item.enabled).map((item) => item.agentId);
|
|
24
|
+
return normalizeAgentConfig({
|
|
25
|
+
...stored,
|
|
26
|
+
agentId: agent.id,
|
|
27
|
+
instructions: stored.instructions.trim() === agent.definition.instructions
|
|
28
|
+
? stored.instructions : agent.definition.instructions,
|
|
29
|
+
skills: same(skillRefs, agent.definition.resources.skills)
|
|
30
|
+
? stored.skills : agent.definition.resources.skills,
|
|
31
|
+
loadout: { toolIds: agent.definition.toolbox.toolIds },
|
|
32
|
+
subagents: same(subagentRefs, agent.definition.resources.subagents)
|
|
33
|
+
? stored.subagents : agent.definition.resources.subagents,
|
|
34
|
+
}, stored);
|
|
35
|
+
}
|
|
36
|
+
export function listAgentConfigs(agents) {
|
|
37
|
+
return agents.listAgents().map(configFromAgent);
|
|
38
|
+
}
|
|
39
|
+
export function updateAgentConfig(agents, agentId, patch) {
|
|
40
|
+
const current = agents.getAgent(agentId);
|
|
41
|
+
if (!current)
|
|
42
|
+
throw new AgentError('not_found', `Agent not found: ${agentId}`);
|
|
43
|
+
const existing = configFromAgent(current);
|
|
44
|
+
const config = normalizeAgentConfig({ ...existing, ...patch, agentId }, existing);
|
|
45
|
+
const metadata = { ...current.definition.metadata, [METADATA_KEY]: config };
|
|
46
|
+
const projected = {
|
|
47
|
+
instructions: config.instructions,
|
|
48
|
+
resources: {
|
|
49
|
+
skills: config.skills.filter((item) => item.enabled).map((item) => item.name || item.id),
|
|
50
|
+
subagents: config.subagents.filter((item) => item.enabled).map((item) => item.agentId),
|
|
51
|
+
},
|
|
52
|
+
toolbox: { toolIds: config.loadout.toolIds },
|
|
53
|
+
metadata,
|
|
54
|
+
};
|
|
55
|
+
return { agent: agents.updateAgent(agentId, projected), config };
|
|
56
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { JsonObject } from '../types.js';
|
|
2
|
+
export interface ConfigSkill {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
content: string;
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
source: JsonObject;
|
|
9
|
+
}
|
|
10
|
+
export interface ConfigHook {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
event: string;
|
|
15
|
+
phase: string;
|
|
16
|
+
matcher: string;
|
|
17
|
+
type: string;
|
|
18
|
+
command?: string;
|
|
19
|
+
timeout?: number;
|
|
20
|
+
statusMessage?: string;
|
|
21
|
+
source: JsonObject;
|
|
22
|
+
}
|
|
23
|
+
export interface ConfigSubagent {
|
|
24
|
+
id: string;
|
|
25
|
+
agentId: string;
|
|
26
|
+
name: string;
|
|
27
|
+
description: string;
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
source: JsonObject;
|
|
30
|
+
}
|
|
31
|
+
export interface AgentConfig {
|
|
32
|
+
version: 1;
|
|
33
|
+
agentId?: string;
|
|
34
|
+
runtimeHomeLayout: 'per-agent';
|
|
35
|
+
instructions: string;
|
|
36
|
+
skills: ConfigSkill[];
|
|
37
|
+
loadout: {
|
|
38
|
+
toolIds: string[];
|
|
39
|
+
};
|
|
40
|
+
hooks: ConfigHook[];
|
|
41
|
+
subagents: ConfigSubagent[];
|
|
42
|
+
}
|
|
43
|
+
export interface NativeAgent {
|
|
44
|
+
name: string;
|
|
45
|
+
description: string;
|
|
46
|
+
driverId: string;
|
|
47
|
+
instructions: string;
|
|
48
|
+
source: JsonObject;
|
|
49
|
+
}
|
|
50
|
+
export interface NativeConfigImport {
|
|
51
|
+
config: Partial<AgentConfig>;
|
|
52
|
+
agents: NativeAgent[];
|
|
53
|
+
sources: string[];
|
|
54
|
+
}
|
|
55
|
+
export type AgentConfigInput = AgentConfig | Record<string, unknown>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { isObject } from '../json.js';
|
|
2
|
+
import { importAgentConfig } from '../config/import.js';
|
|
3
|
+
import { configFromAgent, listAgentConfigs, updateAgentConfig } from '../config/store.js';
|
|
4
|
+
function agentId(body) {
|
|
5
|
+
return typeof body.agent_id === 'string' ? body.agent_id.trim() : '';
|
|
6
|
+
}
|
|
7
|
+
export async function routeAgentConfig(context) {
|
|
8
|
+
const [resource, action, child] = context.path;
|
|
9
|
+
if (resource !== 'agent-config')
|
|
10
|
+
return false;
|
|
11
|
+
if (!action && context.method === 'GET') {
|
|
12
|
+
context.json(200, { configs: listAgentConfigs(context.agents) });
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
if (child || context.method !== 'POST')
|
|
16
|
+
return false;
|
|
17
|
+
const body = await context.body();
|
|
18
|
+
const id = agentId(body);
|
|
19
|
+
if (!id) {
|
|
20
|
+
context.json(400, { error: 'agent_id is required' });
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
const agent = context.agents.getAgent(id);
|
|
24
|
+
if (!agent) {
|
|
25
|
+
context.json(404, { error: `Agent not found: ${id}` });
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
if (action === 'get') {
|
|
29
|
+
context.json(200, { config: configFromAgent(agent) });
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
if (action === 'update') {
|
|
33
|
+
const config = isObject(body.config) ? body.config : {};
|
|
34
|
+
context.json(200, { ok: true, config: updateAgentConfig(context.agents, id, config).config });
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (action === 'import-native') {
|
|
38
|
+
const result = importAgentConfig(context.agents, id, {
|
|
39
|
+
...(context.nativeHomeDir ? { homeDir: context.nativeHomeDir } : {}),
|
|
40
|
+
replace: body.replace === true,
|
|
41
|
+
});
|
|
42
|
+
context.json(200, { ok: true, ...result });
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
package/dist/http/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import { Agents } from '../agents.js';
|
|
3
3
|
import { asAgentError } from '../errors.js';
|
|
4
|
+
import { routeAgentConfig } from './config-routes.js';
|
|
4
5
|
import { routeAgents } from './agent-routes.js';
|
|
5
6
|
import { guardRequest, readJson, sendJson } from './request.js';
|
|
6
7
|
import { routeSessions } from './session-routes.js';
|
|
@@ -44,10 +45,11 @@ export function createRestServer(options = {}) {
|
|
|
44
45
|
path,
|
|
45
46
|
url,
|
|
46
47
|
skillRoots: options.skillRoots || {},
|
|
48
|
+
...(options.nativeHomeDir ? { nativeHomeDir: options.nativeHomeDir } : {}),
|
|
47
49
|
body: () => readJson(request, bodyLimit),
|
|
48
50
|
json: (status, value) => sendJson(response, status, value),
|
|
49
51
|
};
|
|
50
|
-
if (await routeSkills(context))
|
|
52
|
+
if (await routeAgentConfig(context) || await routeSkills(context))
|
|
51
53
|
return;
|
|
52
54
|
if (path.shift() !== 'v1') {
|
|
53
55
|
sendJson(response, 404, { error: { code: 'not_found', message: 'Route not found.' } });
|
package/dist/http-types.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface RestServerOptions extends AgentsOptions {
|
|
|
8
8
|
token?: string;
|
|
9
9
|
bodyLimitBytes?: number;
|
|
10
10
|
skillRoots?: SkillRootOptions;
|
|
11
|
+
nativeHomeDir?: string;
|
|
11
12
|
}
|
|
12
13
|
export interface RestServer {
|
|
13
14
|
agents: Agents;
|
|
@@ -21,6 +22,7 @@ export interface RouteContext {
|
|
|
21
22
|
path: string[];
|
|
22
23
|
url: URL;
|
|
23
24
|
skillRoots: SkillRootOptions;
|
|
25
|
+
nativeHomeDir?: string;
|
|
24
26
|
body(): Promise<Record<string, unknown>>;
|
|
25
27
|
json(status: number, value: unknown): void;
|
|
26
28
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,3 +8,8 @@ export { buildCanonicalSkillRoots, classifySkillRoot } from './skills/roots.js';
|
|
|
8
8
|
export type { SkillRootClassification, SkillRootOptions } from './skills/roots.js';
|
|
9
9
|
export { parseSkillFrontmatter, scanInstalledSkills } from './skills/scanner.js';
|
|
10
10
|
export type { InstalledSkill, SkillScanOptions } from './skills/scanner.js';
|
|
11
|
+
export { normalizeAgentConfig, stableConfigId } from './config/schema.js';
|
|
12
|
+
export { configFromAgent, listAgentConfigs, updateAgentConfig } from './config/store.js';
|
|
13
|
+
export { importAgentConfig } from './config/import.js';
|
|
14
|
+
export { importNativeConfig } from './config/native.js';
|
|
15
|
+
export type * from './config/types.js';
|
package/dist/index.js
CHANGED
|
@@ -5,3 +5,7 @@ export { definitionHash, normalizeDefinition, patchDefinition } from './definiti
|
|
|
5
5
|
export { messageText, normalizeMessage } from './messages.js';
|
|
6
6
|
export { buildCanonicalSkillRoots, classifySkillRoot } from './skills/roots.js';
|
|
7
7
|
export { parseSkillFrontmatter, scanInstalledSkills } from './skills/scanner.js';
|
|
8
|
+
export { normalizeAgentConfig, stableConfigId } from './config/schema.js';
|
|
9
|
+
export { configFromAgent, listAgentConfigs, updateAgentConfig } from './config/store.js';
|
|
10
|
+
export { importAgentConfig } from './config/import.js';
|
|
11
|
+
export { importNativeConfig } from './config/native.js';
|
package/dist/mcp/server.js
CHANGED
|
@@ -16,7 +16,7 @@ export function createMcpServer(options = {}) {
|
|
|
16
16
|
return {
|
|
17
17
|
protocolVersion: message.params?.protocolVersion || '2024-11-05',
|
|
18
18
|
capabilities: { tools: { listChanged: false } },
|
|
19
|
-
serverInfo: { name: 'amalgm-agents', version: '0.1.
|
|
19
|
+
serverInfo: { name: 'amalgm-agents', version: '0.1.2' },
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
if (message.method === 'ping')
|
package/docs/REST.md
CHANGED
|
@@ -10,6 +10,19 @@ token receives the kernel's canonical 401 from `@amalgm/core/transport`,
|
|
|
10
10
|
compared in constant time. `/healthz` is a minimal unauthenticated liveness
|
|
11
11
|
response. JSON bodies default to a 512 KB limit.
|
|
12
12
|
|
|
13
|
+
## Agent configuration
|
|
14
|
+
|
|
15
|
+
| Method | Path | Result |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `GET` | `/agent-config` | List normalized configs from live agent revisions |
|
|
18
|
+
| `POST` | `/agent-config/get` | Read `{ agent_id }` |
|
|
19
|
+
| `POST` | `/agent-config/update` | Atomically apply `{ agent_id, config }` |
|
|
20
|
+
| `POST` | `/agent-config/import-native` | Import safe native fields for `{ agent_id, replace? }` |
|
|
21
|
+
|
|
22
|
+
Set `nativeHomeDir` when embedding the server or `--native-home`/
|
|
23
|
+
`AMALGM_NATIVE_HOME` with the REST binary. Native import intentionally ignores
|
|
24
|
+
auth files, secrets, and MCP server configuration.
|
|
25
|
+
|
|
13
26
|
## Agents
|
|
14
27
|
|
|
15
28
|
| Method | Path | Result |
|