macro-agent 0.0.16 → 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 (45) hide show
  1. package/dist/acp/macro-agent.d.ts +2 -0
  2. package/dist/acp/macro-agent.d.ts.map +1 -1
  3. package/dist/acp/macro-agent.js +52 -20
  4. package/dist/acp/macro-agent.js.map +1 -1
  5. package/dist/agent/agent-manager.d.ts.map +1 -1
  6. package/dist/agent/agent-manager.js +23 -5
  7. package/dist/agent/agent-manager.js.map +1 -1
  8. package/dist/map/adapter/acp-over-map.d.ts +16 -0
  9. package/dist/map/adapter/acp-over-map.d.ts.map +1 -1
  10. package/dist/map/adapter/acp-over-map.js +242 -24
  11. package/dist/map/adapter/acp-over-map.js.map +1 -1
  12. package/dist/map/adapter/map-adapter.d.ts +1 -0
  13. package/dist/map/adapter/map-adapter.d.ts.map +1 -1
  14. package/dist/map/adapter/map-adapter.js +57 -8
  15. package/dist/map/adapter/map-adapter.js.map +1 -1
  16. package/dist/store/event-store.d.ts +5 -0
  17. package/dist/store/event-store.d.ts.map +1 -1
  18. package/dist/store/event-store.js +126 -53
  19. package/dist/store/event-store.js.map +1 -1
  20. package/dist/store/instance.d.ts +0 -2
  21. package/dist/store/instance.d.ts.map +1 -1
  22. package/dist/store/instance.js +1 -24
  23. package/dist/store/instance.js.map +1 -1
  24. package/dist/store/types/agents.d.ts +5 -0
  25. package/dist/store/types/agents.d.ts.map +1 -1
  26. package/package.json +3 -3
  27. package/references/acp-factory-ref/package-lock.json +2 -2
  28. package/references/acp-factory-ref/package.json +2 -2
  29. package/references/claude-code-acp/package-lock.json +2 -2
  30. package/references/claude-code-acp/package.json +1 -1
  31. package/references/claude-code-acp/src/acp-agent.ts +3 -6
  32. package/src/acp/__tests__/history.test.ts +8 -4
  33. package/src/acp/macro-agent.ts +60 -26
  34. package/src/agent/__tests__/agent-manager.test.ts +4 -6
  35. package/src/agent/agent-manager.ts +24 -5
  36. package/src/map/adapter/__tests__/acp-over-map-cancel.test.ts +802 -0
  37. package/src/map/adapter/__tests__/acp-over-map-history.test.ts +1123 -0
  38. package/src/map/adapter/__tests__/acp-over-map-persistence.e2e.test.ts +440 -0
  39. package/src/map/adapter/acp-over-map.ts +282 -25
  40. package/src/map/adapter/map-adapter.ts +79 -9
  41. package/src/store/__tests__/event-store.test.ts +44 -12
  42. package/src/store/__tests__/instance.test.ts +5 -7
  43. package/src/store/event-store.ts +157 -57
  44. package/src/store/instance.ts +1 -29
  45. package/src/store/types/agents.ts +1 -0
@@ -9,8 +9,8 @@
9
9
  */
10
10
 
11
11
  import { createStore, Store } from 'tinybase';
12
- import { createFilePersister } from 'tinybase/persisters/persister-file';
13
- import { createCustomPersister } from 'tinybase/persisters';
12
+ import { createCustomPersister, createCustomSqlitePersister } from 'tinybase/persisters';
13
+ import type { DatabasePersisterConfig } from 'tinybase/persisters';
14
14
  import Database from 'better-sqlite3';
15
15
  import { nanoid } from 'nanoid';
16
16
  import * as path from 'path';
@@ -65,38 +65,80 @@ import type { StorageBackend, ExportedEvent } from './backends/types.js';
65
65
  import { createTinyBaseBackend } from './backends/tinybase-backend.js';
66
66
 
67
67
  // ─────────────────────────────────────────────────────────────────────────────
68
- // Custom better-sqlite3 Persister
68
+ // Tabular better-sqlite3 Persister
69
69
  // ─────────────────────────────────────────────────────────────────────────────
70
70
 
