@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/dist/schema.js CHANGED
@@ -1,56 +1,31 @@
1
- const SCHEMA_VERSION = 3;
2
- const REGISTRY_SCHEMA = `
3
- CREATE TABLE agents (
4
- id TEXT PRIMARY KEY,
5
- current_revision_id TEXT,
6
- current_revision_number INTEGER NOT NULL DEFAULT 0,
7
- created_at TEXT NOT NULL,
8
- updated_at TEXT NOT NULL,
9
- deleted_at TEXT
10
- );
11
-
12
- CREATE TABLE agent_revisions (
13
- id TEXT PRIMARY KEY,
14
- agent_id TEXT NOT NULL REFERENCES agents(id),
15
- revision_number INTEGER NOT NULL,
16
- definition_hash TEXT NOT NULL,
17
- definition_json TEXT NOT NULL,
18
- created_at TEXT NOT NULL,
19
- UNIQUE(agent_id, revision_number)
20
- );
21
-
22
- CREATE INDEX agent_revisions_agent_idx
23
- ON agent_revisions(agent_id, revision_number DESC);
24
- `;
25
- function upgradeRegistry(database, current) {
1
+ const SCHEMA_VERSION = 2;
2
+ function migrateRevisionHashes(database) {
26
3
  database.pragma('foreign_keys = OFF');
27
4
  try {
28
- database.exec('BEGIN IMMEDIATE');
29
- if (current === 1) {
30
- database.exec(`
31
- CREATE TABLE agent_revisions_next (
32
- id TEXT PRIMARY KEY,
33
- agent_id TEXT NOT NULL REFERENCES agents(id),
34
- revision_number INTEGER NOT NULL,
35
- definition_hash TEXT NOT NULL,
36
- definition_json TEXT NOT NULL,
37
- created_at TEXT NOT NULL,
38
- UNIQUE(agent_id, revision_number)
39
- );
40
- INSERT INTO agent_revisions_next
41
- (id, agent_id, revision_number, definition_hash, definition_json, created_at)
42
- SELECT id, agent_id, revision_number, definition_hash, definition_json, created_at
43
- FROM agent_revisions;
44
- DROP TABLE agent_revisions;
45
- ALTER TABLE agent_revisions_next RENAME TO agent_revisions;
46
- CREATE INDEX agent_revisions_agent_idx
47
- ON agent_revisions(agent_id, revision_number DESC);
48
- `);
49
- }
50
5
  database.exec(`
51
- DROP TABLE IF EXISTS session_events;
52
- DROP TABLE IF EXISTS turns;
53
- DROP TABLE IF EXISTS sessions;
6
+ BEGIN IMMEDIATE;
7
+
8
+ CREATE TABLE agent_revisions_next (
9
+ id TEXT PRIMARY KEY,
10
+ agent_id TEXT NOT NULL REFERENCES agents(id),
11
+ revision_number INTEGER NOT NULL,
12
+ definition_hash TEXT NOT NULL,
13
+ definition_json TEXT NOT NULL,
14
+ created_at TEXT NOT NULL,
15
+ UNIQUE(agent_id, revision_number)
16
+ );
17
+
18
+ INSERT INTO agent_revisions_next
19
+ (id, agent_id, revision_number, definition_hash, definition_json, created_at)
20
+ SELECT id, agent_id, revision_number, definition_hash, definition_json, created_at
21
+ FROM agent_revisions;
22
+
23
+ DROP TABLE agent_revisions;
24
+ ALTER TABLE agent_revisions_next RENAME TO agent_revisions;
25
+
26
+ CREATE INDEX agent_revisions_agent_idx
27
+ ON agent_revisions(agent_id, revision_number DESC);
28
+
54
29
  PRAGMA user_version = ${SCHEMA_VERSION};
55
30
  COMMIT;
56
31
  `);
@@ -71,9 +46,82 @@ export function migrate(database) {
71
46
  }
72
47
  if (current === SCHEMA_VERSION)
73
48
  return;
74
- if (current > 0) {
75
- upgradeRegistry(database, current);
49
+ if (current === 1) {
50
+ migrateRevisionHashes(database);
76
51
  return;
77
52
  }
78
- database.exec(`${REGISTRY_SCHEMA}\nPRAGMA user_version = ${SCHEMA_VERSION};`);
53
+ database.exec(`
54
+ CREATE TABLE agents (
55
+ id TEXT PRIMARY KEY,
56
+ current_revision_id TEXT,
57
+ current_revision_number INTEGER NOT NULL DEFAULT 0,
58
+ created_at TEXT NOT NULL,
59
+ updated_at TEXT NOT NULL,
60
+ deleted_at TEXT
61
+ );
62
+
63
+ CREATE TABLE agent_revisions (
64
+ id TEXT PRIMARY KEY,
65
+ agent_id TEXT NOT NULL REFERENCES agents(id),
66
+ revision_number INTEGER NOT NULL,
67
+ definition_hash TEXT NOT NULL,
68
+ definition_json TEXT NOT NULL,
69
+ created_at TEXT NOT NULL,
70
+ UNIQUE(agent_id, revision_number)
71
+ );
72
+
73
+ CREATE TABLE sessions (
74
+ id TEXT PRIMARY KEY,
75
+ agent_id TEXT NOT NULL REFERENCES agents(id),
76
+ agent_revision_id TEXT NOT NULL REFERENCES agent_revisions(id),
77
+ agent_revision_number INTEGER NOT NULL,
78
+ status TEXT NOT NULL CHECK(status IN ('active', 'archived')),
79
+ driver_session_id TEXT,
80
+ metadata_json TEXT NOT NULL,
81
+ created_at TEXT NOT NULL,
82
+ updated_at TEXT NOT NULL,
83
+ archived_at TEXT
84
+ );
85
+
86
+ CREATE TABLE turns (
87
+ id TEXT PRIMARY KEY,
88
+ session_id TEXT NOT NULL REFERENCES sessions(id),
89
+ idempotency_key TEXT NOT NULL,
90
+ status TEXT NOT NULL CHECK(status IN (
91
+ 'queued', 'running', 'cancelling', 'completed', 'failed', 'cancelled', 'interrupted'
92
+ )),
93
+ input_json TEXT NOT NULL,
94
+ result_json TEXT,
95
+ error_json TEXT,
96
+ created_at TEXT NOT NULL,
97
+ started_at TEXT,
98
+ completed_at TEXT,
99
+ UNIQUE(session_id, idempotency_key)
100
+ );
101
+
102
+ CREATE TABLE session_events (
103
+ id TEXT PRIMARY KEY,
104
+ session_id TEXT NOT NULL REFERENCES sessions(id),
105
+ turn_id TEXT REFERENCES turns(id),
106
+ sequence INTEGER NOT NULL,
107
+ type TEXT NOT NULL,
108
+ data_json TEXT NOT NULL,
109
+ created_at TEXT NOT NULL,
110
+ UNIQUE(session_id, sequence)
111
+ );
112
+
113
+ CREATE INDEX agent_revisions_agent_idx
114
+ ON agent_revisions(agent_id, revision_number DESC);
115
+ CREATE INDEX sessions_agent_idx
116
+ ON sessions(agent_id, updated_at DESC);
117
+ CREATE INDEX turns_session_idx
118
+ ON turns(session_id, created_at ASC);
119
+ CREATE UNIQUE INDEX turns_one_active_per_session_idx
120
+ ON turns(session_id)
121
+ WHERE status IN ('queued', 'running', 'cancelling');
122
+ CREATE INDEX session_events_session_idx
123
+ ON session_events(session_id, sequence ASC);
124
+
125
+ PRAGMA user_version = ${SCHEMA_VERSION};
126
+ `);
79
127
  }
@@ -0,0 +1,15 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { AgentRevision, AgentSession, JsonObject } from './types.js';
3
+ import type { EventStore } from './event-store.js';
4
+ export declare class SessionStore {
5
+ private readonly database;
6
+ private readonly events;
7
+ private readonly now;
8
+ constructor(database: Database.Database, events: EventStore, now: () => string);
9
+ create(revision: AgentRevision, sessionId?: string, metadata?: JsonObject): AgentSession;
10
+ get(id: string): AgentSession | null;
11
+ require(id: string): AgentSession;
12
+ list(agentId?: string, includeArchived?: boolean): AgentSession[];
13
+ archive(id: string, hasActiveTurn: boolean): AgentSession;
14
+ setDriverSession(id: string, driverSessionId: string): void;
15
+ }
@@ -0,0 +1,83 @@
1
+ import { AgentError } from './errors.js';
2
+ import { newId, optionalId, requireId } from './ids.js';
3
+ import { assertJsonObject } from './json.js';
4
+ import { sessionFromRow } from './rows.js';
5
+ export class SessionStore {
6
+ database;
7
+ events;
8
+ now;
9
+ constructor(database, events, now) {
10
+ this.database = database;
11
+ this.events = events;
12
+ this.now = now;
13
+ }
14
+ create(revision, sessionId, metadata = {}) {
15
+ const id = optionalId(sessionId, 'session id') || newId('session');
16
+ if (this.get(id))
17
+ throw new AgentError('conflict', `Session id already exists: ${id}`);
18
+ const timestamp = this.now();
19
+ const storedMetadata = assertJsonObject(metadata, 'metadata');
20
+ let event;
21
+ this.database.transaction(() => {
22
+ this.database.prepare(`
23
+ INSERT INTO sessions
24
+ (id, agent_id, agent_revision_id, agent_revision_number, status, metadata_json, created_at, updated_at)
25
+ VALUES (?, ?, ?, ?, 'active', ?, ?, ?)
26
+ `).run(id, revision.agentId, revision.id, revision.number, JSON.stringify(storedMetadata), timestamp, timestamp);
27
+ event = this.events.write(id, null, 'session.started', {
28
+ agentId: revision.agentId,
29
+ agentRevisionId: revision.id,
30
+ agentRevision: revision.number,
31
+ });
32
+ })();
33
+ if (event)
34
+ this.events.announce(event);
35
+ return this.require(id);
36
+ }
37
+ get(id) {
38
+ const clean = requireId(id, 'session id');
39
+ const row = this.database.prepare('SELECT * FROM sessions WHERE id = ?').get(clean);
40
+ return row ? sessionFromRow(row) : null;
41
+ }
42
+ require(id) {
43
+ const session = this.get(id);
44
+ if (!session)
45
+ throw new AgentError('not_found', `Session not found: ${id}`);
46
+ return session;
47
+ }
48
+ list(agentId, includeArchived = false) {
49
+ const filters = [];
50
+ const values = [];
51
+ if (agentId) {
52
+ filters.push('agent_id = ?');
53
+ values.push(requireId(agentId, 'agent id'));
54
+ }
55
+ if (!includeArchived)
56
+ filters.push("status = 'active'");
57
+ const where = filters.length ? ` WHERE ${filters.join(' AND ')}` : '';
58
+ return this.database.prepare(`SELECT * FROM sessions${where} ORDER BY updated_at DESC`).all(...values)
59
+ .map(sessionFromRow);
60
+ }
61
+ archive(id, hasActiveTurn) {
62
+ const session = this.require(id);
63
+ if (hasActiveTurn)
64
+ throw new AgentError('conflict', 'Cannot archive a session with an active turn.');
65
+ if (session.status === 'archived')
66
+ return session;
67
+ const timestamp = this.now();
68
+ let event;
69
+ this.database.transaction(() => {
70
+ this.database.prepare(`
71
+ UPDATE sessions SET status = 'archived', archived_at = ?, updated_at = ? WHERE id = ?
72
+ `).run(timestamp, timestamp, session.id);
73
+ event = this.events.write(session.id, null, 'session.archived', {});
74
+ })();
75
+ if (event)
76
+ this.events.announce(event);
77
+ return this.require(session.id);
78
+ }
79
+ setDriverSession(id, driverSessionId) {
80
+ this.database.prepare('UPDATE sessions SET driver_session_id = ?, updated_at = ? WHERE id = ?')
81
+ .run(driverSessionId, this.now(), requireId(id, 'session id'));
82
+ }
83
+ }
@@ -0,0 +1,31 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { EventStore } from './event-store.js';
3
+ import type { SessionStore } from './session-store.js';
4
+ import type { AgentFailure, AgentMessage, AgentTurn, JsonObject } from './types.js';
5
+ export declare class TurnStore {
6
+ private readonly database;
7
+ private readonly sessions;
8
+ private readonly events;
9
+ private readonly now;
10
+ constructor(database: Database.Database, sessions: SessionStore, events: EventStore, now: () => string);
11
+ reserve(sessionId: string, input: AgentMessage, key?: string): {
12
+ turn: AgentTurn;
13
+ duplicate: boolean;
14
+ };
15
+ markRunning(id: string): AgentTurn;
16
+ requestCancellation(id: string): AgentTurn;
17
+ complete(id: string, result: JsonObject): AgentTurn;
18
+ fail(id: string, failure: AgentFailure): AgentTurn;
19
+ cancel(id: string): AgentTurn;
20
+ interrupt(id: string, failure: AgentFailure): AgentTurn;
21
+ get(id: string): AgentTurn | null;
22
+ require(id: string): AgentTurn;
23
+ list(sessionId: string): AgentTurn[];
24
+ active(sessionId: string): AgentTurn | null;
25
+ history(sessionId: string, beforeTurnId?: string): AgentMessage[];
26
+ recoverInterrupted(): number;
27
+ private byKey;
28
+ private transition;
29
+ private terminal;
30
+ private touch;
31
+ }
@@ -0,0 +1,164 @@
1
+ import { AgentError } from './errors.js';
2
+ import { newId, optionalKey, randomKey, requireId } from './ids.js';
3
+ import { isObject } from './json.js';
4
+ import { turnFromRow } from './rows.js';
5
+ const ACTIVE_SQL = `status IN ('queued', 'running', 'cancelling')`;
6
+ const TERMINAL = new Set(['completed', 'failed', 'cancelled', 'interrupted']);
7
+ export class TurnStore {
8
+ database;
9
+ sessions;
10
+ events;
11
+ now;
12
+ constructor(database, sessions, events, now) {
13
+ this.database = database;
14
+ this.sessions = sessions;
15
+ this.events = events;
16
+ this.now = now;
17
+ }
18
+ reserve(sessionId, input, key) {
19
+ const session = this.sessions.require(sessionId);
20
+ if (session.status !== 'active')
21
+ throw new AgentError('conflict', 'Session is archived.');
22
+ const idempotencyKey = optionalKey(key) || randomKey('turn-key');
23
+ const existing = this.byKey(session.id, idempotencyKey);
24
+ if (existing)
25
+ return { turn: existing, duplicate: true };
26
+ if (this.active(session.id))
27
+ throw new AgentError('conflict', 'Session already has an active turn.');
28
+ const id = newId('turn');
29
+ const timestamp = this.now();
30
+ let event;
31
+ try {
32
+ this.database.transaction(() => {
33
+ this.database.prepare(`
34
+ INSERT INTO turns (id, session_id, idempotency_key, status, input_json, created_at)
35
+ VALUES (?, ?, ?, 'queued', ?, ?)
36
+ `).run(id, session.id, idempotencyKey, JSON.stringify(input), timestamp);
37
+ event = this.events.write(session.id, id, 'turn.input', { message: input });
38
+ this.touch(session.id, timestamp);
39
+ })();
40
+ }
41
+ catch (error) {
42
+ const racedDuplicate = this.byKey(session.id, idempotencyKey);
43
+ if (racedDuplicate)
44
+ return { turn: racedDuplicate, duplicate: true };
45
+ if (isUniqueConstraint(error)) {
46
+ throw new AgentError('conflict', 'Session already has an active turn.');
47
+ }
48
+ throw error;
49
+ }
50
+ if (event)
51
+ this.events.announce(event);
52
+ return { turn: this.require(id), duplicate: false };
53
+ }
54
+ markRunning(id) {
55
+ return this.transition(id, 'running', 'turn.running', true);
56
+ }
57
+ requestCancellation(id) {
58
+ return this.transition(id, 'cancelling', 'turn.cancelling');
59
+ }
60
+ complete(id, result) {
61
+ return this.terminal(id, 'completed', result, null, 'turn.completed');
62
+ }
63
+ fail(id, failure) {
64
+ return this.terminal(id, 'failed', null, failure, 'turn.failed');
65
+ }
66
+ cancel(id) {
67
+ return this.terminal(id, 'cancelled', null, null, 'turn.cancelled');
68
+ }
69
+ interrupt(id, failure) {
70
+ return this.terminal(id, 'interrupted', null, failure, 'turn.interrupted');
71
+ }
72
+ get(id) {
73
+ const row = this.database.prepare('SELECT * FROM turns WHERE id = ?').get(requireId(id, 'turn id'));
74
+ return row ? turnFromRow(row) : null;
75
+ }
76
+ require(id) {
77
+ const turn = this.get(id);
78
+ if (!turn)
79
+ throw new AgentError('not_found', `Turn not found: ${id}`);
80
+ return turn;
81
+ }
82
+ list(sessionId) {
83
+ this.sessions.require(sessionId);
84
+ return this.database.prepare('SELECT * FROM turns WHERE session_id = ? ORDER BY created_at ASC')
85
+ .all(sessionId).map(turnFromRow);
86
+ }
87
+ active(sessionId) {
88
+ const row = this.database.prepare(`
89
+ SELECT * FROM turns WHERE session_id = ? AND ${ACTIVE_SQL} ORDER BY created_at DESC LIMIT 1
90
+ `).get(sessionId);
91
+ return row ? turnFromRow(row) : null;
92
+ }
93
+ history(sessionId, beforeTurnId) {
94
+ const messages = [];
95
+ for (const turn of this.list(sessionId)) {
96
+ if (turn.id === beforeTurnId)
97
+ break;
98
+ messages.push(turn.input);
99
+ for (const event of this.events.forTurn(turn.id)) {
100
+ const message = isObject(event.data.message) ? event.data.message : null;
101
+ if (event.type === 'message' && message)
102
+ messages.push(message);
103
+ }
104
+ }
105
+ return messages;
106
+ }
107
+ recoverInterrupted() {
108
+ const rows = this.database.prepare(`SELECT * FROM turns WHERE ${ACTIVE_SQL}`).all();
109
+ for (const row of rows) {
110
+ this.interrupt(turnFromRow(row).id, {
111
+ code: 'process_restarted',
112
+ message: 'The owning process restarted before the driver settled.',
113
+ details: {},
114
+ });
115
+ }
116
+ return rows.length;
117
+ }
118
+ byKey(sessionId, key) {
119
+ const row = this.database.prepare('SELECT * FROM turns WHERE session_id = ? AND idempotency_key = ?')
120
+ .get(sessionId, key);
121
+ return row ? turnFromRow(row) : null;
122
+ }
123
+ transition(id, status, type, started = false) {
124
+ const turn = this.require(id);
125
+ const timestamp = this.now();
126
+ let event;
127
+ this.database.transaction(() => {
128
+ this.database.prepare(`UPDATE turns SET status = ?, started_at = COALESCE(started_at, ?) WHERE id = ?`)
129
+ .run(status, started ? timestamp : turn.startedAt, turn.id);
130
+ event = this.events.write(turn.sessionId, turn.id, type, {});
131
+ this.touch(turn.sessionId, timestamp);
132
+ })();
133
+ if (event)
134
+ this.events.announce(event);
135
+ return this.require(turn.id);
136
+ }
137
+ terminal(id, status, result, error, type) {
138
+ const turn = this.require(id);
139
+ if (TERMINAL.has(turn.status))
140
+ return turn;
141
+ const timestamp = this.now();
142
+ let event;
143
+ this.database.transaction(() => {
144
+ this.database.prepare(`
145
+ UPDATE turns SET status = ?, result_json = ?, error_json = ?, completed_at = ? WHERE id = ?
146
+ `).run(status, result && JSON.stringify(result), error && JSON.stringify(error), timestamp, turn.id);
147
+ const data = error ? { error: error } : {};
148
+ event = this.events.write(turn.sessionId, turn.id, type, data);
149
+ this.touch(turn.sessionId, timestamp);
150
+ })();
151
+ if (event)
152
+ this.events.announce(event);
153
+ return this.require(turn.id);
154
+ }
155
+ touch(sessionId, timestamp) {
156
+ this.database.prepare('UPDATE sessions SET updated_at = ? WHERE id = ?').run(timestamp, sessionId);
157
+ }
158
+ }
159
+ function isUniqueConstraint(error) {
160
+ return Boolean(error
161
+ && typeof error === 'object'
162
+ && 'code' in error
163
+ && String(error.code).startsWith('SQLITE_CONSTRAINT'));
164
+ }
package/dist/types.d.ts CHANGED
@@ -63,8 +63,114 @@ export interface AgentRecord {
63
63
  updatedAt: string;
64
64
  deletedAt: string | null;
65
65
  }
66
+ export type SessionStatus = 'active' | 'archived';
67
+ export type TurnStatus = 'queued' | 'running' | 'cancelling' | 'completed' | 'failed' | 'cancelled' | 'interrupted';
68
+ export interface TextPart {
69
+ type: 'text';
70
+ text: string;
71
+ }
72
+ export interface ReferencePart {
73
+ type: 'reference';
74
+ uri: string;
75
+ name: string | null;
76
+ mediaType: string | null;
77
+ }
78
+ export interface DataPart {
79
+ type: 'data';
80
+ name: string;
81
+ data: JsonValue;
82
+ }
83
+ export type AgentPart = TextPart | ReferencePart | DataPart;
84
+ export type MessageRole = 'user' | 'assistant' | 'system';
85
+ export interface AgentMessage {
86
+ role: MessageRole;
87
+ parts: AgentPart[];
88
+ }
89
+ export interface AgentSession {
90
+ id: string;
91
+ agentId: string;
92
+ agentRevisionId: string;
93
+ agentRevision: number;
94
+ status: SessionStatus;
95
+ driverSessionId: string | null;
96
+ metadata: JsonObject;
97
+ createdAt: string;
98
+ updatedAt: string;
99
+ archivedAt: string | null;
100
+ }
101
+ export interface AgentTurn {
102
+ id: string;
103
+ sessionId: string;
104
+ idempotencyKey: string;
105
+ status: TurnStatus;
106
+ input: AgentMessage;
107
+ result: JsonObject | null;
108
+ error: AgentFailure | null;
109
+ createdAt: string;
110
+ startedAt: string | null;
111
+ completedAt: string | null;
112
+ }
113
+ export interface AgentFailure {
114
+ code: string;
115
+ message: string;
116
+ details: JsonObject;
117
+ }
118
+ export interface SessionEvent {
119
+ id: string;
120
+ sessionId: string;
121
+ turnId: string | null;
122
+ sequence: number;
123
+ type: string;
124
+ data: JsonObject;
125
+ createdAt: string;
126
+ }
127
+ export interface DriverEvent {
128
+ type: string;
129
+ data: JsonObject;
130
+ }
131
+ export interface DriverRunRequest {
132
+ agent: AgentRevision;
133
+ session: AgentSession;
134
+ turn: AgentTurn;
135
+ history: AgentMessage[];
136
+ input: AgentMessage;
137
+ driverSessionId: string | null;
138
+ }
139
+ export interface DriverRunResult {
140
+ message?: AgentMessage;
141
+ driverSessionId?: string;
142
+ metadata?: JsonObject;
143
+ }
144
+ export interface DriverRunContext {
145
+ signal: AbortSignal;
146
+ emit(event: DriverEvent): SessionEvent;
147
+ }
148
+ export interface AgentDriver {
149
+ id: string;
150
+ run(request: DriverRunRequest, context: DriverRunContext): Promise<DriverRunResult | void>;
151
+ }
66
152
  export interface AgentsOptions {
67
153
  stateDir?: string;
68
154
  databasePath?: string;
155
+ drivers?: AgentDriver[];
156
+ maxInputBytes?: number;
157
+ maxEventBytes?: number;
158
+ turnTimeoutMs?: number;
69
159
  now?: () => string;
70
160
  }
161
+ export interface StartSessionInput {
162
+ agentId: string;
163
+ revisionId?: string;
164
+ sessionId?: string;
165
+ metadata?: JsonObject;
166
+ }
167
+ export interface SendInput {
168
+ message: string | AgentMessage;
169
+ idempotencyKey?: string;
170
+ }
171
+ export interface EnqueuedTurn {
172
+ turn: AgentTurn;
173
+ completion: Promise<AgentTurn>;
174
+ duplicate: boolean;
175
+ }
176
+ export type EventListener = (event: SessionEvent) => void;
@@ -2,22 +2,27 @@
2
2
 
3
3
  ## Ownership
4
4
 
5
- The Agents service owns three facts:
5
+ The Agents service owns four facts:
6
6
 
7
7
  1. which agent identities exist;
8
- 2. the immutable revisions of each identity; and
9
- 3. which installed agents and Agent Home descriptors are available locally.
8
+ 2. the immutable revisions of each identity;
9
+ 3. which revision each session uses; and
10
+ 4. the ordered turns and events inside each session.
10
11
 
11
12
  Everything else crosses an adapter boundary.
12
13
 
13
14
  ```text
14
15
  SDK
15
16
 
16
- CLI ───┐ │ ┌─── REST
17
+ CLI ───┐ │ ┌─── REST + SSE
17
18
  ├── Agents ───┤
18
19
  MCP ───┘ │ └─── Engine adapter
19
20
 
20
- SQLite registry
21
+ SQLite ledger
22
+
23
+ AgentDriver
24
+ ┌──────┼──────┐
25
+ Codex Claude custom
21
26
  ```
22
27
 
23
28
  CLI, MCP, REST, and Engine never write SQLite directly. `Agents` is the
@@ -27,17 +32,15 @@ public service; the stores below it are implementation details.
27
32
 
28
33
  | Product | Owns | Agents keeps |
29
34
  |---|---|---|
30
- | Chat | conversations, prepared execution, turns, streams, persistence, reconnect, usage, interrupt | exact agent revision selection |
35
+ | Chat | presentation, titles, participants, read state | no Chat record |
31
36
  | Tools | tools, actions, drivers, loadouts | opaque tool/action ids |
32
37
  | Skills | skill content and installation | opaque skill ids |
33
38
  | Credentials | secret material and authorization | opaque `authRef` |
34
- | Shell | native harness processes, auth, machine effects, exact SDK composition | resolved revision projection |
35
- | Agents | identities, installations, Agent Home descriptors, immutable revisions | canonical registry records |
39
+ | Engine | native harness processes, cloud routing, composition | injected drivers |
40
+ | Agents | definitions, revisions, sessions, turns, events | canonical records |
36
41
 
37
- A Chat conversation is executable history. It names one Agents installation
38
- and the exact immutable revision used by each prepared execution. Agents never
39
- imports Chat and Chat never imports the Agents store; Shell composes their
40
- public capabilities through an explicit resolver.
42
+ An Agents session is executable history. A Chat conversation is a presentation
43
+ object. A Chat may point to a session; the reverse dependency is forbidden.
41
44
 
42
45
  ## Mutation path
43
46
 
@@ -46,16 +49,25 @@ merges a typed patch into the current definition and compares its canonical
46
49
  hash with the current revision. Equal content is idempotent; changed content
47
50
  appends a revision and moves the identity's current pointer.
48
51
 
49
- Deleting an agent sets `deleted_at`. It does not delete immutable revisions or
50
- Chat conversations that already name them.
52
+ Deleting an agent sets `deleted_at`. It does not delete revisions or sessions.
51
53
  The id cannot be recreated accidentally.
52
54
 
53
- ## Resolution path
55
+ ## Execution path
56
+
57
+ 1. A caller starts a session; the current revision id is copied onto it.
58
+ 2. A turn and its input event commit in one transaction.
59
+ 3. The turn becomes `running` before its driver is invoked.
60
+ 4. Driver events commit to the ordered event ledger before subscribers see them.
61
+ 5. The driver returns a message, opaque native session id, and metadata.
62
+ 6. The turn reaches exactly one terminal state.
63
+
64
+ One session serializes turns. Different sessions can execute concurrently.
54
65
 
55
- 1. A caller selects an installed agent identity.
56
- 2. Agents returns the requested immutable revision, or the current revision
57
- when no revision was requested.
58
- 3. Shell projects that revision into Chat's execution-preparation resolver.
59
- 4. Chat freezes the resolved facts before accepting a turn.
66
+ ## Why drivers are injected
60
67
 
61
- Agents ends at this resolution result. Chat is the only execution path.
68
+ Native agent runtimes have different authentication, installation, process,
69
+ streaming, and resume rules. Baking them into storage would couple every user
70
+ of the SDK to Engine. `AgentDriver` receives a complete immutable revision and
71
+ ordered history, then emits durable events. Engine can reuse its current
72
+ chat-core adapters behind this interface; standalone users can provide their
73
+ own drivers.