@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.
Files changed (103) hide show
  1. package/PURPOSE.md +45 -0
  2. package/README.md +110 -0
  3. package/dist/agent-store.d.ts +17 -0
  4. package/dist/agent-store.js +111 -0
  5. package/dist/agents.d.ts +39 -0
  6. package/dist/agents.js +129 -0
  7. package/dist/bin/agents.d.ts +2 -0
  8. package/dist/bin/agents.js +5 -0
  9. package/dist/bin/fatal.d.ts +1 -0
  10. package/dist/bin/fatal.js +5 -0
  11. package/dist/bin/mcp.d.ts +2 -0
  12. package/dist/bin/mcp.js +19 -0
  13. package/dist/bin/rest.d.ts +2 -0
  14. package/dist/bin/rest.js +21 -0
  15. package/dist/cli/agent-commands.d.ts +3 -0
  16. package/dist/cli/agent-commands.js +23 -0
  17. package/dist/cli/args.d.ts +7 -0
  18. package/dist/cli/args.js +31 -0
  19. package/dist/cli/files.d.ts +2 -0
  20. package/dist/cli/files.js +21 -0
  21. package/dist/cli/help.d.ts +1 -0
  22. package/dist/cli/help.js +24 -0
  23. package/dist/cli/open.d.ts +3 -0
  24. package/dist/cli/open.js +12 -0
  25. package/dist/cli/run.d.ts +1 -0
  26. package/dist/cli/run.js +38 -0
  27. package/dist/cli/session-commands.d.ts +4 -0
  28. package/dist/cli/session-commands.js +54 -0
  29. package/dist/cli.d.ts +2 -0
  30. package/dist/cli.js +2 -0
  31. package/dist/database.d.ts +9 -0
  32. package/dist/database.js +35 -0
  33. package/dist/definition.d.ts +4 -0
  34. package/dist/definition.js +93 -0
  35. package/dist/drivers.d.ts +4 -0
  36. package/dist/drivers.js +25 -0
  37. package/dist/errors.d.ts +9 -0
  38. package/dist/errors.js +28 -0
  39. package/dist/event-store.d.ts +13 -0
  40. package/dist/event-store.js +50 -0
  41. package/dist/http/agent-routes.d.ts +2 -0
  42. package/dist/http/agent-routes.js +40 -0
  43. package/dist/http/request.d.ts +5 -0
  44. package/dist/http/request.js +40 -0
  45. package/dist/http/server.d.ts +2 -0
  46. package/dist/http/server.js +82 -0
  47. package/dist/http/session-routes.d.ts +2 -0
  48. package/dist/http/session-routes.js +67 -0
  49. package/dist/http/stream.d.ts +3 -0
  50. package/dist/http/stream.js +30 -0
  51. package/dist/http-types.d.ts +23 -0
  52. package/dist/http-types.js +1 -0
  53. package/dist/http.d.ts +2 -0
  54. package/dist/http.js +1 -0
  55. package/dist/ids.d.ts +5 -0
  56. package/dist/ids.js +32 -0
  57. package/dist/index.d.ts +6 -0
  58. package/dist/index.js +5 -0
  59. package/dist/json.d.ts +7 -0
  60. package/dist/json.js +41 -0
  61. package/dist/mcp/agent-tools.d.ts +3 -0
  62. package/dist/mcp/agent-tools.js +51 -0
  63. package/dist/mcp/helpers.d.ts +5 -0
  64. package/dist/mcp/helpers.js +27 -0
  65. package/dist/mcp/server.d.ts +6 -0
  66. package/dist/mcp/server.js +73 -0
  67. package/dist/mcp/session-tools.d.ts +3 -0
  68. package/dist/mcp/session-tools.js +81 -0
  69. package/dist/mcp/tools.d.ts +3 -0
  70. package/dist/mcp/tools.js +5 -0
  71. package/dist/mcp/types.d.ts +18 -0
  72. package/dist/mcp/types.js +1 -0
  73. package/dist/mcp.d.ts +3 -0
  74. package/dist/mcp.js +2 -0
  75. package/dist/messages.d.ts +3 -0
  76. package/dist/messages.js +56 -0
  77. package/dist/rows.d.ts +7 -0
  78. package/dist/rows.js +65 -0
  79. package/dist/runtime.d.ts +29 -0
  80. package/dist/runtime.js +176 -0
  81. package/dist/schema.d.ts +2 -0
  82. package/dist/schema.js +127 -0
  83. package/dist/session-store.d.ts +15 -0
  84. package/dist/session-store.js +83 -0
  85. package/dist/turn-store.d.ts +31 -0
  86. package/dist/turn-store.js +164 -0
  87. package/dist/types.d.ts +176 -0
  88. package/dist/types.js +1 -0
  89. package/docs/ARCHITECTURE.md +73 -0
  90. package/docs/CLI.md +61 -0
  91. package/docs/DATA_MODEL.md +56 -0
  92. package/docs/DEFINITIONS.md +65 -0
  93. package/docs/DRIVERS.md +72 -0
  94. package/docs/ENGINE_INTEGRATION.md +74 -0
  95. package/docs/MCP.md +24 -0
  96. package/docs/REST.md +78 -0
  97. package/docs/SDK.md +72 -0
  98. package/docs/SECURITY.md +38 -0
  99. package/examples/basic.ts +31 -0
  100. package/examples/reviewer.json +25 -0
  101. package/package.json +64 -0
  102. package/skills/amalgm-agents/SKILL.md +30 -0
  103. package/skills/amalgm-agents/agents/openai.yaml +4 -0
