@amalgm/agents 0.1.0
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 +45 -0
- package/README.md +110 -0
- package/dist/agent-store.d.ts +17 -0
- package/dist/agent-store.js +111 -0
- package/dist/agents.d.ts +39 -0
- package/dist/agents.js +129 -0
- package/dist/bin/agents.d.ts +2 -0
- package/dist/bin/agents.js +5 -0
- package/dist/bin/fatal.d.ts +1 -0
- package/dist/bin/fatal.js +5 -0
- package/dist/bin/mcp.d.ts +2 -0
- package/dist/bin/mcp.js +19 -0
- package/dist/bin/rest.d.ts +2 -0
- package/dist/bin/rest.js +21 -0
- package/dist/cli/agent-commands.d.ts +3 -0
- package/dist/cli/agent-commands.js +23 -0
- package/dist/cli/args.d.ts +7 -0
- package/dist/cli/args.js +31 -0
- package/dist/cli/files.d.ts +2 -0
- package/dist/cli/files.js +21 -0
- package/dist/cli/help.d.ts +1 -0
- package/dist/cli/help.js +24 -0
- package/dist/cli/open.d.ts +3 -0
- package/dist/cli/open.js +12 -0
- package/dist/cli/run.d.ts +1 -0
- package/dist/cli/run.js +38 -0
- package/dist/cli/session-commands.d.ts +4 -0
- package/dist/cli/session-commands.js +54 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +2 -0
- package/dist/database.d.ts +9 -0
- package/dist/database.js +35 -0
- package/dist/definition.d.ts +4 -0
- package/dist/definition.js +93 -0
- package/dist/drivers.d.ts +4 -0
- package/dist/drivers.js +25 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.js +28 -0
- package/dist/event-store.d.ts +13 -0
- package/dist/event-store.js +50 -0
- package/dist/http/agent-routes.d.ts +2 -0
- package/dist/http/agent-routes.js +40 -0
- package/dist/http/request.d.ts +5 -0
- package/dist/http/request.js +40 -0
- package/dist/http/server.d.ts +2 -0
- package/dist/http/server.js +82 -0
- package/dist/http/session-routes.d.ts +2 -0
- package/dist/http/session-routes.js +67 -0
- package/dist/http/stream.d.ts +3 -0
- package/dist/http/stream.js +30 -0
- package/dist/http-types.d.ts +23 -0
- package/dist/http-types.js +1 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.js +1 -0
- package/dist/ids.d.ts +5 -0
- package/dist/ids.js +32 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/json.d.ts +7 -0
- package/dist/json.js +41 -0
- package/dist/mcp/agent-tools.d.ts +3 -0
- package/dist/mcp/agent-tools.js +51 -0
- package/dist/mcp/helpers.d.ts +5 -0
- package/dist/mcp/helpers.js +27 -0
- package/dist/mcp/server.d.ts +6 -0
- package/dist/mcp/server.js +73 -0
- package/dist/mcp/session-tools.d.ts +3 -0
- package/dist/mcp/session-tools.js +81 -0
- package/dist/mcp/tools.d.ts +3 -0
- package/dist/mcp/tools.js +5 -0
- package/dist/mcp/types.d.ts +18 -0
- package/dist/mcp/types.js +1 -0
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.js +2 -0
- package/dist/messages.d.ts +3 -0
- package/dist/messages.js +56 -0
- package/dist/rows.d.ts +7 -0
- package/dist/rows.js +65 -0
- package/dist/runtime.d.ts +29 -0
- package/dist/runtime.js +176 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +127 -0
- package/dist/session-store.d.ts +15 -0
- package/dist/session-store.js +83 -0
- package/dist/turn-store.d.ts +31 -0
- package/dist/turn-store.js +164 -0
- package/dist/types.d.ts +176 -0
- package/dist/types.js +1 -0
- package/docs/ARCHITECTURE.md +73 -0
- package/docs/CLI.md +61 -0
- package/docs/DATA_MODEL.md +56 -0
- package/docs/DEFINITIONS.md +65 -0
- package/docs/DRIVERS.md +72 -0
- package/docs/ENGINE_INTEGRATION.md +74 -0
- package/docs/MCP.md +24 -0
- package/docs/REST.md +78 -0
- package/docs/SDK.md +72 -0
- package/docs/SECURITY.md +38 -0
- package/examples/basic.ts +31 -0
- package/examples/reviewer.json +25 -0
- package/package.json +64 -0
- package/skills/amalgm-agents/SKILL.md +30 -0
- package/skills/amalgm-agents/agents/openai.yaml +4 -0
package/PURPOSE.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Amalgm Agents
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Amalgm Agents is the small, standalone product for defining agents and talking
|
|
6
|
+
to them. A user can create, inspect, edit, and delete an agent, then start or
|
|
7
|
+
continue a durable session with it through the SDK, CLI, REST, or MCP. Agents
|
|
8
|
+
are not chats: Chat may present a session, but the agent definition and session
|
|
9
|
+
remain usable without Chat or Amalgm Engine.
|
|
10
|
+
|
|
11
|
+
Engine composes Agents with harnesses, credentials, Toolbox, files, Realtime,
|
|
12
|
+
and UI. This package stores references to those products and delegates
|
|
13
|
+
execution to an injected driver; it does not duplicate their state.
|
|
14
|
+
|
|
15
|
+
## Primitives
|
|
16
|
+
|
|
17
|
+
- An **agent** is a stable identity with a complete current definition.
|
|
18
|
+
- A **session** is a durable conversation with one agent definition.
|
|
19
|
+
- A **turn** is one accepted input and its driver result.
|
|
20
|
+
- A **driver** runs the agent using a host-provided harness.
|
|
21
|
+
- The **Agents service** is the behavior shared by every public surface.
|
|
22
|
+
|
|
23
|
+
## Axioms
|
|
24
|
+
|
|
25
|
+
1. A user can create, list, inspect, edit, and delete agents.
|
|
26
|
+
2. A user or another agent can start, inspect, and continue an agent session
|
|
27
|
+
without creating or depending on a Chat conversation.
|
|
28
|
+
3. An agent definition can reference its harness, model, instructions, tools,
|
|
29
|
+
skills, files, credentials, and subagents without Agents owning those
|
|
30
|
+
products.
|
|
31
|
+
4. Any compatible agent harness can be used through the same driver contract.
|
|
32
|
+
5. Agent definitions and sessions survive restart, while uncertain interrupted
|
|
33
|
+
execution is never silently replayed.
|
|
34
|
+
6. SDK, CLI, REST, MCP, and skill workflows expose the same Agents behavior;
|
|
35
|
+
none is a second implementation.
|
|
36
|
+
7. An unchanged current definition is idempotent; every actual change creates
|
|
37
|
+
a new immutable revision, even when its content matches an earlier revision.
|
|
38
|
+
|
|
39
|
+
## Consequences
|
|
40
|
+
|
|
41
|
+
The package needs no cloud command inbox, scheduler, trigger relay, Chat
|
|
42
|
+
record, or general event-routing system. Engine and other hosts may call the
|
|
43
|
+
SDK whenever they need to route work, but routing is outside the Agents
|
|
44
|
+
product. Internal revisions, ordered events, idempotency, and cancellation
|
|
45
|
+
exist only to make the six behaviors above durable and predictable.
|
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Amalgm Agents
|
|
2
|
+
|
|
3
|
+
`@amalgm/agents` is a local-first TypeScript SDK for persistent agent
|
|
4
|
+
definitions and durable multi-turn sessions. The same service powers its CLI,
|
|
5
|
+
REST API, and MCP server.
|
|
6
|
+
|
|
7
|
+
It is intentionally not Chat. Chat may present an agent session, but Agents
|
|
8
|
+
owns the agent definition, the pinned execution revision, turns, and ordered
|
|
9
|
+
events.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @amalgm/agents
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Node 20 or newer is required.
|
|
18
|
+
|
|
19
|
+
## First agent
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { Agents, defineDriver } from '@amalgm/agents';
|
|
23
|
+
|
|
24
|
+
const echo = defineDriver({
|
|
25
|
+
id: 'echo',
|
|
26
|
+
async run(request) {
|
|
27
|
+
const text = request.input.parts
|
|
28
|
+
.filter((part) => part.type === 'text')
|
|
29
|
+
.map((part) => part.text)
|
|
30
|
+
.join('');
|
|
31
|
+
return {
|
|
32
|
+
message: {
|
|
33
|
+
role: 'assistant',
|
|
34
|
+
parts: [{ type: 'text', text: `Received: ${text}` }],
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const agents = new Agents({ stateDir: './state', drivers: [echo] });
|
|
41
|
+
|
|
42
|
+
agents.createAgent({
|
|
43
|
+
id: 'reviewer',
|
|
44
|
+
name: 'Reviewer',
|
|
45
|
+
driver: { id: 'echo' },
|
|
46
|
+
instructions: 'Review work carefully.',
|
|
47
|
+
toolbox: { toolIds: ['git'], actionIds: ['git.diff'] },
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const session = agents.startSession({ agentId: 'reviewer' });
|
|
51
|
+
const turn = await agents.send(session.id, {
|
|
52
|
+
message: 'Review the current diff.',
|
|
53
|
+
idempotencyKey: 'review-001',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
console.log(turn.status);
|
|
57
|
+
console.log(agents.listEvents(session.id));
|
|
58
|
+
await agents.close();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
An agent definition is normalized into a complete immutable revision. A
|
|
62
|
+
session stays pinned to that revision even after the agent is edited.
|
|
63
|
+
|
|
64
|
+
## Product surfaces
|
|
65
|
+
|
|
66
|
+
| Surface | Entry point | Purpose |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| SDK | `@amalgm/agents` | Embed the canonical service and supply drivers |
|
|
69
|
+
| CLI | `amalgm-agents` | Manage definitions and sessions |
|
|
70
|
+
| REST | `@amalgm/agents/http`, `amalgm-agents-rest` | Local HTTP and SSE integration |
|
|
71
|
+
| MCP | `@amalgm/agents/mcp`, `amalgm-agents-mcp` | Agent CRUD and agent-to-agent calls |
|
|
72
|
+
| Skill | `skills/amalgm-agents` | Minimal workflow over the Agents tools |
|
|
73
|
+
|
|
74
|
+
## CLI
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
amalgm-agents agent create ./agent.json
|
|
78
|
+
amalgm-agents agent list
|
|
79
|
+
amalgm-agents session start reviewer
|
|
80
|
+
amalgm-agents session send <session-id> "Review the diff"
|
|
81
|
+
amalgm-agents talk reviewer "Review the diff"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The CLI reads the same SQLite database as the SDK. Driver modules are loaded
|
|
85
|
+
with `--drivers ./dist/codex-driver.js` or `AMALGM_AGENT_DRIVERS`.
|
|
86
|
+
|
|
87
|
+
## Documentation
|
|
88
|
+
|
|
89
|
+
- [Purpose and axioms](./PURPOSE.md)
|
|
90
|
+
- [Architecture and boundaries](./docs/ARCHITECTURE.md)
|
|
91
|
+
- [Agent definitions](./docs/DEFINITIONS.md)
|
|
92
|
+
- [SDK reference](./docs/SDK.md)
|
|
93
|
+
- [Driver contract](./docs/DRIVERS.md)
|
|
94
|
+
- [CLI reference](./docs/CLI.md)
|
|
95
|
+
- [REST API](./docs/REST.md)
|
|
96
|
+
- [MCP server](./docs/MCP.md)
|
|
97
|
+
- [Persistence and recovery](./docs/DATA_MODEL.md)
|
|
98
|
+
- [Security model](./docs/SECURITY.md)
|
|
99
|
+
- [Engine integration](./docs/ENGINE_INTEGRATION.md)
|
|
100
|
+
|
|
101
|
+
## Development
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npm ci
|
|
105
|
+
npm run verify
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`verify` runs strict typechecking for source, tests, and examples; enforces the
|
|
109
|
+
220-line source-file limit; builds JavaScript and declarations; and runs the
|
|
110
|
+
real SQLite, process-restart, TCP/SSE, stdio MCP, and CLI tests.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
import type { AgentDefinitionInput, AgentPatch, AgentRecord, AgentRevision } from './types.js';
|
|
3
|
+
export declare class AgentStore {
|
|
4
|
+
private readonly database;
|
|
5
|
+
private readonly now;
|
|
6
|
+
constructor(database: Database.Database, now: () => string);
|
|
7
|
+
create(input: AgentDefinitionInput): AgentRecord;
|
|
8
|
+
apply(input: AgentDefinitionInput): AgentRecord;
|
|
9
|
+
update(id: string, patch: AgentPatch): AgentRecord;
|
|
10
|
+
delete(id: string): AgentRecord;
|
|
11
|
+
get(id: string, includeDeleted?: boolean): AgentRecord | null;
|
|
12
|
+
require(id: string, includeDeleted?: boolean): AgentRecord;
|
|
13
|
+
list(includeDeleted?: boolean): AgentRecord[];
|
|
14
|
+
revision(agentId: string, revisionId?: string): AgentRevision;
|
|
15
|
+
private rawAgent;
|
|
16
|
+
private insertRevision;
|
|
17
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { definitionHash, normalizeDefinition, patchDefinition } from './definition.js';
|
|
2
|
+
import { AgentError } from './errors.js';
|
|
3
|
+
import { newId, requireId } from './ids.js';
|
|
4
|
+
import { agentFromRow, revisionFromRow } from './rows.js';
|
|
5
|
+
const AGENT_SELECT = `
|
|
6
|
+
SELECT a.*, r.id AS revision_id, r.revision_number, r.definition_hash,
|
|
7
|
+
r.definition_json, r.created_at AS revision_created_at
|
|
8
|
+
FROM agents a
|
|
9
|
+
JOIN agent_revisions r ON r.id = a.current_revision_id
|
|
10
|
+
`;
|
|
11
|
+
export class AgentStore {
|
|
12
|
+
database;
|
|
13
|
+
now;
|
|
14
|
+
constructor(database, now) {
|
|
15
|
+
this.database = database;
|
|
16
|
+
this.now = now;
|
|
17
|
+
}
|
|
18
|
+
create(input) {
|
|
19
|
+
const definition = normalizeDefinition(input);
|
|
20
|
+
if (this.rawAgent(definition.id)) {
|
|
21
|
+
throw new AgentError('conflict', `Agent id already exists: ${definition.id}`);
|
|
22
|
+
}
|
|
23
|
+
const timestamp = this.now();
|
|
24
|
+
const revisionId = newId('revision');
|
|
25
|
+
this.database.transaction(() => {
|
|
26
|
+
this.database.prepare(`
|
|
27
|
+
INSERT INTO agents (id, created_at, updated_at) VALUES (?, ?, ?)
|
|
28
|
+
`).run(definition.id, timestamp, timestamp);
|
|
29
|
+
this.insertRevision(revisionId, definition.id, 1, definition, timestamp);
|
|
30
|
+
this.database.prepare(`
|
|
31
|
+
UPDATE agents SET current_revision_id = ?, current_revision_number = 1 WHERE id = ?
|
|
32
|
+
`).run(revisionId, definition.id);
|
|
33
|
+
})();
|
|
34
|
+
return this.require(definition.id);
|
|
35
|
+
}
|
|
36
|
+
apply(input) {
|
|
37
|
+
const definition = normalizeDefinition(input);
|
|
38
|
+
const existing = this.get(definition.id, true);
|
|
39
|
+
if (!existing)
|
|
40
|
+
return this.create(definition);
|
|
41
|
+
if (existing.deletedAt)
|
|
42
|
+
throw new AgentError('deleted', `Agent is deleted: ${definition.id}`);
|
|
43
|
+
const hash = definitionHash(definition);
|
|
44
|
+
const equal = this.database.prepare(`
|
|
45
|
+
SELECT 1 FROM agent_revisions WHERE id = ? AND definition_hash = ?
|
|
46
|
+
`).get(existing.currentRevisionId, hash);
|
|
47
|
+
if (equal)
|
|
48
|
+
return existing;
|
|
49
|
+
const timestamp = this.now();
|
|
50
|
+
const revisionId = newId('revision');
|
|
51
|
+
const number = existing.currentRevision + 1;
|
|
52
|
+
this.database.transaction(() => {
|
|
53
|
+
this.insertRevision(revisionId, existing.id, number, definition, timestamp);
|
|
54
|
+
this.database.prepare(`
|
|
55
|
+
UPDATE agents SET current_revision_id = ?, current_revision_number = ?, updated_at = ?
|
|
56
|
+
WHERE id = ? AND deleted_at IS NULL
|
|
57
|
+
`).run(revisionId, number, timestamp, existing.id);
|
|
58
|
+
})();
|
|
59
|
+
return this.require(existing.id);
|
|
60
|
+
}
|
|
61
|
+
update(id, patch) {
|
|
62
|
+
const existing = this.require(id);
|
|
63
|
+
return this.apply(patchDefinition(existing.definition, patch));
|
|
64
|
+
}
|
|
65
|
+
delete(id) {
|
|
66
|
+
const existing = this.require(id, true);
|
|
67
|
+
if (existing.deletedAt)
|
|
68
|
+
return existing;
|
|
69
|
+
const timestamp = this.now();
|
|
70
|
+
this.database.prepare(`UPDATE agents SET deleted_at = ?, updated_at = ? WHERE id = ?`)
|
|
71
|
+
.run(timestamp, timestamp, existing.id);
|
|
72
|
+
return this.require(existing.id, true);
|
|
73
|
+
}
|
|
74
|
+
get(id, includeDeleted = false) {
|
|
75
|
+
const clean = requireId(id, 'agent id');
|
|
76
|
+
const deleted = includeDeleted ? '' : ' AND a.deleted_at IS NULL';
|
|
77
|
+
const row = this.database.prepare(`${AGENT_SELECT} WHERE a.id = ?${deleted}`).get(clean);
|
|
78
|
+
return row ? agentFromRow(row) : null;
|
|
79
|
+
}
|
|
80
|
+
require(id, includeDeleted = false) {
|
|
81
|
+
const agent = this.get(id, includeDeleted);
|
|
82
|
+
if (!agent)
|
|
83
|
+
throw new AgentError('not_found', `Agent not found: ${id}`);
|
|
84
|
+
return agent;
|
|
85
|
+
}
|
|
86
|
+
list(includeDeleted = false) {
|
|
87
|
+
const where = includeDeleted ? '' : ' WHERE a.deleted_at IS NULL';
|
|
88
|
+
return this.database.prepare(`${AGENT_SELECT}${where} ORDER BY a.updated_at DESC`).all()
|
|
89
|
+
.map(agentFromRow);
|
|
90
|
+
}
|
|
91
|
+
revision(agentId, revisionId) {
|
|
92
|
+
const agent = this.require(agentId, true);
|
|
93
|
+
const id = revisionId || agent.currentRevisionId;
|
|
94
|
+
const row = this.database.prepare(`
|
|
95
|
+
SELECT * FROM agent_revisions WHERE id = ? AND agent_id = ?
|
|
96
|
+
`).get(id, agent.id);
|
|
97
|
+
if (!row)
|
|
98
|
+
throw new AgentError('not_found', `Agent revision not found: ${id}`);
|
|
99
|
+
return revisionFromRow(row);
|
|
100
|
+
}
|
|
101
|
+
rawAgent(id) {
|
|
102
|
+
return this.database.prepare('SELECT 1 FROM agents WHERE id = ?').get(id);
|
|
103
|
+
}
|
|
104
|
+
insertRevision(id, agentId, number, definition, timestamp) {
|
|
105
|
+
this.database.prepare(`
|
|
106
|
+
INSERT INTO agent_revisions
|
|
107
|
+
(id, agent_id, revision_number, definition_hash, definition_json, created_at)
|
|
108
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
109
|
+
`).run(id, agentId, number, definitionHash(definition), JSON.stringify(definition), timestamp);
|
|
110
|
+
}
|
|
111
|
+
}
|
package/dist/agents.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { AgentDefinitionInput, AgentDriver, AgentPatch, AgentRecord, AgentSession, AgentsOptions, AgentTurn, EnqueuedTurn, EventListener, JsonObject, SendInput, SessionEvent, StartSessionInput } from './types.js';
|
|
2
|
+
export declare class Agents {
|
|
3
|
+
readonly databasePath: string;
|
|
4
|
+
readonly recoveredTurns: number;
|
|
5
|
+
private readonly opened;
|
|
6
|
+
private readonly agentStore;
|
|
7
|
+
private readonly eventStore;
|
|
8
|
+
private readonly sessionStore;
|
|
9
|
+
private readonly turnStore;
|
|
10
|
+
private readonly runtime;
|
|
11
|
+
private readonly listeners;
|
|
12
|
+
private closed;
|
|
13
|
+
constructor(options?: AgentsOptions);
|
|
14
|
+
createAgent(input: AgentDefinitionInput): AgentRecord;
|
|
15
|
+
updateAgent(id: string, patch: AgentPatch): AgentRecord;
|
|
16
|
+
deleteAgent(id: string): AgentRecord;
|
|
17
|
+
getAgent(id: string, includeDeleted?: boolean): AgentRecord | null;
|
|
18
|
+
listAgents(includeDeleted?: boolean): AgentRecord[];
|
|
19
|
+
startSession(input: StartSessionInput): AgentSession;
|
|
20
|
+
getSession(id: string): AgentSession | null;
|
|
21
|
+
listSessions(agentId?: string, includeArchived?: boolean): AgentSession[];
|
|
22
|
+
archiveSession(id: string): AgentSession;
|
|
23
|
+
enqueue(id: string, input: SendInput): EnqueuedTurn;
|
|
24
|
+
send(id: string, input: SendInput): Promise<AgentTurn>;
|
|
25
|
+
talk(agentId: string, input: SendInput & {
|
|
26
|
+
sessionId?: string;
|
|
27
|
+
}): EnqueuedTurn & {
|
|
28
|
+
session: AgentSession;
|
|
29
|
+
};
|
|
30
|
+
cancelSession(id: string): AgentTurn;
|
|
31
|
+
listTurns(sessionId: string): AgentTurn[];
|
|
32
|
+
listEvents(sessionId: string, after?: number, limit?: number): SessionEvent[];
|
|
33
|
+
registerDriver(driver: AgentDriver): void;
|
|
34
|
+
driverIds(): string[];
|
|
35
|
+
subscribe(sessionId: string, listener: EventListener): () => void;
|
|
36
|
+
health(): JsonObject;
|
|
37
|
+
close(): Promise<void>;
|
|
38
|
+
private publish;
|
|
39
|
+
}
|
package/dist/agents.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { AgentStore } from './agent-store.js';
|
|
3
|
+
import { openDatabase } from './database.js';
|
|
4
|
+
import { EventStore } from './event-store.js';
|
|
5
|
+
import { AgentError } from './errors.js';
|
|
6
|
+
import { AgentRuntime } from './runtime.js';
|
|
7
|
+
import { SessionStore } from './session-store.js';
|
|
8
|
+
import { TurnStore } from './turn-store.js';
|
|
9
|
+
export class Agents {
|
|
10
|
+
databasePath;
|
|
11
|
+
recoveredTurns;
|
|
12
|
+
opened;
|
|
13
|
+
agentStore;
|
|
14
|
+
eventStore;
|
|
15
|
+
sessionStore;
|
|
16
|
+
turnStore;
|
|
17
|
+
runtime;
|
|
18
|
+
listeners = new Map();
|
|
19
|
+
closed = false;
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
this.opened = openDatabase(options);
|
|
22
|
+
this.databasePath = this.opened.databasePath;
|
|
23
|
+
const now = options.now || (() => new Date().toISOString());
|
|
24
|
+
this.eventStore = new EventStore(this.opened.database, now, (event) => this.publish(event));
|
|
25
|
+
this.agentStore = new AgentStore(this.opened.database, now);
|
|
26
|
+
this.sessionStore = new SessionStore(this.opened.database, this.eventStore, now);
|
|
27
|
+
this.turnStore = new TurnStore(this.opened.database, this.sessionStore, this.eventStore, now);
|
|
28
|
+
this.recoveredTurns = this.turnStore.recoverInterrupted();
|
|
29
|
+
this.runtime = new AgentRuntime(this.agentStore, this.sessionStore, this.turnStore, this.eventStore, {
|
|
30
|
+
maxInputBytes: options.maxInputBytes || 256_000,
|
|
31
|
+
maxEventBytes: options.maxEventBytes || 256_000,
|
|
32
|
+
turnTimeoutMs: options.turnTimeoutMs ?? 600_000,
|
|
33
|
+
}, options.drivers);
|
|
34
|
+
}
|
|
35
|
+
createAgent(input) {
|
|
36
|
+
return this.agentStore.create(input);
|
|
37
|
+
}
|
|
38
|
+
updateAgent(id, patch) {
|
|
39
|
+
return this.agentStore.update(id, patch);
|
|
40
|
+
}
|
|
41
|
+
deleteAgent(id) {
|
|
42
|
+
return this.agentStore.delete(id);
|
|
43
|
+
}
|
|
44
|
+
getAgent(id, includeDeleted = false) {
|
|
45
|
+
return this.agentStore.get(id, includeDeleted);
|
|
46
|
+
}
|
|
47
|
+
listAgents(includeDeleted = false) {
|
|
48
|
+
return this.agentStore.list(includeDeleted);
|
|
49
|
+
}
|
|
50
|
+
startSession(input) {
|
|
51
|
+
const agent = this.agentStore.require(input.agentId);
|
|
52
|
+
const revision = this.agentStore.revision(agent.id, input.revisionId);
|
|
53
|
+
return this.sessionStore.create(revision, input.sessionId, input.metadata || {});
|
|
54
|
+
}
|
|
55
|
+
getSession(id) {
|
|
56
|
+
return this.sessionStore.get(id);
|
|
57
|
+
}
|
|
58
|
+
listSessions(agentId, includeArchived = false) {
|
|
59
|
+
return this.sessionStore.list(agentId, includeArchived);
|
|
60
|
+
}
|
|
61
|
+
archiveSession(id) {
|
|
62
|
+
return this.sessionStore.archive(id, Boolean(this.turnStore.active(id)));
|
|
63
|
+
}
|
|
64
|
+
enqueue(id, input) {
|
|
65
|
+
this.sessionStore.require(id);
|
|
66
|
+
return this.runtime.enqueue(id, input);
|
|
67
|
+
}
|
|
68
|
+
send(id, input) {
|
|
69
|
+
this.sessionStore.require(id);
|
|
70
|
+
return this.runtime.send(id, input);
|
|
71
|
+
}
|
|
72
|
+
talk(agentId, input) {
|
|
73
|
+
const session = input.sessionId
|
|
74
|
+
? this.sessionStore.require(input.sessionId)
|
|
75
|
+
: this.startSession({ agentId });
|
|
76
|
+
if (session.agentId !== agentId) {
|
|
77
|
+
throw new AgentError('conflict', `Session ${session.id} belongs to agent ${session.agentId}.`);
|
|
78
|
+
}
|
|
79
|
+
return { session, ...this.enqueue(session.id, input) };
|
|
80
|
+
}
|
|
81
|
+
cancelSession(id) {
|
|
82
|
+
this.sessionStore.require(id);
|
|
83
|
+
return this.runtime.cancel(id);
|
|
84
|
+
}
|
|
85
|
+
listTurns(sessionId) {
|
|
86
|
+
return this.turnStore.list(sessionId);
|
|
87
|
+
}
|
|
88
|
+
listEvents(sessionId, after = 0, limit = 200) {
|
|
89
|
+
this.sessionStore.require(sessionId);
|
|
90
|
+
return this.eventStore.list(sessionId, after, limit);
|
|
91
|
+
}
|
|
92
|
+
registerDriver(driver) {
|
|
93
|
+
this.runtime.register(driver);
|
|
94
|
+
}
|
|
95
|
+
driverIds() {
|
|
96
|
+
return this.runtime.driverIds();
|
|
97
|
+
}
|
|
98
|
+
subscribe(sessionId, listener) {
|
|
99
|
+
this.sessionStore.require(sessionId);
|
|
100
|
+
const listeners = this.listeners.get(sessionId) || new Set();
|
|
101
|
+
listeners.add(listener);
|
|
102
|
+
this.listeners.set(sessionId, listeners);
|
|
103
|
+
return () => {
|
|
104
|
+
listeners.delete(listener);
|
|
105
|
+
if (listeners.size === 0)
|
|
106
|
+
this.listeners.delete(sessionId);
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
health() {
|
|
110
|
+
return {
|
|
111
|
+
ok: !this.closed,
|
|
112
|
+
database: path.basename(this.databasePath),
|
|
113
|
+
recoveredTurns: this.recoveredTurns,
|
|
114
|
+
drivers: this.driverIds(),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
async close() {
|
|
118
|
+
if (this.closed)
|
|
119
|
+
return;
|
|
120
|
+
this.closed = true;
|
|
121
|
+
await this.runtime.shutdown();
|
|
122
|
+
this.listeners.clear();
|
|
123
|
+
this.opened.close();
|
|
124
|
+
}
|
|
125
|
+
publish(event) {
|
|
126
|
+
for (const listener of this.listeners.get(event.sessionId) || [])
|
|
127
|
+
listener(event);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function fatal(error: unknown): void;
|
package/dist/bin/mcp.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from '../cli/args.js';
|
|
3
|
+
import { openAgents } from '../cli/open.js';
|
|
4
|
+
import { createMcpServer } from '../mcp/server.js';
|
|
5
|
+
import { fatal } from './fatal.js';
|
|
6
|
+
async function main() {
|
|
7
|
+
const args = parseArgs(process.argv.slice(2));
|
|
8
|
+
if (args.flags.has('help')) {
|
|
9
|
+
process.stdout.write('Usage: amalgm-agents-mcp [--state-dir <path>] [--drivers <a.js,b.js>]\n');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const agents = await openAgents(args);
|
|
13
|
+
const server = createMcpServer({ agents });
|
|
14
|
+
const stop = await server.start();
|
|
15
|
+
const close = () => void stop().then(() => agents.close()).catch(fatal);
|
|
16
|
+
process.once('SIGTERM', close);
|
|
17
|
+
process.once('SIGINT', close);
|
|
18
|
+
}
|
|
19
|
+
void main().catch(fatal);
|
package/dist/bin/rest.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs, flag } from '../cli/args.js';
|
|
3
|
+
import { openAgents } from '../cli/open.js';
|
|
4
|
+
import { createRestServer } from '../http/server.js';
|
|
5
|
+
import { fatal } from './fatal.js';
|
|
6
|
+
async function main() {
|
|
7
|
+
const args = parseArgs(process.argv.slice(2));
|
|
8
|
+
if (args.flags.has('help')) {
|
|
9
|
+
process.stdout.write('Usage: amalgm-agents-rest [--host 127.0.0.1] [--port 4317] [--token value]\n');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const agents = await openAgents(args);
|
|
13
|
+
const token = flag(args, 'token') || process.env.AMALGM_AGENTS_TOKEN;
|
|
14
|
+
const rest = createRestServer({ agents, ...(token ? { token } : {}) });
|
|
15
|
+
const address = await rest.listen(Number(flag(args, 'port') || 4317), flag(args, 'host') || '127.0.0.1');
|
|
16
|
+
process.stderr.write(`amalgm-agents REST listening on http://${address.address}:${address.port}\n`);
|
|
17
|
+
const close = () => void rest.close().then(() => agents.close()).catch(fatal);
|
|
18
|
+
process.once('SIGTERM', close);
|
|
19
|
+
process.once('SIGINT', close);
|
|
20
|
+
}
|
|
21
|
+
void main().catch(fatal);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AgentError } from '../errors.js';
|
|
2
|
+
import { enabled } from './args.js';
|
|
3
|
+
import { readObjectFile } from './files.js';
|
|
4
|
+
export function runAgentCommand(agents, args) {
|
|
5
|
+
const [, action, first, second] = args.words;
|
|
6
|
+
if (action === 'list')
|
|
7
|
+
return { agents: agents.listAgents(enabled(args, 'include-deleted')) };
|
|
8
|
+
if (action === 'show' && first) {
|
|
9
|
+
const agent = agents.getAgent(first, enabled(args, 'include-deleted'));
|
|
10
|
+
if (!agent)
|
|
11
|
+
throw new AgentError('not_found', `Agent not found: ${first}`);
|
|
12
|
+
return { agent };
|
|
13
|
+
}
|
|
14
|
+
if (action === 'create' && first) {
|
|
15
|
+
return { agent: agents.createAgent(readObjectFile(first)) };
|
|
16
|
+
}
|
|
17
|
+
if (action === 'update' && first && second) {
|
|
18
|
+
return { agent: agents.updateAgent(first, readObjectFile(second)) };
|
|
19
|
+
}
|
|
20
|
+
if (action === 'delete' && first)
|
|
21
|
+
return { agent: agents.deleteAgent(first) };
|
|
22
|
+
throw new AgentError('invalid_input', 'Unknown or incomplete agent command. Run with --help.');
|
|
23
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface ParsedArgs {
|
|
2
|
+
words: string[];
|
|
3
|
+
flags: Map<string, string | boolean>;
|
|
4
|
+
}
|
|
5
|
+
export declare function parseArgs(argv: string[]): ParsedArgs;
|
|
6
|
+
export declare function flag(args: ParsedArgs, name: string): string | undefined;
|
|
7
|
+
export declare function enabled(args: ParsedArgs, name: string): boolean;
|
package/dist/cli/args.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function parseArgs(argv) {
|
|
2
|
+
const words = [];
|
|
3
|
+
const flags = new Map();
|
|
4
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
5
|
+
const item = argv[index];
|
|
6
|
+
if (!item?.startsWith('--')) {
|
|
7
|
+
if (item !== undefined)
|
|
8
|
+
words.push(item);
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
const [rawName, inline] = item.slice(2).split('=', 2);
|
|
12
|
+
const next = argv[index + 1];
|
|
13
|
+
if (inline !== undefined)
|
|
14
|
+
flags.set(rawName || '', inline);
|
|
15
|
+
else if (next !== undefined && !next.startsWith('--')) {
|
|
16
|
+
flags.set(rawName || '', next);
|
|
17
|
+
index += 1;
|
|
18
|
+
}
|
|
19
|
+
else
|
|
20
|
+
flags.set(rawName || '', true);
|
|
21
|
+
}
|
|
22
|
+
return { words, flags };
|
|
23
|
+
}
|
|
24
|
+
export function flag(args, name) {
|
|
25
|
+
const value = args.flags.get(name);
|
|
26
|
+
return typeof value === 'string' ? value : undefined;
|
|
27
|
+
}
|
|
28
|
+
export function enabled(args, name) {
|
|
29
|
+
const value = args.flags.get(name);
|
|
30
|
+
return value === true || value === 'true';
|
|
31
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { AgentError } from '../errors.js';
|
|
4
|
+
export function readObjectFile(file) {
|
|
5
|
+
const target = path.resolve(file);
|
|
6
|
+
let value;
|
|
7
|
+
try {
|
|
8
|
+
value = JSON.parse(fs.readFileSync(target, 'utf8'));
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
12
|
+
throw new AgentError('invalid_input', `Cannot read JSON file ${target}: ${message}`);
|
|
13
|
+
}
|
|
14
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
15
|
+
throw new AgentError('invalid_input', `JSON file must contain an object: ${target}`);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
export function writeJson(value) {
|
|
20
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const HELP = "amalgm-agents \u2014 local agent definitions and durable sessions\n\nAgent definitions:\n amalgm-agents agent list [--include-deleted]\n amalgm-agents agent show <id>\n amalgm-agents agent create <definition.json>\n amalgm-agents agent update <id> <patch.json>\n amalgm-agents agent delete <id>\n\nSessions:\n amalgm-agents session list [--agent <id>] [--include-archived]\n amalgm-agents session start <agent-id> [--id <id>] [--revision <id>]\n amalgm-agents session show <id>\n amalgm-agents session events <id> [--after <sequence>]\n amalgm-agents session send <id> <message> [--key <idempotency-key>]\n amalgm-agents session cancel <id>\n amalgm-agents session archive <id>\n amalgm-agents talk <agent-id> <message> [--session <id>] [--background]\n\nGlobal options:\n --state-dir <path> Agents state directory\n --drivers <a.js,b.js> Driver modules (or AMALGM_AGENT_DRIVERS)\n --help Show this help\n";
|
package/dist/cli/help.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const HELP = `amalgm-agents — local agent definitions and durable sessions
|
|
2
|
+
|
|
3
|
+
Agent definitions:
|
|
4
|
+
amalgm-agents agent list [--include-deleted]
|
|
5
|
+
amalgm-agents agent show <id>
|
|
6
|
+
amalgm-agents agent create <definition.json>
|
|
7
|
+
amalgm-agents agent update <id> <patch.json>
|
|
8
|
+
amalgm-agents agent delete <id>
|
|
9
|
+
|
|
10
|
+
Sessions:
|
|
11
|
+
amalgm-agents session list [--agent <id>] [--include-archived]
|
|
12
|
+
amalgm-agents session start <agent-id> [--id <id>] [--revision <id>]
|
|
13
|
+
amalgm-agents session show <id>
|
|
14
|
+
amalgm-agents session events <id> [--after <sequence>]
|
|
15
|
+
amalgm-agents session send <id> <message> [--key <idempotency-key>]
|
|
16
|
+
amalgm-agents session cancel <id>
|
|
17
|
+
amalgm-agents session archive <id>
|
|
18
|
+
amalgm-agents talk <agent-id> <message> [--session <id>] [--background]
|
|
19
|
+
|
|
20
|
+
Global options:
|
|
21
|
+
--state-dir <path> Agents state directory
|
|
22
|
+
--drivers <a.js,b.js> Driver modules (or AMALGM_AGENT_DRIVERS)
|
|
23
|
+
--help Show this help
|
|
24
|
+
`;
|
package/dist/cli/open.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Agents } from '../agents.js';
|
|
2
|
+
import { driverSpecifiers, loadDriverModules } from '../drivers.js';
|
|
3
|
+
import { flag } from './args.js';
|
|
4
|
+
export async function openAgents(args) {
|
|
5
|
+
const modules = flag(args, 'drivers');
|
|
6
|
+
const stateDir = flag(args, 'state-dir');
|
|
7
|
+
const drivers = await loadDriverModules(driverSpecifiers(modules));
|
|
8
|
+
return new Agents({
|
|
9
|
+
drivers,
|
|
10
|
+
...(stateDir ? { stateDir } : {}),
|
|
11
|
+
});
|
|
12
|
+
}
|