71
71
  /**
72
- * Creates a custom TinyBase persister for better-sqlite3.
73
- * TinyBase's built-in createSqlite3Persister expects node-sqlite3 (async/callback API),
74
- * but we use better-sqlite3 (sync API). This custom persister bridges the gap.
72
+ * Build tabular config for the 10 TinyBase tables.
73
+ * Identity mapping: TinyBase table name === SQLite table name.
75
74
  */
76
- function createBetterSqlite3Persister(
75
+ function getTabularConfig(): DatabasePersisterConfig {
76
+ const tableNames = [
77
+ 'events', 'agents', 'tasks', 'messages',
78
+ 'sessions', 'conversations', 'turns',
79
+ 'threads', 'subscriptions', 'participants',
80
+ ];
81
+
82
+ const load: Record<string, string> = {};
83
+ const save: Record<string, string> = {};
84
+ for (const t of tableNames) {
85
+ load[t] = t; // SQLite table -> TinyBase table (same name)
86
+ save[t] = t; // TinyBase table -> SQLite table (same name)
87
+ }
88
+
89
+ return {
90
+ mode: 'tabular',
91
+ tables: { load, save },
92
+ autoLoadIntervalSeconds: 1,
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Creates a tabular TinyBase persister backed by better-sqlite3.
98
+ *
99
+ * Unlike the old JSON blob approach (entire store as one JSON string in a
100
+ * single row), tabular mode maps each TinyBase table to a real SQLite table
101
+ * and only writes changed rows on autoSave — making persistence O(delta)
102
+ * instead of O(total).
103
+ */
104
+ function createTabularBetterSqlite3Persister(
77
105
  store: Store,
78
106
  db: ReturnType<typeof Database>,
79
- tableName: string = 'tinybase_store'
80
107
  ) {
81
- // Create table if not exists
82
- db.exec(`CREATE TABLE IF NOT EXISTS ${tableName} (_id TEXT PRIMARY KEY, store TEXT)`);
108
+ // Wrap better-sqlite3's sync API as the async DatabaseExecuteCommand.
109
+ // TinyBase generates SQL with $1, $2, ... placeholders (PostgreSQL-style),
110
+ // but better-sqlite3 uses ? for positional array binding. Convert them.
111
+ const executeCommand = async (sql: string, params?: any[]): Promise<Record<string, any>[]> => {
112
+ const convertedSql = sql.replace(/\$\d+/g, '?');
113
+ const trimmed = convertedSql.trimStart().toUpperCase();
114
+ const stmt = db.prepare(convertedSql);
115
+ if (trimmed.startsWith('SELECT') || trimmed.startsWith('PRAGMA')) {
116
+ return (params ? stmt.all(...params) : stmt.all()) as Record<string, any>[];
117
+ }
118
+ params ? stmt.run(...params) : stmt.run();
119
+ return [];
120
+ };
83
121
 
84
- return createCustomPersister(
122
+ return createCustomSqlitePersister(
85
123
  store,
86
- // getPersisted - load from SQLite
87
- async () => {
88
- const row = db.prepare(`SELECT store FROM ${tableName} WHERE _id = '_'`).get() as { store: string } | undefined;
89
- return row ? JSON.parse(row.store) : undefined;
90
- },
91
- // setPersisted - save to SQLite
92
- async (getContent) => {
93
- const json = JSON.stringify(getContent());
94
- db.prepare(`INSERT OR REPLACE INTO ${tableName} (_id, store) VALUES ('_', ?)`).run(json);
95
- },
96
- // addPersisterListener - poll for external changes (cross-process)
97
- (listener) => setInterval(listener, 1000),
98
- // delPersisterListener - cleanup polling
99
- (interval: ReturnType<typeof setInterval>) => clearInterval(interval),
124
+ getTabularConfig(),
125
+ executeCommand,
126
+ // addChangeListener better-sqlite3 has no native change events
127
+ (_listener: (tableName: string) => void) => null as any,
128
+ // delChangeListener — no-op
129
+ (_handle: any) => {},
130
+ // onSqlCommand
131
+ undefined,
132
+ // onIgnoredError
133
+ (error: any) => console.warn('[EventStore] Persister error:', error),
134
+ // destroy
135
+ () => db.close(),
136
+ // persist mode (1 = StoreOnly)
137
+ 1 as any,
138
+ // thing (the db instance)
139
+ db,
140
+ // getThing accessor name
141
+ 'getDb',
100
142
  );
101
143
  }
102
144
 
@@ -161,6 +203,7 @@ export interface EventStore {
161
203
  // Agent view
162
204
  getAgent(agentId: AgentId): Agent | null;
163
205
  listAgents(filter?: { state?: AgentState; parent?: AgentId | null }): Agent[];
206
+ updateAgentPlan(agentId: AgentId, plan: Array<{ content: string; priority: string; status: string }>): void;
164
207
 
165
208
  // Task view
166
209
  getTask(taskId: TaskId): Task | null;
@@ -252,7 +295,15 @@ export function parseDuration(duration: string): number {
252
295
  export async function createEventStore(config: StoreConfig = {}): Promise<EventStore> {
253
296
  // Resolve instance configuration
254
297
  const resolved = resolveInstancePath(config);
255
- const { instanceId, instancePath, namespace, isNew, isLegacy, backendType } = resolved;
298
+ const { instanceId, instancePath, namespace, isNew, backendType } = resolved;
299
+
300
+ // Reject deprecated legacy path option
301
+ if (config.path) {
302
+ throw new Error(
303
+ '[macro-agent] The `path` option has been removed. ' +
304
+ 'Use `instanceId` and `baseDir` instead for per-instance isolation.'
305
+ );
306
+ }
256
307
 
257
308
  // Track baseDir for MCP subprocess communication
258
309
  const baseDir = config.baseDir ?? path.join(os.homedir(), '.multiagent');
@@ -261,15 +312,6 @@ export async function createEventStore(config: StoreConfig = {}): Promise<EventS
261
312
  const peerVisibility: PeerVisibilityConfig =
262
313
  config.peerVisibility ?? DEFAULT_PEER_VISIBILITY;
263
314
 
264
- // Emit deprecation warning for legacy path option
265
- if (isLegacy) {
266
- console.warn(
267
- '[macro-agent] DEPRECATION WARNING: The `path` option is deprecated and will be removed in a future version. ' +
268
- 'Use `instanceId` and `baseDir` instead for per-instance isolation. ' +
269
- 'See documentation for migration guide.'
270
- );
271
- }
272
-
273
315
  // Emit warning for in-memory mode (only in non-test environments)
274
316
  if (config.inMemory && process.env.NODE_ENV !== 'test') {
275
317
  console.warn(
@@ -282,31 +324,53 @@ export async function createEventStore(config: StoreConfig = {}): Promise<EventS
282
324
  const store = createStore();
283
325
 
284
326
  // Set up persister based on backend type
285
- let persister: ReturnType<typeof createFilePersister> | ReturnType<typeof createBetterSqlite3Persister> | null = null;
327
+ let persister: ReturnType<typeof createTabularBetterSqlite3Persister> | null = null;
286
328
  let db: ReturnType<typeof Database> | null = null;
287
329
 
288
330
  if (!config.inMemory) {
289
- if (isLegacy) {
290
- // Legacy mode: Use JSON file persister at the specified path
291
- const dir = path.dirname(instancePath);
292
- if (!fs.existsSync(dir)) {
293
- fs.mkdirSync(dir, { recursive: true });
294
- }
295
- persister = createFilePersister(store, instancePath);
296
- } else {
297
- // New instances: Use SQLite with custom better-sqlite3 persister
298
- ensureInstanceDir(instancePath);
299
- const dbPath = path.join(instancePath, 'store.sqlite');
300
- db = new Database(dbPath);
301
- db.pragma('journal_mode = WAL');
302
- db.pragma('busy_timeout = 5000');
303
- persister = createBetterSqlite3Persister(store, db);
331
+ ensureInstanceDir(instancePath);
332
+ const dbPath = path.join(instancePath, 'store.sqlite');
333
+ db = new Database(dbPath);
334
+ db.pragma('journal_mode = WAL');
335
+ db.pragma('busy_timeout = 5000');
336
+
337
+ // Migration: if old JSON blob table exists, load data from it first
338
+ const oldTableExists = db.prepare(
339
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='tinybase_store'"
340
+ ).get();
341
+
342
+ if (oldTableExists) {
343
+ const oldPersister = createCustomPersister(
344
+ store,
345
+ async () => {
346
+ const row = db!.prepare("SELECT store FROM tinybase_store WHERE _id = '_'").get() as { store: string } | undefined;
347
+ return row ? JSON.parse(row.store) : undefined;
348
+ },
349
+ async () => {},
350
+ (listener) => setInterval(listener, 1000),
351
+ (interval: ReturnType<typeof setInterval>) => clearInterval(interval),
352
+ );
353
+ await oldPersister.load();
354
+ oldPersister.destroy();
304
355
  }
356
+
357
+ persister = createTabularBetterSqlite3Persister(store, db);
358
+
359
+ if (oldTableExists) {
360
+ // Save migrated data to new tabular format, then drop old table
361
+ await persister.save();
362
+ db.exec('DROP TABLE IF EXISTS tinybase_store');
363
+ }
364
+
305
365
  await persister.load();
366
+ // Auto-save: persist to disk whenever the in-memory store changes.
367
+ // Without this, emit() only writes to TinyBase's in-memory store and
368
+ // data is lost if the server is killed before an explicit persist().
369
+ await persister.startAutoSave();
306
370
  }
307
371
 
308
372
  // Initialize/update instance metadata
309
- if (!config.inMemory && !isLegacy) {
373
+ if (!config.inMemory) {
310
374
  if (isNew) {
311
375
  const meta = createInstanceMeta(resolved, config);
312
376
  writeInstanceMeta(instancePath, meta);
@@ -456,6 +520,25 @@ export async function createEventStore(config: StoreConfig = {}): Promise<EventS
456
520
  return agents;
457
521
  }
458
522
 
523
+ /**
524
+ * Update an agent's plan entries (persisted to SQLite via TinyBase)
525
+ */
526
+ function updateAgentPlan(
527
+ agentId: AgentId,
528
+ plan: Array<{ content: string; priority: string; status: string }>,
529
+ ): void {
530
+ const row = store.getRow('agents', agentId);
531
+ if (!row.id) return;
532
+
533
+ store.setPartialRow('agents', agentId, {
534
+ plan: JSON.stringify(plan),
535
+ last_activity_at: Date.now(),
536
+ });
537
+
538
+ const agent = rowToAgent(store.getRow('agents', agentId));
539
+ notifyAgentChange(agentId, agent);
540
+ }
541
+
459
542
  /**
460
543
  * Get task by ID
461
544
  */
@@ -876,11 +959,6 @@ export async function createEventStore(config: StoreConfig = {}): Promise<EventS
876
959
  * Get archives directory path
877
960
  */
878
961
  function getArchivesDir(): string {
879
- // For legacy mode, use parent of the file path
880
- // For new mode, use the instance directory
881
- if (isLegacy) {
882
- return path.join(path.dirname(instancePath), 'archives');
883
- }
884
962
  return path.join(instancePath, 'archives');
885
963
  }
886
964
 
@@ -1145,6 +1223,7 @@ export async function createEventStore(config: StoreConfig = {}): Promise<EventS
1145
1223
  // Views
1146
1224
  getAgent,
1147
1225
  listAgents,
1226
+ updateAgentPlan,
1148
1227
  getTask,
1149
1228
  listTasks,
1150
1229
  getMessages,
@@ -1211,6 +1290,17 @@ function initializeTables(store: Store): void {
1211
1290
  * Rebuild materialized views from the event log
1212
1291
  */
1213
1292
  function rebuildViews(store: Store): void {
1293
+ // Preserve out-of-band agent fields that aren't derived from events.
1294
+ // `plan` is written directly via updateAgentPlan(), not through events,
1295
+ // so it would be lost when we clear and replay.
1296
+ const savedAgentPlan = new Map<string, string>();
1297
+ for (const rowId of store.getRowIds('agents')) {
1298
+ const row = store.getRow('agents', rowId);
1299
+ if (row.plan && row.plan !== '[]') {
1300
+ savedAgentPlan.set(rowId, row.plan as string);
1301
+ }
1302
+ }
1303
+
1214
1304
  // Clear existing views
1215
1305
  for (const rowId of store.getRowIds('agents')) {
1216
1306
  store.delRow('agents', rowId);
@@ -1266,6 +1356,14 @@ function rebuildViews(store: Store): void {
1266
1356
  for (const event of events) {
1267
1357
  applyEventToViews(store, event, noop, noop, noop, noop, noop, noop);
1268
1358
  }
1359
+
1360
+ // Restore out-of-band agent fields preserved before the wipe
1361
+ for (const [agentId, plan] of savedAgentPlan) {
1362
+ const row = store.getRow('agents', agentId);
1363
+ if (row.id) {
1364
+ store.setPartialRow('agents', agentId, { plan });
1365
+ }
1366
+ }
1269
1367
  }
1270
1368
 
1271
1369
  /**
@@ -1364,6 +1462,7 @@ function applySpawnEvent(
1364
1462
  role: payload.role ?? '',
1365
1463
  config: JSON.stringify(payload.config ?? {}),
1366
1464
  cwd: payload.cwd ?? process.cwd(),
1465
+ plan: '[]',
1367
1466
  created_at: event.timestamp,
1368
1467
  started_at: 0,
1369
1468
  stopped_at: 0,
@@ -1712,6 +1811,7 @@ function rowToAgent(row: Record<string, unknown>): Agent {
1712
1811
  role: (row.role as string) || undefined,
1713
1812
  config: row.config ? JSON.parse(row.config as string) : {},
1714
1813
  cwd: (row.cwd as string) || process.cwd(),
1814
+ plan: row.plan ? JSON.parse(row.plan as string) : [],
1715
1815
  created_at: row.created_at as Timestamp,
1716
1816
  started_at: (row.started_at as number) || undefined,
1717
1817
  stopped_at: (row.stopped_at as number) || undefined,
@@ -213,9 +213,6 @@ export interface ResolvedInstance {
213
213
  /** Whether this is a new instance */
214
214
  isNew: boolean;
215
215
 
216
- /** Whether this is using legacy path mode */
217
- isLegacy: boolean;
218
-
219
216
  /** Backend type to use */
220
217
  backendType: string;
221
218
  }
@@ -300,25 +297,11 @@ export function resolveInstancePath(config: StoreConfig): ResolvedInstance {
300
297
  instancePath: ':memory:',
301
298
  namespace,
302
299
  isNew: true,
303
- isLegacy: false,
304
300
  backendType: 'memory',
305
301
  };
306
302
  }
307
303
 
308
- // Legacy path takes precedence for backward compat
309
- if (config.path) {
310
- const legacyInstanceId = deriveInstanceIdFromPath(config.path);
311
- return {
312
- instanceId: legacyInstanceId,
313
- instancePath: config.path,
314
- namespace,
315
- isNew: !fs.existsSync(config.path),
316
- isLegacy: true,
317
- backendType: 'json',
318
- };
319
- }
320
-
321
- // New instance resolution
304
+ // Instance resolution
322
305
  const instanceId = config.instanceId ?? generateInstanceId();
323
306
 
324
307
  if (!isValidInstanceId(instanceId)) {
@@ -339,21 +322,10 @@ export function resolveInstancePath(config: StoreConfig): ResolvedInstance {
339
322
  instancePath,
340
323
  namespace,
341
324
  isNew,
342
- isLegacy: false,
343
325
  backendType,
344
326
  };
345
327
  }
346
328
 
347
- /**
348
- * Derive an instance ID from a legacy path
349
- */
350
- function deriveInstanceIdFromPath(legacyPath: string): string {
351
- // Use the filename without extension as the instance ID
352
- const basename = path.basename(legacyPath, path.extname(legacyPath));
353
- // Sanitize to valid instance ID
354
- return basename.replace(/[^a-zA-Z0-9_-]/g, '_');
355
- }
356
-
357
329
  // ─────────────────────────────────────────────────────────────────────────────
358
330
  // Instance Directory Management
359
331
  // ─────────────────────────────────────────────────────────────────────────────
@@ -40,6 +40,7 @@ export interface Agent {
40
40
  role?: string;
41
41
  config: AgentConfig;
42
42
  cwd: string;
43
+ plan: Array<{ content: string; priority: string; status: string }>;
43
44
  created_at: Timestamp;
44
45
  started_at?: Timestamp;
45
46
  stopped_at?: Timestamp;