@amalgm/agents 0.2.0 → 0.2.1

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.
Files changed (55) hide show
  1. package/PURPOSE.md +27 -25
  2. package/README.md +46 -19
  3. package/dist/agents.d.ts +26 -4
  4. package/dist/agents.js +87 -4
  5. package/dist/bin/mcp.js +1 -1
  6. package/dist/cli/help.d.ts +1 -1
  7. package/dist/cli/help.js +13 -2
  8. package/dist/cli/open.d.ts +1 -1
  9. package/dist/cli/open.js +5 -1
  10. package/dist/cli/run.js +8 -3
  11. package/dist/cli/session-commands.d.ts +4 -0
  12. package/dist/cli/session-commands.js +54 -0
  13. package/dist/drivers.d.ts +4 -0
  14. package/dist/drivers.js +25 -0
  15. package/dist/errors.d.ts +1 -1
  16. package/dist/errors.js +2 -0
  17. package/dist/event-store.d.ts +13 -0
  18. package/dist/event-store.js +50 -0
  19. package/dist/http/server.js +7 -1
  20. package/dist/http/session-routes.d.ts +2 -0
  21. package/dist/http/session-routes.js +67 -0
  22. package/dist/http/stream.d.ts +3 -0
  23. package/dist/http/stream.js +30 -0
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.js +2 -0
  26. package/dist/mcp/agent-tools.js +1 -1
  27. package/dist/mcp/session-tools.d.ts +3 -0
  28. package/dist/mcp/session-tools.js +81 -0
  29. package/dist/mcp/tools.js +2 -1
  30. package/dist/messages.d.ts +3 -0
  31. package/dist/messages.js +56 -0
  32. package/dist/rows.d.ts +4 -1
  33. package/dist/rows.js +39 -0
  34. package/dist/runtime.d.ts +29 -0
  35. package/dist/runtime.js +176 -0
  36. package/dist/schema.js +101 -53
  37. package/dist/session-store.d.ts +15 -0
  38. package/dist/session-store.js +83 -0
  39. package/dist/turn-store.d.ts +31 -0
  40. package/dist/turn-store.js +164 -0
  41. package/dist/types.d.ts +106 -0
  42. package/docs/ARCHITECTURE.md +33 -21
  43. package/docs/CLI.md +43 -3
  44. package/docs/DATA_MODEL.md +46 -7
  45. package/docs/DEFINITIONS.md +4 -4
  46. package/docs/DRIVERS.md +72 -0
  47. package/docs/ENGINE_INTEGRATION.md +67 -22
  48. package/docs/MCP.md +19 -3
  49. package/docs/REST.md +97 -14
  50. package/docs/SDK.md +61 -12
  51. package/docs/SECURITY.md +35 -6
  52. package/examples/basic.ts +22 -8
  53. package/package.json +5 -5
  54. package/skills/amalgm-agents/SKILL.md +12 -3
  55. package/skills/amalgm-agents/agents/openai.yaml +2 -2
package/PURPOSE.md CHANGED
@@ -2,14 +2,15 @@
2
2
 
3
3
  ## Purpose
4
4
 
5
- Amalgm Agents is the small, standalone registry for agent identities,
6
- installations, and immutable revisions. A user or another agent can create,
7
- register, install, list, inspect, edit, and delete an available agent, then
8
- resolve one exact revision for execution by Chat.
9
-
10
- Shell composes Agents with Chat, harnesses, credentials, Toolbox, files, Live,
11
- and UI. This package stores references to those products; it does not duplicate
12
- their state or execute a conversation.
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.
13
14
  It also discovers installed agent skills across the canonical project and
14
15
  harness roots so every transport presents the same available skill catalog.
15
16
  Rich harness configuration is stored inside the same immutable definition and
@@ -20,22 +21,25 @@ hosts supply explicit ports for resources owned by other products.
20
21
  ## Primitives
21
22
 