@@ -0,0 +1 @@
1
+ export declare function runCli(argv?: string[]): Promise<number>;
@@ -0,0 +1,38 @@
1
+ import { asAgentError } from '../errors.js';
2
+ import { parseArgs } from './args.js';
3
+ import { runAgentCommand } from './agent-commands.js';
4
+ import { writeJson } from './files.js';
5
+ import { HELP } from './help.js';
6
+ import { openAgents } from './open.js';
7
+ import { runSessionCommand, runTalkCommand } from './session-commands.js';
8
+ export async function runCli(argv = process.argv.slice(2)) {
9
+ const args = parseArgs(argv);
10
+ if (args.flags.has('help') || args.words.length === 0) {
11
+ process.stdout.write(HELP);
12
+ return 0;
13
+ }
14
+ let agents;
15
+ try {
16
+ agents = await openAgents(args);
17
+ const command = args.words[0];
18
+ const value = command === 'agent'
19
+ ? runAgentCommand(agents, args)
20
+ : command === 'session'
21
+ ? await runSessionCommand(agents, args)
22
+ : command === 'talk'
23
+ ? await runTalkCommand(agents, args)
24
+ : undefined;
25
+ if (value === undefined)
26
+ throw new Error(`Unknown command: ${String(command)}`);
27
+ writeJson(value);
28
+ return 0;
29
+ }
30
+ catch (error) {
31
+ const normalized = asAgentError(error);
32
+ process.stderr.write(`${normalized.code}: ${normalized.message}\n`);
33
+ return 1;
34
+ }
35
+ finally {
36
+ await agents?.close();
37
+ }
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
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { parseArgs } from './cli/args.js';
2
+ export { runCli } from './cli/run.js';
package/dist/cli.js ADDED
@@ -0,0 +1,2 @@
1
+ export { parseArgs } from './cli/args.js';
2
+ export { runCli } from './cli/run.js';
@@ -0,0 +1,9 @@
1
+ import Database from 'better-sqlite3';
2
+ import type { AgentsOptions } from './types.js';
3
+ export interface OpenedDatabase {
4
+ database: Database.Database;
5
+ databasePath: string;
6
+ close(): void;
7
+ }
8
+ export declare function defaultStateDir(): string;
9
+ export declare function openDatabase(options?: AgentsOptions): OpenedDatabase;
@@ -0,0 +1,35 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { resolveProductStateDir } from '@amalgm/core/identity';
5
+ import Database from 'better-sqlite3';
6
+ import { migrate } from './schema.js';
7
+ export function defaultStateDir() {
8
+ return resolveProductStateDir({
9
+ product: 'agents',
10
+ primitive: 'agents',
11
+ env: process.env,
12
+ homedir: path.join(os.homedir(), '.amalgm'),
13
+ });
14
+ }
15
+ export function openDatabase(options = {}) {
16
+ const stateDir = path.resolve(options.stateDir || defaultStateDir());
17
+ const databasePath = path.resolve(options.databasePath || path.join(stateDir, 'agents.db'));
18
+ fs.mkdirSync(path.dirname(databasePath), { recursive: true, mode: 0o700 });
19
+ const database = new Database(databasePath);
20
+ database.pragma('journal_mode = WAL');
21
+ database.pragma('foreign_keys = ON');
22
+ database.pragma('busy_timeout = 5000');
23
+ migrate(database);
24
+ let closed = false;
25
+ return {
26
+ database,
27
+ databasePath,
28
+ close() {
29
+ if (closed)
30
+ return;
31
+ closed = true;
32
+ database.close();
33
+ },
34
+ };
35
+ }
@@ -0,0 +1,4 @@
1
+ import type { AgentDefinition, AgentDefinitionInput, AgentPatch } from './types.js';
2
+ export declare function normalizeDefinition(input: AgentDefinitionInput): AgentDefinition;
3
+ export declare function patchDefinition(current: AgentDefinition, patch: AgentPatch): AgentDefinition;
4
+ export declare function definitionHash(definition: AgentDefinition): string;
@@ -0,0 +1,93 @@
1
+ import crypto from 'node:crypto';
2
+ import { AgentError } from './errors.js';
3
+ import { canonicalJson, isObject } from './json.js';
4
+ import { requireId } from './ids.js';
5
+ function text(value, fallback = '') {
6
+ return typeof value === 'string' ? value.trim() : fallback;
7
+ }
8
+ function strings(value) {
9
+ if (!Array.isArray(value))
10
+ return [];
11
+ return [...new Set(value.map((item) => text(item)).filter(Boolean))];
12
+ }
13
+ function object(value) {
14
+ if (!isObject(value))
15
+ return {};
16
+ return JSON.parse(JSON.stringify(value));
17
+ }
18
+ function driver(value) {
19
+ if (typeof value === 'string')
20
+ return { id: requireId(value, 'driver.id'), config: {} };
21
+ if (!isObject(value))
22
+ throw new AgentError('invalid_input', 'driver is required.');
23
+ return { id: requireId(value.id, 'driver.id'), config: object(value.config) };
24
+ }
25
+ function model(value) {
26
+ if (value === null || value === undefined)
27
+ return null;
28
+ if (!isObject(value))
29
+ throw new AgentError('invalid_input', 'model must be an object or null.');
30
+ const id = text(value.id);
31
+ if (!id)
32
+ throw new AgentError('invalid_input', 'model.id is required.');
33
+ return { id, settings: object(value.settings) };
34
+ }
35
+ export function normalizeDefinition(input) {
36
+ if (!isObject(input))
37
+ throw new AgentError('invalid_input', 'agent definition must be an object.');
38
+ const id = requireId(input.id, 'id');
39
+ const name = text(input.name);
40
+ if (!name)
41
+ throw new AgentError('invalid_input', 'name is required.');
42
+ const resources = isObject(input.resources) ? input.resources : {};
43
+ const toolbox = isObject(input.toolbox) ? input.toolbox : {};
44
+ const workspace = isObject(input.workspace) ? input.workspace : {};
45
+ return {
46
+ id,
47
+ name,
48
+ description: text(input.description),
49
+ instructions: typeof input.instructions === 'string' ? input.instructions.trim() : '',
50
+ driver: driver(input.driver),
51
+ model: model(input.model),
52
+ authRef: text(input.authRef) || null,
53
+ resources: {
54
+ files: strings(resources.files),
55
+ skills: strings(resources.skills),
56
+ subagents: strings(resources.subagents).map((item) => requireId(item, 'resources.subagents[]')),
57
+ },
58
+ toolbox: {
59
+ toolIds: strings(toolbox.toolIds),
60
+ actionIds: strings(toolbox.actionIds),
61
+ },
62
+ workspace: { cwd: text(workspace.cwd) || null },
63
+ metadata: object(input.metadata),
64
+ };
65
+ }
66
+ export function patchDefinition(current, patch) {
67
+ const nextDriver = patch.driver === undefined
68
+ ? current.driver
69
+ : isObject(patch.driver)
70
+ ? { ...current.driver, ...patch.driver, config: patch.driver.config ?? current.driver.config }
71
+ : patch.driver;
72
+ return normalizeDefinition({
73
+ ...current,
74
+ ...patch,
75
+ id: current.id,
76
+ driver: nextDriver,
77
+ model: patch.model === undefined ? current.model : patch.model,
78
+ resources: patch.resources === undefined
79
+ ? current.resources
80
+ : { ...current.resources, ...patch.resources },
81
+ toolbox: patch.toolbox === undefined
82
+ ? current.toolbox
83
+ : { ...current.toolbox, ...patch.toolbox },
84
+ workspace: patch.workspace === undefined
85
+ ? current.workspace
86
+ : { ...current.workspace, ...patch.workspace },
87
+ });
88
+ }
89
+ export function definitionHash(definition) {
90
+ return crypto.createHash('sha256')
91
+ .update(canonicalJson(definition))
92
+ .digest('hex');
93
+ }
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ import type { JsonObject } from './types.js';
2
+ export type AgentErrorCode = 'invalid_input' | 'not_found' | 'conflict' | 'deleted' | 'driver_unavailable' | 'too_large' | 'cancelled' | 'internal';
3
+ export declare class AgentError extends Error {
4
+ readonly code: AgentErrorCode;
5
+ readonly details: JsonObject;
6
+ readonly status: number;
7
+ constructor(code: AgentErrorCode, message: string, details?: JsonObject);
8
+ }
9
+ export declare function asAgentError(error: unknown): AgentError;
package/dist/errors.js ADDED
@@ -0,0 +1,28 @@
1
+ const statusByCode = {
2
+ invalid_input: 400,
3
+ not_found: 404,
4
+ conflict: 409,
5
+ deleted: 410,
6
+ driver_unavailable: 503,
7
+ too_large: 413,
8
+ cancelled: 409,
9
+ internal: 500,
10
+ };
11
+ export class AgentError extends Error {
12
+ code;
13
+ details;
14
+ status;
15
+ constructor(code, message, details = {}) {
16
+ super(message);
17
+ this.name = 'AgentError';
18
+ this.code = code;
19
+ this.details = details;
20
+ this.status = statusByCode[code];
21
+ }
22
+ }
23
+ export function asAgentError(error) {
24
+ if (error instanceof AgentError)
25
+ return error;
26
+ const message = error instanceof Error ? error.message : String(error);
27
+ return new AgentError('internal', message);
28
+ }
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ import type { RouteContext } from '../http-types.js';
2
+ export declare function routeAgents(context: RouteContext): Promise<boolean>;
@@ -0,0 +1,40 @@
1
+ import { AgentError } from '../errors.js';
2
+ export async function routeAgents(context) {
3
+ const [resource, id, child] = context.path;
4
+ if (resource !== 'agents')
5
+ return false;
6
+ if (!id && context.method === 'GET') {
7
+ const includeDeleted = context.url.searchParams.get('include_deleted') === 'true';
8
+ context.json(200, { agents: context.agents.listAgents(includeDeleted) });
9
+ return true;
10
+ }
11
+ if (!id && context.method === 'POST') {
12
+ const body = await context.body();
13
+ context.json(201, { agent: context.agents.createAgent(body) });
14
+ return true;
15
+ }
16
+ if (!id)
17
+ throw new AgentError('not_found', 'Route not found.');
18
+ if (child)
19
+ return false;
20
+ if (context.method === 'GET') {
21
+ const includeDeleted = context.url.searchParams.get('include_deleted') === 'true';
22
+ const agent = context.agents.getAgent(id, includeDeleted);
23
+ if (!agent)
24
+ throw new AgentError('not_found', `Agent not found: ${id}`);
25
+ context.json(200, { agent });
26
+ return true;
27
+ }
28
+ if (context.method === 'PATCH') {
29
+ const body = await context.body();
30
+ context.json(200, {
31
+ agent: context.agents.updateAgent(id, body),
32
+ });
33
+ return true;
34
+ }
35
+ if (context.method === 'DELETE') {
36
+ context.json(200, { agent: context.agents.deleteAgent(id) });
37
+ return true;
38
+ }
39
+ return false;
40
+ }
@@ -0,0 +1,5 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { type BearerGuardVerdict } from '@amalgm/core/transport';
3
+ export declare function readJson(request: IncomingMessage, limit: number): Promise<Record<string, unknown>>;
4
+ export declare function sendJson(response: ServerResponse, status: number, value: unknown): void;
5
+ export declare function guardRequest(request: IncomingMessage, token?: string): BearerGuardVerdict;
@@ -0,0 +1,40 @@
1
+ import { timingSafeEqual } from 'node:crypto';
2
+ import { guardBearerRequest } from '@amalgm/core/transport';
3
+ import { AgentError } from '../errors.js';
4
+ import { isObject } from '../json.js';
5
+ export async function readJson(request, limit) {
6
+ const chunks = [];
7
+ let size = 0;
8
+ for await (const chunk of request) {
9
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
10
+ size += buffer.length;
11
+ if (size > limit)
12
+ throw new AgentError('too_large', `Request body exceeds ${limit} bytes.`);
13
+ chunks.push(buffer);
14
+ }
15
+ if (size === 0)
16
+ return {};
17
+ try {
18
+ const value = JSON.parse(Buffer.concat(chunks).toString('utf8'));
19
+ if (!isObject(value))
20
+ throw new Error('not an object');
21
+ return value;
22
+ }
23
+ catch {
24
+ throw new AgentError('invalid_input', 'Request body must be a JSON object.');
25
+ }
26
+ }
27
+ export function sendJson(response, status, value) {
28
+ const body = JSON.stringify(value);
29
+ response.writeHead(status, {
30
+ 'content-type': 'application/json; charset=utf-8',
31
+ 'content-length': Buffer.byteLength(body),
32
+ 'cache-control': 'no-store',
33
+ });
34
+ response.end(body);
35
+ }
36
+ export function guardRequest(request, token) {
37
+ if (!token)
38
+ return { ok: true };
39
+ return guardBearerRequest({ headers: request.headers, expectedToken: token, timingSafeEqual });
40
+ }
@@ -0,0 +1,2 @@
1
+ import type { RestServer, RestServerOptions } from '../http-types.js';
2
+ export declare function createRestServer(options?: RestServerOptions): RestServer;
@@ -0,0 +1,82 @@
1
+ import http from 'node:http';
2
+ import { Agents } from '../agents.js';
3
+ import { asAgentError } from '../errors.js';
4
+ import { routeAgents } from './agent-routes.js';
5
+ import { guardRequest, readJson, sendJson } from './request.js';
6
+ import { routeSessions } from './session-routes.js';
7
+ import { streamEvents } from './stream.js';
8
+ export function createRestServer(options = {}) {
9
+ const agents = options.agents || new Agents(options);
10
+ const owned = !options.agents;
11
+ const bodyLimit = options.bodyLimitBytes || 512_000;
12
+ const server = http.createServer((request, response) => {
13
+ void dispatch(request, response).catch((error) => {
14
+ if (response.headersSent) {
15
+ response.destroy(error instanceof Error ? error : undefined);
16
+ return;
17
+ }
18
+ const normalized = asAgentError(error);
19
+ sendJson(response, normalized.status, {
20
+ error: { code: normalized.code, message: normalized.message, details: normalized.details },
21
+ });
22
+ });
23
+ });
24
+ async function dispatch(request, response) {
25
+ const url = new URL(request.url || '/', 'http://localhost');
26
+ if (url.pathname === '/healthz') {
27
+ sendJson(response, 200, agents.health());
28
+ return;
29
+ }
30
+ const verdict = guardRequest(request, options.token);
31
+ if (!verdict.ok) {
32
+ response.writeHead(verdict.status, {
33
+ ...verdict.headers,
34
+ 'content-length': Buffer.byteLength(verdict.body),
35
+ });
36
+ response.end(verdict.body);
37
+ return;
38
+ }
39
+ const path = url.pathname.split('/').filter(Boolean);
40
+ if (path.shift() !== 'v1') {
41
+ sendJson(response, 404, { error: { code: 'not_found', message: 'Route not found.' } });
42
+ return;
43
+ }
44
+ if (path[0] === 'sessions' && path[2] === 'events' && path[3] === 'stream' && request.method === 'GET') {
45
+ streamEvents(agents, path[1] || '', request, response, Number(url.searchParams.get('after') || 0));
46
+ return;
47
+ }
48
+ const context = {
49
+ agents,
50
+ method: request.method || 'GET',
51
+ path,
52
+ url,
53
+ body: () => readJson(request, bodyLimit),
54
+ json: (status, value) => sendJson(response, status, value),
55
+ };
56
+ if (await routeAgents(context) || await routeSessions(context))
57
+ return;
58
+ sendJson(response, 404, { error: { code: 'not_found', message: 'Route not found.' } });
59
+ }
60
+ return {
61
+ agents,
62
+ server,
63
+ listen(port = 0, host = '127.0.0.1') {
64
+ return new Promise((resolve, reject) => {
65
+ server.once('error', reject);
66
+ server.listen(port, host, () => {
67
+ server.removeListener('error', reject);
68
+ resolve(server.address());
69
+ });
70
+ });
71
+ },
72
+ async close() {
73
+ await new Promise((resolve, reject) => {
74
+ if (!server.listening)
75
+ return resolve();
76
+ server.close((error) => error ? reject(error) : resolve());
77
+ });
78
+ if (owned)
79
+ await agents.close();
80
+ },
81
+ };
82
+ }
@@ -0,0 +1,2 @@
1
+ import type { RouteContext } from '../http-types.js';
2
+ export declare function routeSessions(context: RouteContext): Promise<boolean>;
@@ -0,0 +1,67 @@
1
+ import { AgentError } from '../errors.js';
2
+ export async function routeSessions(context) {
3
+ const [resource, id, child] = context.path;
4
+ if (resource !== 'sessions')
5
+ return false;
6
+ if (!id && context.method === 'GET') {
7
+ const agentId = context.url.searchParams.get('agent_id') || undefined;
8
+ const archived = context.url.searchParams.get('include_archived') === 'true';
9
+ context.json(200, { sessions: context.agents.listSessions(agentId, archived) });
10
+ return true;
11
+ }
12
+ if (!id && context.method === 'POST') {
13
+ const body = await context.body();
14
+ if (typeof body.agentId !== 'string')
15
+ throw new AgentError('invalid_input', 'agentId is required.');
16
+ context.json(201, { session: context.agents.startSession({
17
+ agentId: body.agentId,
18
+ ...(typeof body.revisionId === 'string' ? { revisionId: body.revisionId } : {}),
19
+ ...(typeof body.sessionId === 'string' ? { sessionId: body.sessionId } : {}),
20
+ ...(body.metadata ? { metadata: body.metadata } : {}),
21
+ }) });
22
+ return true;
23
+ }
24
+ if (!id)
25
+ throw new AgentError('not_found', 'Route not found.');
26
+ if (!child && context.method === 'GET') {
27
+ const session = context.agents.getSession(id);
28
+ if (!session)
29
+ throw new AgentError('not_found', `Session not found: ${id}`);
30
+ context.json(200, { session });
31
+ return true;
32
+ }
33
+ if (!child && context.method === 'DELETE') {
34
+ context.json(200, { session: context.agents.archiveSession(id) });
35
+ return true;
36
+ }
37
+ if (child === 'events' && context.method === 'GET') {
38
+ const after = Number(context.url.searchParams.get('after') || 0);
39
+ const limit = Number(context.url.searchParams.get('limit') || 200);
40
+ context.json(200, { events: context.agents.listEvents(id, after, limit) });
41
+ return true;
42
+ }
43
+ if (child === 'turns' && context.method === 'GET') {
44
+ context.json(200, { turns: context.agents.listTurns(id) });
45
+ return true;
46
+ }
47
+ if (child === 'cancel' && context.method === 'POST') {
48
+ context.json(202, { turn: context.agents.cancelSession(id) });
49
+ return true;
50
+ }
51
+ if (child === 'messages' && context.method === 'POST') {
52
+ const body = await context.body();
53
+ if (body.message === undefined)
54
+ throw new AgentError('invalid_input', 'message is required.');
55
+ const enqueued = context.agents.enqueue(id, {
56
+ message: body.message,
57
+ ...(typeof body.idempotencyKey === 'string' ? { idempotencyKey: body.idempotencyKey } : {}),
58
+ });
59
+ if (body.wait === false) {
60
+ context.json(202, { turn: enqueued.turn, duplicate: enqueued.duplicate });
61
+ return true;
62
+ }
63
+ context.json(200, { turn: await enqueued.completion, duplicate: enqueued.duplicate });
64
+ return true;
65
+ }
66
+ return false;
67
+ }
@@ -0,0 +1,3 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import type { Agents } from '../agents.js';
3
+ export declare function streamEvents(agents: Agents, sessionId: string, request: IncomingMessage, response: ServerResponse, after?: number): void;