@covibes/zeroshot 1.0.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.
- package/CHANGELOG.md +167 -0
- package/LICENSE +21 -0
- package/README.md +364 -0
- package/cli/index.js +3990 -0
- package/cluster-templates/base-templates/debug-workflow.json +181 -0
- package/cluster-templates/base-templates/full-workflow.json +455 -0
- package/cluster-templates/base-templates/single-worker.json +48 -0
- package/cluster-templates/base-templates/worker-validator.json +131 -0
- package/cluster-templates/conductor-bootstrap.json +122 -0
- package/cluster-templates/conductor-junior-bootstrap.json +69 -0
- package/docker/zeroshot-cluster/Dockerfile +132 -0
- package/lib/completion.js +174 -0
- package/lib/id-detector.js +53 -0
- package/lib/settings.js +97 -0
- package/lib/stream-json-parser.js +236 -0
- package/package.json +121 -0
- package/src/agent/agent-config.js +121 -0
- package/src/agent/agent-context-builder.js +241 -0
- package/src/agent/agent-hook-executor.js +329 -0
- package/src/agent/agent-lifecycle.js +555 -0
- package/src/agent/agent-stuck-detector.js +256 -0
- package/src/agent/agent-task-executor.js +1034 -0
- package/src/agent/agent-trigger-evaluator.js +67 -0
- package/src/agent-wrapper.js +459 -0
- package/src/agents/git-pusher-agent.json +20 -0
- package/src/attach/attach-client.js +438 -0
- package/src/attach/attach-server.js +543 -0
- package/src/attach/index.js +35 -0
- package/src/attach/protocol.js +220 -0
- package/src/attach/ring-buffer.js +121 -0
- package/src/attach/socket-discovery.js +242 -0
- package/src/claude-task-runner.js +468 -0
- package/src/config-router.js +80 -0
- package/src/config-validator.js +598 -0
- package/src/github.js +103 -0
- package/src/isolation-manager.js +1042 -0
- package/src/ledger.js +429 -0
- package/src/logic-engine.js +223 -0
- package/src/message-bus-bridge.js +139 -0
- package/src/message-bus.js +202 -0
- package/src/name-generator.js +232 -0
- package/src/orchestrator.js +1938 -0
- package/src/schemas/sub-cluster.js +156 -0
- package/src/sub-cluster-wrapper.js +545 -0
- package/src/task-runner.js +28 -0
- package/src/template-resolver.js +347 -0
- package/src/tui/CHANGES.txt +133 -0
- package/src/tui/LAYOUT.md +261 -0
- package/src/tui/README.txt +192 -0
- package/src/tui/TWO-LEVEL-NAVIGATION.md +186 -0
- package/src/tui/data-poller.js +325 -0
- package/src/tui/demo.js +208 -0
- package/src/tui/formatters.js +123 -0
- package/src/tui/index.js +193 -0
- package/src/tui/keybindings.js +383 -0
- package/src/tui/layout.js +317 -0
- package/src/tui/renderer.js +194 -0
package/src/ledger.js
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ledger - Immutable event log for multi-agent coordination
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - SQLite-backed message storage with indexes
|
|
6
|
+
* - Query API for message retrieval
|
|
7
|
+
* - In-memory cache for recent queries
|
|
8
|
+
* - Subscription mechanism for real-time updates
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const Database = require('better-sqlite3');
|
|
12
|
+
const EventEmitter = require('events');
|
|
13
|
+
const crypto = require('crypto');
|
|
14
|
+
|
|
15
|
+
class Ledger extends EventEmitter {
|
|
16
|
+
constructor(dbPath = ':memory:') {
|
|
17
|
+
super();
|
|
18
|
+
this.db = new Database(dbPath);
|
|
19
|
+
this.cache = new Map(); // LRU cache for queries
|
|
20
|
+
this.cacheLimit = 1000;
|
|
21
|
+
this._initSchema();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_initSchema() {
|
|
25
|
+
// Enable WAL mode for concurrent reads
|
|
26
|
+
this.db.pragma('journal_mode = WAL');
|
|
27
|
+
// Force synchronous writes so other processes see changes immediately
|
|
28
|
+
this.db.pragma('synchronous = NORMAL');
|
|
29
|
+
// Checkpoint WAL frequently for cross-process visibility
|
|
30
|
+
this.db.pragma('wal_autocheckpoint = 1');
|
|
31
|
+
|
|
32
|
+
// Create messages table
|
|
33
|
+
this.db.exec(`
|
|
34
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
35
|
+
id TEXT PRIMARY KEY,
|
|
36
|
+
timestamp INTEGER NOT NULL,
|
|
37
|
+
topic TEXT NOT NULL,
|
|
38
|
+
sender TEXT NOT NULL,
|
|
39
|
+
receiver TEXT NOT NULL,
|
|
40
|
+
content_text TEXT,
|
|
41
|
+
content_data TEXT,
|
|
42
|
+
metadata TEXT,
|
|
43
|
+
cluster_id TEXT NOT NULL
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
CREATE INDEX IF NOT EXISTS idx_timestamp ON messages(timestamp);
|
|
47
|
+
CREATE INDEX IF NOT EXISTS idx_topic ON messages(topic);
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_cluster_sender ON messages(cluster_id, sender);
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_cluster_topic ON messages(cluster_id, topic);
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_cluster_timestamp ON messages(cluster_id, timestamp);
|
|
51
|
+
`);
|
|
52
|
+
|
|
53
|
+
this._prepareStatements();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_prepareStatements() {
|
|
57
|
+
this.stmts = {
|
|
58
|
+
insert: this.db.prepare(`
|
|
59
|
+
INSERT INTO messages (id, timestamp, topic, sender, receiver, content_text, content_data, metadata, cluster_id)
|
|
60
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
61
|
+
`),
|
|
62
|
+
|
|
63
|
+
queryBase: `SELECT * FROM messages WHERE cluster_id = ?`,
|
|
64
|
+
|
|
65
|
+
count: this.db.prepare(`SELECT COUNT(*) as count FROM messages WHERE cluster_id = ?`),
|
|
66
|
+
|
|
67
|
+
getAll: this.db.prepare(`SELECT * FROM messages WHERE cluster_id = ? ORDER BY timestamp ASC`),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Append a message to the ledger
|
|
73
|
+
* @param {Object} message - Message object
|
|
74
|
+
* @returns {Object} The appended message with generated ID
|
|
75
|
+
*/
|
|
76
|
+
append(message) {
|
|
77
|
+
const id = message.id || `msg_${crypto.randomBytes(16).toString('hex')}`;
|
|
78
|
+
const timestamp = message.timestamp || Date.now();
|
|
79
|
+
|
|
80
|
+
const record = {
|
|
81
|
+
id,
|
|
82
|
+
timestamp,
|
|
83
|
+
topic: message.topic,
|
|
84
|
+
sender: message.sender,
|
|
85
|
+
receiver: message.receiver || 'broadcast',
|
|
86
|
+
content_text: message.content?.text || null,
|
|
87
|
+
content_data: message.content?.data ? JSON.stringify(message.content.data) : null,
|
|
88
|
+
metadata: message.metadata ? JSON.stringify(message.metadata) : null,
|
|
89
|
+
cluster_id: message.cluster_id,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
this.stmts.insert.run(
|
|
94
|
+
record.id,
|
|
95
|
+
record.timestamp,
|
|
96
|
+
record.topic,
|
|
97
|
+
record.sender,
|
|
98
|
+
record.receiver,
|
|
99
|
+
record.content_text,
|
|
100
|
+
record.content_data,
|
|
101
|
+
record.metadata,
|
|
102
|
+
record.cluster_id
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// Invalidate cache
|
|
106
|
+
this.cache.clear();
|
|
107
|
+
|
|
108
|
+
// Emit event for subscriptions
|
|
109
|
+
const fullMessage = this._deserializeMessage(record);
|
|
110
|
+
this.emit('message', fullMessage);
|
|
111
|
+
this.emit(`topic:${message.topic}`, fullMessage);
|
|
112
|
+
|
|
113
|
+
return fullMessage;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
throw new Error(`Failed to append message: ${error.message}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Query messages with filters
|
|
121
|
+
* @param {Object} criteria - Query criteria
|
|
122
|
+
* @returns {Array} Matching messages
|
|
123
|
+
*/
|
|
124
|
+
query(criteria) {
|
|
125
|
+
const { cluster_id, topic, sender, receiver, since, until, limit, offset } = criteria;
|
|
126
|
+
|
|
127
|
+
if (!cluster_id) {
|
|
128
|
+
throw new Error('cluster_id is required for queries');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Build query
|
|
132
|
+
const conditions = ['cluster_id = ?'];
|
|
133
|
+
const params = [cluster_id];
|
|
134
|
+
|
|
135
|
+
if (topic) {
|
|
136
|
+
conditions.push('topic = ?');
|
|
137
|
+
params.push(topic);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (sender) {
|
|
141
|
+
conditions.push('sender = ?');
|
|
142
|
+
params.push(sender);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (receiver) {
|
|
146
|
+
conditions.push('receiver = ?');
|
|
147
|
+
params.push(receiver);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (since) {
|
|
151
|
+
conditions.push('timestamp >= ?');
|
|
152
|
+
params.push(typeof since === 'number' ? since : new Date(since).getTime());
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (until) {
|
|
156
|
+
conditions.push('timestamp <= ?');
|
|
157
|
+
params.push(typeof until === 'number' ? until : new Date(until).getTime());
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let sql = `SELECT * FROM messages WHERE ${conditions.join(' AND ')} ORDER BY timestamp ASC`;
|
|
161
|
+
|
|
162
|
+
if (limit) {
|
|
163
|
+
sql += ` LIMIT ?`;
|
|
164
|
+
params.push(limit);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (offset) {
|
|
168
|
+
sql += ` OFFSET ?`;
|
|
169
|
+
params.push(offset);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const stmt = this.db.prepare(sql);
|
|
173
|
+
const rows = stmt.all(...params);
|
|
174
|
+
return rows.map((row) => this._deserializeMessage(row));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Find the last message matching criteria
|
|
179
|
+
* @param {Object} criteria - Query criteria
|
|
180
|
+
* @returns {Object|null} Last matching message
|
|
181
|
+
*/
|
|
182
|
+
findLast(criteria) {
|
|
183
|
+
const { cluster_id, topic, sender, receiver, since, until } = criteria;
|
|
184
|
+
|
|
185
|
+
if (!cluster_id) {
|
|
186
|
+
throw new Error('cluster_id is required for queries');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Build query with DESC order
|
|
190
|
+
const conditions = ['cluster_id = ?'];
|
|
191
|
+
const params = [cluster_id];
|
|
192
|
+
|
|
193
|
+
if (topic) {
|
|
194
|
+
conditions.push('topic = ?');
|
|
195
|
+
params.push(topic);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (sender) {
|
|
199
|
+
conditions.push('sender = ?');
|
|
200
|
+
params.push(sender);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (receiver) {
|
|
204
|
+
conditions.push('receiver = ?');
|
|
205
|
+
params.push(receiver);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (since) {
|
|
209
|
+
conditions.push('timestamp >= ?');
|
|
210
|
+
params.push(typeof since === 'number' ? since : new Date(since).getTime());
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (until) {
|
|
214
|
+
conditions.push('timestamp <= ?');
|
|
215
|
+
params.push(typeof until === 'number' ? until : new Date(until).getTime());
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const sql = `SELECT * FROM messages WHERE ${conditions.join(' AND ')} ORDER BY timestamp DESC LIMIT 1`;
|
|
219
|
+
|
|
220
|
+
const stmt = this.db.prepare(sql);
|
|
221
|
+
const row = stmt.get(...params);
|
|
222
|
+
return row ? this._deserializeMessage(row) : null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Count messages matching criteria
|
|
227
|
+
* @param {Object} criteria - Query criteria
|
|
228
|
+
* @returns {Number} Message count
|
|
229
|
+
*/
|
|
230
|
+
count(criteria) {
|
|
231
|
+
const { cluster_id, topic } = criteria;
|
|
232
|
+
|
|
233
|
+
if (!cluster_id) {
|
|
234
|
+
throw new Error('cluster_id is required for count');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let sql = 'SELECT COUNT(*) as count FROM messages WHERE cluster_id = ?';
|
|
238
|
+
const params = [cluster_id];
|
|
239
|
+
|
|
240
|
+
if (topic) {
|
|
241
|
+
sql += ' AND topic = ?';
|
|
242
|
+
params.push(topic);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const stmt = this.db.prepare(sql);
|
|
246
|
+
const result = stmt.get(...params);
|
|
247
|
+
return result.count;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Get messages since a specific timestamp
|
|
252
|
+
* @param {Object} params - { cluster_id, timestamp }
|
|
253
|
+
* @returns {Array} Messages since timestamp
|
|
254
|
+
*/
|
|
255
|
+
since(params) {
|
|
256
|
+
return this.query({
|
|
257
|
+
cluster_id: params.cluster_id,
|
|
258
|
+
since: params.timestamp,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Get all messages for a cluster
|
|
264
|
+
* @param {String} cluster_id - Cluster ID
|
|
265
|
+
* @returns {Array} All messages
|
|
266
|
+
*/
|
|
267
|
+
getAll(cluster_id) {
|
|
268
|
+
const rows = this.stmts.getAll.all(cluster_id);
|
|
269
|
+
return rows.map((row) => this._deserializeMessage(row));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Subscribe to new messages
|
|
274
|
+
* @param {Function} callback - Called with each new message
|
|
275
|
+
* @returns {Function} Unsubscribe function
|
|
276
|
+
*/
|
|
277
|
+
subscribe(callback) {
|
|
278
|
+
this.on('message', callback);
|
|
279
|
+
return () => this.off('message', callback);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Poll for new messages (cross-process support)
|
|
284
|
+
* @param {String} clusterId - Cluster ID to poll (null for all clusters)
|
|
285
|
+
* @param {Function} callback - Called with each new message
|
|
286
|
+
* @param {Number} intervalMs - Poll interval (default 500ms)
|
|
287
|
+
* @param {Number} initialCount - Number of messages to show initially (default 300)
|
|
288
|
+
* @returns {Function} Stop polling function
|
|
289
|
+
*/
|
|
290
|
+
pollForMessages(clusterId, callback, intervalMs = 500, initialCount = 300) {
|
|
291
|
+
let lastTimestamp = 0;
|
|
292
|
+
let lastMessageIds = new Set();
|
|
293
|
+
let isFirstPoll = true;
|
|
294
|
+
|
|
295
|
+
const poll = () => {
|
|
296
|
+
try {
|
|
297
|
+
let sql, params;
|
|
298
|
+
|
|
299
|
+
if (isFirstPoll) {
|
|
300
|
+
// First poll: get last N messages by count
|
|
301
|
+
if (clusterId) {
|
|
302
|
+
sql =
|
|
303
|
+
'SELECT * FROM (SELECT * FROM messages WHERE cluster_id = ? ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC';
|
|
304
|
+
params = [clusterId, initialCount];
|
|
305
|
+
} else {
|
|
306
|
+
sql =
|
|
307
|
+
'SELECT * FROM (SELECT * FROM messages ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC';
|
|
308
|
+
params = [initialCount];
|
|
309
|
+
}
|
|
310
|
+
isFirstPoll = false;
|
|
311
|
+
} else {
|
|
312
|
+
// Subsequent polls: get messages since last timestamp
|
|
313
|
+
if (clusterId) {
|
|
314
|
+
sql =
|
|
315
|
+
'SELECT * FROM messages WHERE cluster_id = ? AND timestamp >= ? ORDER BY timestamp ASC';
|
|
316
|
+
params = [clusterId, lastTimestamp - 1000]; // 1s buffer for race conditions
|
|
317
|
+
} else {
|
|
318
|
+
sql = 'SELECT * FROM messages WHERE timestamp >= ? ORDER BY timestamp ASC';
|
|
319
|
+
params = [lastTimestamp - 1000];
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const stmt = this.db.prepare(sql);
|
|
324
|
+
const rows = stmt.all(...params);
|
|
325
|
+
|
|
326
|
+
for (const row of rows) {
|
|
327
|
+
// Skip already-seen messages
|
|
328
|
+
if (lastMessageIds.has(row.id)) continue;
|
|
329
|
+
|
|
330
|
+
lastMessageIds.add(row.id);
|
|
331
|
+
const message = this._deserializeMessage(row);
|
|
332
|
+
callback(message);
|
|
333
|
+
|
|
334
|
+
// Update timestamp high-water mark
|
|
335
|
+
if (row.timestamp > lastTimestamp) {
|
|
336
|
+
lastTimestamp = row.timestamp;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Prune old message IDs to prevent memory leak
|
|
341
|
+
if (lastMessageIds.size > 10000) {
|
|
342
|
+
const idsArray = Array.from(lastMessageIds);
|
|
343
|
+
lastMessageIds = new Set(idsArray.slice(-5000));
|
|
344
|
+
}
|
|
345
|
+
} catch (error) {
|
|
346
|
+
// DB busy is expected during concurrent access - log but continue polling
|
|
347
|
+
// Other errors indicate real bugs and should be visible
|
|
348
|
+
console.error(`[Ledger] pollForMessages error (will retry): ${error.message}`);
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// Initial poll
|
|
353
|
+
poll();
|
|
354
|
+
|
|
355
|
+
// Set up interval
|
|
356
|
+
const intervalId = setInterval(poll, intervalMs);
|
|
357
|
+
|
|
358
|
+
// Return stop function
|
|
359
|
+
return () => clearInterval(intervalId);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Subscribe to specific topic
|
|
364
|
+
* @param {String} topic - Topic to subscribe to
|
|
365
|
+
* @param {Function} callback - Called with matching messages
|
|
366
|
+
* @returns {Function} Unsubscribe function
|
|
367
|
+
*/
|
|
368
|
+
subscribeTopic(topic, callback) {
|
|
369
|
+
const event = `topic:${topic}`;
|
|
370
|
+
this.on(event, callback);
|
|
371
|
+
return () => this.off(event, callback);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Deserialize a database row into a message object
|
|
376
|
+
* @private
|
|
377
|
+
*/
|
|
378
|
+
_deserializeMessage(row) {
|
|
379
|
+
const message = {
|
|
380
|
+
id: row.id,
|
|
381
|
+
timestamp: row.timestamp,
|
|
382
|
+
topic: row.topic,
|
|
383
|
+
sender: row.sender,
|
|
384
|
+
receiver: row.receiver,
|
|
385
|
+
cluster_id: row.cluster_id,
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
if (row.content_text || row.content_data) {
|
|
389
|
+
message.content = {};
|
|
390
|
+
if (row.content_text) {
|
|
391
|
+
message.content.text = row.content_text;
|
|
392
|
+
}
|
|
393
|
+
if (row.content_data) {
|
|
394
|
+
try {
|
|
395
|
+
message.content.data = JSON.parse(row.content_data);
|
|
396
|
+
} catch {
|
|
397
|
+
message.content.data = null;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (row.metadata) {
|
|
403
|
+
try {
|
|
404
|
+
message.metadata = JSON.parse(row.metadata);
|
|
405
|
+
} catch {
|
|
406
|
+
message.metadata = null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return message;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Close the database connection
|
|
415
|
+
*/
|
|
416
|
+
close() {
|
|
417
|
+
this.db.close();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Clear all messages (for testing)
|
|
422
|
+
*/
|
|
423
|
+
clear() {
|
|
424
|
+
this.db.exec('DELETE FROM messages');
|
|
425
|
+
this.cache.clear();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
module.exports = Ledger;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LogicEngine - JavaScript sandbox for agent decision logic
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - Isolated VM for executing agent trigger logic
|
|
6
|
+
* - Timeout enforcement (1 second)
|
|
7
|
+
* - Ledger API access for queries
|
|
8
|
+
* - Helper functions for common patterns
|
|
9
|
+
* - Sandbox security (no fs, network, child_process)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const vm = require('vm');
|
|
13
|
+
|
|
14
|
+
class LogicEngine {
|
|
15
|
+
constructor(messageBus, cluster) {
|
|
16
|
+
this.messageBus = messageBus;
|
|
17
|
+
this.cluster = cluster;
|
|
18
|
+
this.timeout = 1000; // 1 second
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Evaluate a trigger logic script
|
|
23
|
+
* @param {String} script - JavaScript code to evaluate
|
|
24
|
+
* @param {Object} agent - Agent context
|
|
25
|
+
* @param {Object} message - Triggering message
|
|
26
|
+
* @returns {Boolean} Whether agent should wake up
|
|
27
|
+
*/
|
|
28
|
+
evaluate(script, agent, message) {
|
|
29
|
+
try {
|
|
30
|
+
// Build sandbox context
|
|
31
|
+
const context = this._buildContext(agent, message);
|
|
32
|
+
|
|
33
|
+
// Create isolated context with frozen prototypes
|
|
34
|
+
// This prevents prototype pollution attacks
|
|
35
|
+
const isolatedContext = {};
|
|
36
|
+
|
|
37
|
+
// Freeze Object, Array, Function prototypes in the sandbox
|
|
38
|
+
isolatedContext.Object = Object.freeze({ ...Object });
|
|
39
|
+
isolatedContext.Array = Array;
|
|
40
|
+
isolatedContext.Function = Function;
|
|
41
|
+
|
|
42
|
+
// Copy safe context properties
|
|
43
|
+
Object.assign(isolatedContext, context);
|
|
44
|
+
|
|
45
|
+
// Wrap script to prevent prototype access
|
|
46
|
+
const wrappedScript = `(function() {
|
|
47
|
+
'use strict';
|
|
48
|
+
// Prevent prototype pollution
|
|
49
|
+
const frozenObject = Object;
|
|
50
|
+
const frozenArray = Array;
|
|
51
|
+
Object.freeze(frozenObject.prototype);
|
|
52
|
+
Object.freeze(frozenArray.prototype);
|
|
53
|
+
|
|
54
|
+
${script}
|
|
55
|
+
})()`;
|
|
56
|
+
|
|
57
|
+
// Create and run in context
|
|
58
|
+
vm.createContext(isolatedContext);
|
|
59
|
+
const result = vm.runInContext(wrappedScript, isolatedContext, {
|
|
60
|
+
timeout: this.timeout,
|
|
61
|
+
displayErrors: true,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Coerce to boolean
|
|
65
|
+
return Boolean(result);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(`Logic evaluation error for agent ${agent.id}:`, error.message);
|
|
68
|
+
return false; // Default to false on error
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Build sandbox context with APIs and helpers
|
|
74
|
+
* @private
|
|
75
|
+
*/
|
|
76
|
+
_buildContext(agent, message) {
|
|
77
|
+
const clusterId = agent.cluster_id;
|
|
78
|
+
|
|
79
|
+
// Ledger API wrapper (auto-scoped to cluster)
|
|
80
|
+
const ledgerAPI = {
|
|
81
|
+
query: (criteria) => {
|
|
82
|
+
return this.messageBus.query({ ...criteria, cluster_id: clusterId });
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
findLast: (criteria) => {
|
|
86
|
+
return this.messageBus.findLast({ ...criteria, cluster_id: clusterId });
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
count: (criteria) => {
|
|
90
|
+
return this.messageBus.count({ ...criteria, cluster_id: clusterId });
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
since: (timestamp) => {
|
|
94
|
+
return this.messageBus.since({ cluster_id: clusterId, timestamp });
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Helper functions
|
|
99
|
+
const helpers = {
|
|
100
|
+
/**
|
|
101
|
+
* Check if all agents have responded to a topic since a timestamp
|
|
102
|
+
*/
|
|
103
|
+
allResponded: (agents, topic, since) => {
|
|
104
|
+
const responses = ledgerAPI.query({ topic, since });
|
|
105
|
+
const responders = new Set(responses.map((r) => r.sender));
|
|
106
|
+
return agents.every((a) => responders.has(a.id || a));
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check if all responses have approved=true
|
|
111
|
+
*/
|
|
112
|
+
hasConsensus: (topic, since) => {
|
|
113
|
+
const responses = ledgerAPI.query({ topic, since });
|
|
114
|
+
if (responses.length === 0) return false;
|
|
115
|
+
return responses.every((r) => r.content?.data?.approved === true);
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get time since last message on topic (in milliseconds)
|
|
120
|
+
*/
|
|
121
|
+
timeSinceLastMessage: (topic) => {
|
|
122
|
+
const last = ledgerAPI.findLast({ topic });
|
|
123
|
+
if (!last) return Infinity;
|
|
124
|
+
return Date.now() - last.timestamp;
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Check if a topic has any messages since timestamp
|
|
129
|
+
*/
|
|
130
|
+
hasMessagesSince: (topic, since) => {
|
|
131
|
+
const count = ledgerAPI.count({ topic, since });
|
|
132
|
+
return count > 0;
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get cluster config based on complexity and task type
|
|
137
|
+
* Returns: { base: 'template-name', params: { ... } }
|
|
138
|
+
*/
|
|
139
|
+
getConfig: require('./config-router').getConfig,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Cluster API wrapper
|
|
143
|
+
const clusterAPI = {
|
|
144
|
+
id: clusterId,
|
|
145
|
+
|
|
146
|
+
getAgents: () => {
|
|
147
|
+
return this.cluster ? this.cluster.agents || [] : [];
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
getAgentsByRole: (role) => {
|
|
151
|
+
return this.cluster ? (this.cluster.agents || []).filter((a) => a.role === role) : [];
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
getAgent: (id) => {
|
|
155
|
+
return this.cluster ? (this.cluster.agents || []).find((a) => a.id === id) : null;
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Build context
|
|
160
|
+
return {
|
|
161
|
+
// Agent context
|
|
162
|
+
agent: {
|
|
163
|
+
id: agent.id,
|
|
164
|
+
role: agent.role,
|
|
165
|
+
iteration: agent.iteration || 0,
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
// Triggering message
|
|
169
|
+
message: message || null,
|
|
170
|
+
|
|
171
|
+
// APIs
|
|
172
|
+
ledger: ledgerAPI,
|
|
173
|
+
cluster: clusterAPI,
|
|
174
|
+
helpers,
|
|
175
|
+
|
|
176
|
+
// Safe built-ins
|
|
177
|
+
Set,
|
|
178
|
+
Map,
|
|
179
|
+
Array,
|
|
180
|
+
Object,
|
|
181
|
+
String,
|
|
182
|
+
Number,
|
|
183
|
+
Boolean,
|
|
184
|
+
Math,
|
|
185
|
+
Date,
|
|
186
|
+
JSON,
|
|
187
|
+
|
|
188
|
+
// No-op console (prevent output in production)
|
|
189
|
+
console: {
|
|
190
|
+
log: () => {},
|
|
191
|
+
error: () => {},
|
|
192
|
+
warn: () => {},
|
|
193
|
+
info: () => {},
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Validate script syntax without executing
|
|
200
|
+
* @param {String} script - JavaScript code
|
|
201
|
+
* @returns {Object} { valid: Boolean, error: String }
|
|
202
|
+
*/
|
|
203
|
+
validateScript(script) {
|
|
204
|
+
try {
|
|
205
|
+
// Wrap in function like evaluate() does
|
|
206
|
+
const wrappedScript = `(function() { ${script} })()`;
|
|
207
|
+
new vm.Script(wrappedScript);
|
|
208
|
+
return { valid: true, error: null };
|
|
209
|
+
} catch (error) {
|
|
210
|
+
return { valid: false, error: error.message };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Set timeout for script execution
|
|
216
|
+
* @param {Number} ms - Timeout in milliseconds
|
|
217
|
+
*/
|
|
218
|
+
setTimeout(ms) {
|
|
219
|
+
this.timeout = ms;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = LogicEngine;
|