22
23
  - An **agent** is a stable identity with a complete current definition.
23
- - An **installation** is one locally available agent identity and its Agent
24
- Home descriptor.
25
- - A **resolved agent** is one exact immutable revision selected for execution.
24
+ - A **session** is a durable conversation with one agent definition.
25
+ - A **turn** is one accepted input and its driver result.
26
+ - A **driver** runs the agent using a host-provided harness.
26
27
  - The **Agents service** is the behavior shared by every public surface.
27
28
 
28
29
  ## Axioms
29
30
 
31
+ Managed releases bind to one exact published Core version and build from the
32
+ locked dependency graph on the supported Node toolchain.
33
+
30
34
  1. A user can create, list, inspect, edit, and delete agents.
31
- 2. A user or another agent can resolve an available agent to one exact
32
- immutable revision without starting a conversation.
35
+ 2. A user or another agent can start, inspect, and continue an agent session
36
+ without creating or depending on a Chat conversation.
33
37
  3. An agent definition can reference its harness, model, instructions, tools,
34
38
  skills, files, credentials, and subagents without Agents owning those
35
39
  products.
36
- 4. Harness identity is part of an agent revision; provider execution belongs
37
- to Chat behind a host runtime port.
38
- 5. Agent definitions, installations, and revisions survive restart.
40
+ 4. Any compatible agent harness can be used through the same driver contract.
41
+ 5. Agent definitions and sessions survive restart, while uncertain interrupted
42
+ execution is never silently replayed.
39
43
  6. SDK, CLI, REST, MCP, and skill workflows expose the same Agents behavior;
40
44
  none is a second implementation.
41
45
  7. An unchanged current definition is idempotent; every actual change creates
@@ -47,12 +51,10 @@ hosts supply explicit ports for resources owned by other products.
47
51
 
48
52
  ## Consequences
49
53
 
50
- The package needs no conversation, turn, transcript, stream, reconnect,
51
- interrupt, cloud command inbox, scheduler, trigger relay, or general
52
- event-routing system. Shell and other hosts resolve Agents revisions while
53
- preparing a Chat execution. Bundle ports call Apps, Automations, and Toolbox
54
- public APIs; Agents never imports their stores or duplicates their lifecycle
55
- behavior.
56
-
57
- There is no execution subsystem in Agents. Chat is the single owner of every
58
- conversation and turn.
54
+ The package needs no cloud command inbox, scheduler, trigger relay, Chat
55
+ record, or general event-routing system. Engine and other hosts may call the
56
+ SDK whenever they need to route work, but routing is outside the Agents
57
+ product. Bundle ports call Apps, Automations, and Toolbox public APIs; Agents
58
+ never imports their stores or duplicates their lifecycle behavior. Internal
59
+ revisions, ordered events, idempotency, and cancellation exist only to make
60
+ the behaviors above durable and predictable.
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # Amalgm Agents
2
2
 
3
3
  `@amalgm/agents` is a local-first TypeScript SDK for persistent agent
4
- identities, installations, and immutable revisions. The same registry powers
5
- its CLI, REST API, and MCP server.
4
+ definitions and durable multi-turn sessions. The same service powers its CLI,
5
+ REST API, and MCP server.
6
6
 
7
- It is intentionally not Chat. Agents resolves the exact installed revision;
8
- `@amalgm/chat` owns the durable conversation, turns, stream, reconnect, and
9
- interrupt lifecycle that executes it.
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
10
 
11
11
  ## Install
12
12
 
@@ -19,9 +19,25 @@ Node 20 or newer is required.
19
19
  ## First agent
20
20
 
