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.
- package/dist/acp/macro-agent.d.ts +2 -0
- package/dist/acp/macro-agent.d.ts.map +1 -1
- package/dist/acp/macro-agent.js +52 -20
- package/dist/acp/macro-agent.js.map +1 -1
- package/dist/agent/agent-manager.d.ts.map +1 -1
- package/dist/agent/agent-manager.js +23 -5
- package/dist/agent/agent-manager.js.map +1 -1
- package/dist/map/adapter/acp-over-map.d.ts +16 -0
- package/dist/map/adapter/acp-over-map.d.ts.map +1 -1
- package/dist/map/adapter/acp-over-map.js +242 -24
- package/dist/map/adapter/acp-over-map.js.map +1 -1
- package/dist/map/adapter/map-adapter.d.ts +1 -0
- package/dist/map/adapter/map-adapter.d.ts.map +1 -1
- package/dist/map/adapter/map-adapter.js +57 -8
- package/dist/map/adapter/map-adapter.js.map +1 -1
- package/dist/store/event-store.d.ts +5 -0
- package/dist/store/event-store.d.ts.map +1 -1
- package/dist/store/event-store.js +126 -53
- package/dist/store/event-store.js.map +1 -1
- package/dist/store/instance.d.ts +0 -2
- package/dist/store/instance.d.ts.map +1 -1
- package/dist/store/instance.js +1 -24
- package/dist/store/instance.js.map +1 -1
- package/dist/store/types/agents.d.ts +5 -0
- package/dist/store/types/agents.d.ts.map +1 -1
- package/package.json +3 -3
- package/references/acp-factory-ref/package-lock.json +2 -2
- package/references/acp-factory-ref/package.json +2 -2
- package/references/claude-code-acp/package-lock.json +2 -2
- package/references/claude-code-acp/package.json +1 -1
- package/references/claude-code-acp/src/acp-agent.ts +3 -6
- package/src/acp/__tests__/history.test.ts +8 -4
- package/src/acp/macro-agent.ts +60 -26
- package/src/agent/__tests__/agent-manager.test.ts +4 -6
- package/src/agent/agent-manager.ts +24 -5
- package/src/map/adapter/__tests__/acp-over-map-cancel.test.ts +802 -0
- package/src/map/adapter/__tests__/acp-over-map-history.test.ts +1123 -0
- package/src/map/adapter/__tests__/acp-over-map-persistence.e2e.test.ts +440 -0
- package/src/map/adapter/acp-over-map.ts +282 -25
- package/src/map/adapter/map-adapter.ts +79 -9
- package/src/store/__tests__/event-store.test.ts +44 -12
- package/src/store/__tests__/instance.test.ts +5 -7
- package/src/store/event-store.ts +157 -57
- package/src/store/instance.ts +1 -29
- package/src/store/types/agents.ts +1 -0
package/src/store/event-store.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { createStore, Store } from 'tinybase';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
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
|
-
//
|
|
68
|
+
// Tabular better-sqlite3 Persister
|
|
69
69
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
|
-
*
|
|
73
|
-
* TinyBase
|
|
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
|
|
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
|
-
//
|
|
82
|
-
|
|
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
|
|
122
|
+
return createCustomSqlitePersister(
|
|
85
123
|
store,
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
//
|
|
97
|
-
(
|
|
98
|
-
//
|
|
99
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
|
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,
|
package/src/store/instance.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
// ─────────────────────────────────────────────────────────────────────────────
|