21
21
  ```ts
22
- import { Agents } from '@amalgm/agents';
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
+ });
23
39
 
24
- const agents = new Agents({ stateDir: './state' });
40
+ const agents = new Agents({ stateDir: './state', drivers: [echo] });
25
41
 
26
42
  agents.createAgent({
27
43
  id: 'reviewer',
@@ -31,22 +47,28 @@ agents.createAgent({
31
47
  toolbox: { toolIds: ['git'], actionIds: ['git.diff'] },
32
48
  });
33
49
 
34
- const revision = agents.getAgentRevision('reviewer');
35
- console.log(revision.id, revision.definition.driver.id);
36
- agents.close();
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();
37
59
  ```
38
60
 
39
- An agent definition is normalized into a complete immutable revision. Chat
40
- records the resolved revision id in every prepared execution.
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.
41
63
 
42
64
  ## Product surfaces
43
65
 
44
66
  | Surface | Entry point | Purpose |
45
67
  |---|---|---|
46
- | SDK | `@amalgm/agents` | Embed the canonical registry and resolve revisions |
47
- | CLI | `amalgm-agents` | Manage agent identities and installations |
48
- | REST | `@amalgm/agents/http`, `amalgm-agents-rest` | Local registry integration |
49
- | MCP | `@amalgm/agents/mcp`, `amalgm-agents-mcp` | Agent registry tools |
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 |
50
72
  | Skill | `skills/amalgm-agents` | Minimal workflow over the Agents tools |
51
73
 
52
74
  Installed skills are available from the REST surface without introducing a
@@ -100,9 +122,13 @@ explicitly; dependencies are never omitted to make a bundle look portable.
100
122
  ```bash
101
123
  amalgm-agents agent create ./agent.json
102
124
  amalgm-agents agent list
125
+ amalgm-agents session start reviewer
126
+ amalgm-agents session send <session-id> "Review the diff"
127
+ amalgm-agents talk reviewer "Review the diff"
103
128
  ```
104
129
 
105
- The CLI reads the same SQLite registry as the SDK.
130
+ The CLI reads the same SQLite database as the SDK. Driver modules are loaded
131
+ with `--drivers ./dist/codex-driver.js` or `AMALGM_AGENT_DRIVERS`.
106
132
 
107
133
  ## Documentation
108
134
 
@@ -110,10 +136,11 @@ The CLI reads the same SQLite registry as the SDK.
110
136
  - [Architecture and boundaries](./docs/ARCHITECTURE.md)
111
137
  - [Agent definitions](./docs/DEFINITIONS.md)
112
138
  - [SDK reference](./docs/SDK.md)
139
+ - [Driver contract](./docs/DRIVERS.md)
113
140
  - [CLI reference](./docs/CLI.md)
114
141
  - [REST API](./docs/REST.md)
115
142
  - [MCP server](./docs/MCP.md)
116
- - [Persistence](./docs/DATA_MODEL.md)
143
+ - [Persistence and recovery](./docs/DATA_MODEL.md)
117
144
  - [Security model](./docs/SECURITY.md)
118
145
  - [Engine integration](./docs/ENGINE_INTEGRATION.md)
119
146
 
@@ -126,4 +153,4 @@ npm run verify
126
153
 
127
154
  `verify` runs strict typechecking for source, tests, and examples; enforces the
128
155
  220-line source-file limit; builds JavaScript and declarations; and runs the
129
- real SQLite, TCP, stdio MCP, and CLI tests.
156
+ real SQLite, process-restart, TCP/SSE, stdio MCP, and CLI tests.
package/dist/agents.d.ts CHANGED
@@ -1,8 +1,14 @@
1
- import type { AgentDefinitionInput, AgentPatch, AgentRecord, AgentRevision, AgentsOptions, JsonObject } from './types.js';
1
+ import type { AgentDefinitionInput, AgentDriver, AgentPatch, AgentRecord, AgentSession, AgentsOptions, AgentTurn, EnqueuedTurn, EventListener, JsonObject, SendInput, SessionEvent, StartSessionInput } from './types.js';
2
2
  export declare class Agents {
3
3
  readonly databasePath: string;
4
+ readonly recoveredTurns: number;
4
5
  private readonly opened;
5
6
  private readonly agentStore;
7
+ private readonly eventStore;
8
+ private readonly sessionStore;
9
+ private readonly turnStore;
10
+ private readonly runtime;
11
+ private readonly listeners;
6
12
  private closed;
7
13
  constructor(options?: AgentsOptions);
8
14
  createAgent(input: AgentDefinitionInput): AgentRecord;
@@ -10,8 +16,24 @@ export declare class Agents {
10
16
  deleteAgent(id: string): AgentRecord;
11
17
  getAgent(id: string, includeDeleted?: boolean): AgentRecord | null;
12
18
  listAgents(includeDeleted?: boolean): AgentRecord[];
13
- /** Resolve one exact immutable revision for a host such as Chat. */
14
- getAgentRevision(agentId: string, revisionId?: string): AgentRevision;
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;
15
36
  health(): JsonObject;
16
- close(): void;
37
+ close(): Promise<void>;
38
+ private publish;
17
39
  }
package/dist/agents.js CHANGED
@@ -1,16 +1,36 @@
1
1
  import path from 'node:path';
2
2
  import { AgentStore } from './agent-store.js';
3
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';
4
9
  export class Agents {
5
10
  databasePath;
11
+ recoveredTurns;
6
12
  opened;
7
13
  agentStore;
14
+ eventStore;
15
+ sessionStore;
16
+ turnStore;
17
+ runtime;
18
+ listeners = new Map();
8
19
  closed = false;
9
20
  constructor(options = {}) {
10
21
  this.opened = openDatabase(options);
11
22
  this.databasePath = this.opened.databasePath;
12
23
  const now = options.now || (() => new Date().toISOString());
24
+ this.eventStore = new EventStore(this.opened.database, now, (event) => this.publish(event));
13
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);
14
34
  }
15
35
  createAgent(input) {
16
36
  return this.agentStore.create(input);
@@ -27,20 +47,83 @@ export class Agents {
27
47
  listAgents(includeDeleted = false) {
28
48
  return this.agentStore.list(includeDeleted);
29
49
  }
30
- /** Resolve one exact immutable revision for a host such as Chat. */
31
- getAgentRevision(agentId, revisionId) {
32
- return this.agentStore.revision(agentId, revisionId);
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
+ };
33
108
  }
34
109
  health() {
35
110
  return {
36
111
  ok: !this.closed,
37
112
  database: path.basename(this.databasePath),
113
+ recoveredTurns: this.recoveredTurns,
114
+ drivers: this.driverIds(),
38
115
  };
39
116
  }
40
- close() {
117
+ async close() {
41
118
  if (this.closed)
42
119
  return;
43
120
  this.closed = true;
121
+ await this.runtime.shutdown();
122
+ this.listeners.clear();
44
123
  this.opened.close();
45
124
  }
125
+ publish(event) {
126
+ for (const listener of this.listeners.get(event.sessionId) || [])
127
+ listener(event);
128
+ }
46
129
  }
package/dist/bin/mcp.js CHANGED
@@ -6,7 +6,7 @@ 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-mcp [--state-dir <path>]\n');
9
+ process.stdout.write('Usage: amalgm-agents-mcp [--state-dir <path>] [--drivers <a.js,b.js>]\n');
10
10
  return;
11
11
  }
12
12
  const agents = await openAgents(args);
@@ -1 +1 @@
1
- export declare const HELP = "amalgm-agents \u2014 local agent identities and immutable revisions\n\nAgents:\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\nGlobal options:\n --state-dir <path> Agents state directory\n --help Show this help\n";
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 CHANGED
@@ -1,13 +1,24 @@
1
- export const HELP = `amalgm-agents — local agent identities and immutable revisions
1
+ export const HELP = `amalgm-agents — local agent definitions and durable sessions
2
2
 
3
- Agents:
3
+ Agent definitions:
4
4
  amalgm-agents agent list [--include-deleted]
5
5
  amalgm-agents agent show <id>
6
6
  amalgm-agents agent create <definition.json>
7
7
  amalgm-agents agent update <id> <patch.json>
8
8
  amalgm-agents agent delete <id>
9
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
+
10
20
  Global options:
11
21
  --state-dir <path> Agents state directory
22
+ --drivers <a.js,b.js> Driver modules (or AMALGM_AGENT_DRIVERS)
12
23
  --help Show this help
13
24
  `;
@@ -1,3 +1,3 @@
1
1
  import { Agents } from '../agents.js';
2
2
  import type { ParsedArgs } from './args.js';
3
- export declare function openAgents(args: ParsedArgs): Agents;
3
+ export declare function openAgents(args: ParsedArgs): Promise<Agents>;
package/dist/cli/open.js CHANGED
@@ -1,8 +1,12 @@
1
1
  import { Agents } from '../agents.js';
2
+ import { driverSpecifiers, loadDriverModules } from '../drivers.js';
2
3
  import { flag } from './args.js';
3
- export function openAgents(args) {
4
+ export async function openAgents(args) {
5
+ const modules = flag(args, 'drivers');
4
6
  const stateDir = flag(args, 'state-dir');
7
+ const drivers = await loadDriverModules(driverSpecifiers(modules));
5
8
  return new Agents({
9
+ drivers,
6
10
  ...(stateDir ? { stateDir } : {}),
7
11
  });
8
12
  }
package/dist/cli/run.js CHANGED
@@ -4,6 +4,7 @@ import { runAgentCommand } from './agent-commands.js';
4
4
  import { writeJson } from './files.js';
5
5
  import { HELP } from './help.js';
6
6
  import { openAgents } from './open.js';
7
+ import { runSessionCommand, runTalkCommand } from './session-commands.js';
7
8
  export async function runCli(argv = process.argv.slice(2)) {
8
9
  const args = parseArgs(argv);
9
10
  if (args.flags.has('help') || args.words.length === 0) {
@@ -12,11 +13,15 @@ export async function runCli(argv = process.argv.slice(2)) {
12
13
  }
13
14
  let agents;
14
15
  try {
15
- agents = openAgents(args);
16
+ agents = await openAgents(args);
16
17
  const command = args.words[0];
17
18
  const value = command === 'agent'
18
19
  ? runAgentCommand(agents, args)
19
- : undefined;
20
+ : command === 'session'
21
+ ? await runSessionCommand(agents, args)
22
+ : command === 'talk'
23
+ ? await runTalkCommand(agents, args)
24
+ : undefined;
20
25
  if (value === undefined)
21
26
  throw new Error(`Unknown command: ${String(command)}`);
22
27
  writeJson(value);
@@ -28,6 +33,6 @@ export async function runCli(argv = process.argv.slice(2)) {
28
33
  return 1;
29
34
  }
30
35
  finally {
31
- agents?.close();
36
+ await agents?.close();
32
37
  }
33
38
  }
@@ -0,0 +1,4 @@
1
+ import type { Agents } from '../agents.js';
2
+ import type { ParsedArgs } from './args.js';
3
+ export declare function runSessionCommand(agents: Agents, args: ParsedArgs): Promise<unknown>;
4
+ export declare function runTalkCommand(agents: Agents, args: ParsedArgs): Promise<unknown>;
@@ -0,0 +1,54 @@
1
+ import { AgentError } from '../errors.js';
2
+ import { enabled, flag } from './args.js';
3
+ export async function runSessionCommand(agents, args) {
4
+ const [, action, first, second] = args.words;
5
+ if (action === 'list') {
6
+ return { sessions: agents.listSessions(flag(args, 'agent'), enabled(args, 'include-archived')) };
7
+ }
8
+ if (action === 'start' && first) {
9
+ const sessionId = flag(args, 'id');
10
+ const revisionId = flag(args, 'revision');
11
+ return { session: agents.startSession({
12
+ agentId: first,
13
+ ...(sessionId ? { sessionId } : {}),
14
+ ...(revisionId ? { revisionId } : {}),
15
+ }) };
16
+ }
17
+ if (action === 'show' && first) {
18
+ const session = agents.getSession(first);
19
+ if (!session)
20
+ throw new AgentError('not_found', `Session not found: ${first}`);
21
+ return { session, turns: agents.listTurns(first), events: agents.listEvents(first) };
22
+ }
23
+ if (action === 'events' && first) {
24
+ return { events: agents.listEvents(first, Number(flag(args, 'after') || 0)) };
25
+ }
26
+ if (action === 'send' && first && second) {
27
+ const idempotencyKey = flag(args, 'key');
28
+ return { turn: await agents.send(first, {
29
+ message: second,
30
+ ...(idempotencyKey ? { idempotencyKey } : {}),
31
+ }) };
32
+ }
33
+ if (action === 'cancel' && first)
34
+ return { turn: agents.cancelSession(first) };
35
+ if (action === 'archive' && first)
36
+ return { session: agents.archiveSession(first) };
37
+ throw new AgentError('invalid_input', 'Unknown or incomplete session command. Run with --help.');
38
+ }
39
+ export async function runTalkCommand(agents, args) {
40
+ const [, agentId, message] = args.words;
41
+ if (!agentId || !message)
42
+ throw new AgentError('invalid_input', 'talk requires agent id and message.');
43
+ const sessionId = flag(args, 'session');
44
+ const idempotencyKey = flag(args, 'key');
45
+ const call = agents.talk(agentId, {
46
+ message,
47
+ ...(sessionId ? { sessionId } : {}),
48
+ ...(idempotencyKey ? { idempotencyKey } : {}),
49
+ });
50
+ if (enabled(args, 'background')) {
51
+ return { session: call.session, turn: call.turn, duplicate: call.duplicate };
52
+ }
53
+ return { session: call.session, turn: await call.completion, duplicate: call.duplicate };
54
+ }
@@ -0,0 +1,4 @@
1
+ import type { AgentDriver } from './types.js';
2
+ export declare function defineDriver(driver: AgentDriver): AgentDriver;
3
+ export declare function loadDriverModules(specifiers: string[]): Promise<AgentDriver[]>;
4
+ export declare function driverSpecifiers(value?: string): string[];
@@ -0,0 +1,25 @@
1
+ import path from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+ import { AgentError } from './errors.js';
4
+ export function defineDriver(driver) {
5
+ if (!driver || typeof driver.id !== 'string' || typeof driver.run !== 'function') {
6
+ throw new AgentError('invalid_input', 'Agent drivers require id and run.');
7
+ }
8
+ return driver;
9
+ }
10
+ export async function loadDriverModules(specifiers) {
11
+ const drivers = [];
12
+ for (const specifier of specifiers) {
13
+ const target = specifier.startsWith('.') || specifier.startsWith('/')
14
+ ? pathToFileURL(path.resolve(specifier)).href
15
+ : specifier;
16
+ const loaded = await import(target);
17
+ const value = loaded.default ?? loaded.driver ?? loaded;
18
+ const driver = typeof value === 'function' ? await value() : value;
19
+ drivers.push(defineDriver(driver));
20
+ }
21
+ return drivers;
22
+ }
23
+ export function driverSpecifiers(value = process.env.AMALGM_AGENT_DRIVERS || '') {
24
+ return value.split(',').map((item) => item.trim()).filter(Boolean);
25
+ }
package/dist/errors.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { JsonObject } from './types.js';
2
- export type AgentErrorCode = 'invalid_input' | 'not_found' | 'conflict' | 'deleted' | 'too_large' | 'internal';
2
+ export type AgentErrorCode = 'invalid_input' | 'not_found' | 'conflict' | 'deleted' | 'driver_unavailable' | 'too_large' | 'cancelled' | 'internal';
3
3
  export declare class AgentError extends Error {
4
4
  readonly code: AgentErrorCode;
5
5
  readonly details: JsonObject;
package/dist/errors.js CHANGED
@@ -3,7 +3,9 @@ const statusByCode = {
3
3
  not_found: 404,
4
4
  conflict: 409,
5
5
  deleted: 410,
6
+ driver_unavailable: 503,
6
7
  too_large: 413,
8
+ cancelled: 409,
7
9
  internal: 500,
8
10
  };
9
11
  export class AgentError extends Error {
@@ -0,0 +1,13 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { EventListener, JsonObject, SessionEvent } from './types.js';
3
+ export declare class EventStore {
4
+ private readonly database;
5
+ private readonly now;
6
+ private readonly listener;
7
+ constructor(database: Database.Database, now: () => string, listener: EventListener);
8
+ append(sessionId: string, turnId: string | null, type: string, data: JsonObject): SessionEvent;
9
+ write(sessionId: string, turnId: string | null, type: string, data: JsonObject): SessionEvent;
10
+ announce(event: SessionEvent): void;
11
+ list(sessionId: string, after?: number, limit?: number): SessionEvent[];
12
+ forTurn(turnId: string): SessionEvent[];
13
+ }
@@ -0,0 +1,50 @@
1
+ import { AgentError } from './errors.js';
2
+ import { newId } from './ids.js';
3
+ import { eventFromRow } from './rows.js';
4
+ export class EventStore {
5
+ database;
6
+ now;
7
+ listener;
8
+ constructor(database, now, listener) {
9
+ this.database = database;
10
+ this.now = now;
11
+ this.listener = listener;
12
+ }
13
+ append(sessionId, turnId, type, data) {
14
+ let event;
15
+ this.database.transaction(() => {
16
+ event = this.write(sessionId, turnId, type, data);
17
+ this.database.prepare('UPDATE sessions SET updated_at = ? WHERE id = ?').run(this.now(), sessionId);
18
+ })();
19
+ if (!event)
20
+ throw new AgentError('internal', 'Event transaction produced no event.');
21
+ this.announce(event);
22
+ return event;
23
+ }
24
+ write(sessionId, turnId, type, data) {
25
+ const sequence = Number(this.database.prepare(`
26
+ SELECT COALESCE(MAX(sequence), 0) + 1 AS value FROM session_events WHERE session_id = ?
27
+ `).get(sessionId).value);
28
+ const id = newId('event');
29
+ const createdAt = this.now();
30
+ this.database.prepare(`
31
+ INSERT INTO session_events (id, session_id, turn_id, sequence, type, data_json, created_at)
32
+ VALUES (?, ?, ?, ?, ?, ?, ?)
33
+ `).run(id, sessionId, turnId, sequence, type, JSON.stringify(data), createdAt);
34
+ return { id, sessionId, turnId, sequence, type, data, createdAt };
35
+ }
36
+ announce(event) {
37
+ this.listener(event);
38
+ }
39
+ list(sessionId, after = 0, limit = 200) {
40
+ const cursor = Number.isFinite(after) ? Math.max(0, Math.floor(after)) : 0;
41
+ const bounded = Number.isFinite(limit) ? Math.max(1, Math.min(1000, Math.floor(limit))) : 200;
42
+ return this.database.prepare(`
43
+ SELECT * FROM session_events WHERE session_id = ? AND sequence > ? ORDER BY sequence ASC LIMIT ?
44
+ `).all(sessionId, cursor, bounded).map(eventFromRow);
45
+ }
46
+ forTurn(turnId) {
47
+ return this.database.prepare('SELECT * FROM session_events WHERE turn_id = ? ORDER BY sequence ASC')
48
+ .all(turnId).map(eventFromRow);
49
+ }
50
+ }