opencode-swarm-plugin 0.12.30 → 0.13.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/.beads/issues.jsonl +204 -10
- package/.opencode/skills/tdd/SKILL.md +182 -0
- package/README.md +165 -17
- package/bin/swarm.ts +120 -31
- package/bun.lock +23 -0
- package/dist/index.js +4020 -438
- package/dist/pglite.data +0 -0
- package/dist/pglite.wasm +0 -0
- package/dist/plugin.js +4008 -514
- package/examples/commands/swarm.md +114 -19
- package/examples/skills/beads-workflow/SKILL.md +75 -28
- package/examples/skills/swarm-coordination/SKILL.md +92 -1
- package/global-skills/testing-patterns/SKILL.md +430 -0
- package/global-skills/testing-patterns/references/dependency-breaking-catalog.md +586 -0
- package/package.json +11 -5
- package/src/index.ts +44 -5
- package/src/streams/agent-mail.test.ts +777 -0
- package/src/streams/agent-mail.ts +535 -0
- package/src/streams/debug.test.ts +500 -0
- package/src/streams/debug.ts +629 -0
- package/src/streams/effect/ask.integration.test.ts +314 -0
- package/src/streams/effect/ask.ts +202 -0
- package/src/streams/effect/cursor.integration.test.ts +418 -0
- package/src/streams/effect/cursor.ts +288 -0
- package/src/streams/effect/deferred.test.ts +357 -0
- package/src/streams/effect/deferred.ts +445 -0
- package/src/streams/effect/index.ts +17 -0
- package/src/streams/effect/layers.ts +73 -0
- package/src/streams/effect/lock.test.ts +385 -0
- package/src/streams/effect/lock.ts +399 -0
- package/src/streams/effect/mailbox.test.ts +260 -0
- package/src/streams/effect/mailbox.ts +318 -0
- package/src/streams/events.test.ts +628 -0
- package/src/streams/events.ts +214 -0
- package/src/streams/index.test.ts +229 -0
- package/src/streams/index.ts +492 -0
- package/src/streams/migrations.test.ts +355 -0
- package/src/streams/migrations.ts +269 -0
- package/src/streams/projections.test.ts +611 -0
- package/src/streams/projections.ts +302 -0
- package/src/streams/store.integration.test.ts +548 -0
- package/src/streams/store.ts +546 -0
- package/src/streams/swarm-mail.ts +552 -0
- package/src/swarm-mail.integration.test.ts +970 -0
- package/src/swarm-mail.ts +739 -0
- package/src/swarm.ts +84 -59
- package/src/tool-availability.ts +35 -2
- package/global-skills/mcp-tool-authoring/SKILL.md +0 -695
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event Store - Append-only event log with PGLite
|
|
3
|
+
*
|
|
4
|
+
* Core operations:
|
|
5
|
+
* - append(): Add events to the log
|
|
6
|
+
* - read(): Read events with filters
|
|
7
|
+
* - replay(): Rebuild state from events
|
|
8
|
+
*
|
|
9
|
+
* All state changes go through events. Projections compute current state.
|
|
10
|
+
*/
|
|
11
|
+
import { getDatabase } from "./index";
|
|
12
|
+
import {
|
|
13
|
+
type AgentEvent,
|
|
14
|
+
createEvent,
|
|
15
|
+
type AgentRegisteredEvent,
|
|
16
|
+
type MessageSentEvent,
|
|
17
|
+
type FileReservedEvent,
|
|
18
|
+
} from "./events";
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Event Store Operations
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Append an event to the log
|
|
26
|
+
*
|
|
27
|
+
* Also updates materialized views (agents, messages, reservations)
|
|
28
|
+
*/
|
|
29
|
+
export async function appendEvent(
|
|
30
|
+
event: AgentEvent,
|
|
31
|
+
projectPath?: string,
|
|
32
|
+
): Promise<AgentEvent & { id: number; sequence: number }> {
|
|
33
|
+
const db = await getDatabase(projectPath);
|
|
34
|
+
|
|
35
|
+
// Extract common fields
|
|
36
|
+
const { type, project_key, timestamp, ...rest } = event;
|
|
37
|
+
|
|
38
|
+
// Insert event
|
|
39
|
+
const result = await db.query<{ id: number; sequence: number }>(
|
|
40
|
+
`INSERT INTO events (type, project_key, timestamp, data)
|
|
41
|
+
VALUES ($1, $2, $3, $4)
|
|
42
|
+
RETURNING id, sequence`,
|
|
43
|
+
[type, project_key, timestamp, JSON.stringify(rest)],
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const row = result.rows[0];
|
|
47
|
+
if (!row) {
|
|
48
|
+
throw new Error("Failed to insert event - no row returned");
|
|
49
|
+
}
|
|
50
|
+
const { id, sequence } = row;
|
|
51
|
+
|
|
52
|
+
// Update materialized views based on event type
|
|
53
|
+
await updateMaterializedViews(db, { ...event, id, sequence });
|
|
54
|
+
|
|
55
|
+
return { ...event, id, sequence };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Append multiple events in a transaction
|
|
60
|
+
*/
|
|
61
|
+
export async function appendEvents(
|
|
62
|
+
events: AgentEvent[],
|
|
63
|
+
projectPath?: string,
|
|
64
|
+
): Promise<Array<AgentEvent & { id: number; sequence: number }>> {
|
|
65
|
+
const db = await getDatabase(projectPath);
|
|
66
|
+
const results: Array<AgentEvent & { id: number; sequence: number }> = [];
|
|
67
|
+
|
|
68
|
+
await db.exec("BEGIN");
|
|
69
|
+
try {
|
|
70
|
+
for (const event of events) {
|
|
71
|
+
const { type, project_key, timestamp, ...rest } = event;
|
|
72
|
+
|
|
73
|
+
const result = await db.query<{ id: number; sequence: number }>(
|
|
74
|
+
`INSERT INTO events (type, project_key, timestamp, data)
|
|
75
|
+
VALUES ($1, $2, $3, $4)
|
|
76
|
+
RETURNING id, sequence`,
|
|
77
|
+
[type, project_key, timestamp, JSON.stringify(rest)],
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const row = result.rows[0];
|
|
81
|
+
if (!row) {
|
|
82
|
+
throw new Error("Failed to insert event - no row returned");
|
|
83
|
+
}
|
|
84
|
+
const { id, sequence } = row;
|
|
85
|
+
const enrichedEvent = { ...event, id, sequence };
|
|
86
|
+
|
|
87
|
+
await updateMaterializedViews(db, enrichedEvent);
|
|
88
|
+
results.push(enrichedEvent);
|
|
89
|
+
}
|
|
90
|
+
await db.exec("COMMIT");
|
|
91
|
+
} catch (error) {
|
|
92
|
+
await db.exec("ROLLBACK");
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return results;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Read events with optional filters
|
|
101
|
+
*/
|
|
102
|
+
export async function readEvents(
|
|
103
|
+
options: {
|
|
104
|
+
projectKey?: string;
|
|
105
|
+
types?: AgentEvent["type"][];
|
|
106
|
+
since?: number; // timestamp
|
|
107
|
+
until?: number; // timestamp
|
|
108
|
+
afterSequence?: number;
|
|
109
|
+
limit?: number;
|
|
110
|
+
offset?: number;
|
|
111
|
+
} = {},
|
|
112
|
+
projectPath?: string,
|
|
113
|
+
): Promise<Array<AgentEvent & { id: number; sequence: number }>> {
|
|
114
|
+
const db = await getDatabase(projectPath);
|
|
115
|
+
|
|
116
|
+
const conditions: string[] = [];
|
|
117
|
+
const params: unknown[] = [];
|
|
118
|
+
let paramIndex = 1;
|
|
119
|
+
|
|
120
|
+
if (options.projectKey) {
|
|
121
|
+
conditions.push(`project_key = $${paramIndex++}`);
|
|
122
|
+
params.push(options.projectKey);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (options.types && options.types.length > 0) {
|
|
126
|
+
conditions.push(`type = ANY($${paramIndex++})`);
|
|
127
|
+
params.push(options.types);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (options.since !== undefined) {
|
|
131
|
+
conditions.push(`timestamp >= $${paramIndex++}`);
|
|
132
|
+
params.push(options.since);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (options.until !== undefined) {
|
|
136
|
+
conditions.push(`timestamp <= $${paramIndex++}`);
|
|
137
|
+
params.push(options.until);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (options.afterSequence !== undefined) {
|
|
141
|
+
conditions.push(`sequence > $${paramIndex++}`);
|
|
142
|
+
params.push(options.afterSequence);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const whereClause =
|
|
146
|
+
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
147
|
+
|
|
148
|
+
let query = `
|
|
149
|
+
SELECT id, type, project_key, timestamp, sequence, data
|
|
150
|
+
FROM events
|
|
151
|
+
${whereClause}
|
|
152
|
+
ORDER BY sequence ASC
|
|
153
|
+
`;
|
|
154
|
+
|
|
155
|
+
if (options.limit) {
|
|
156
|
+
query += ` LIMIT $${paramIndex++}`;
|
|
157
|
+
params.push(options.limit);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (options.offset) {
|
|
161
|
+
query += ` OFFSET $${paramIndex++}`;
|
|
162
|
+
params.push(options.offset);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const result = await db.query<{
|
|
166
|
+
id: number;
|
|
167
|
+
type: string;
|
|
168
|
+
project_key: string;
|
|
169
|
+
timestamp: string;
|
|
170
|
+
sequence: number;
|
|
171
|
+
data: string;
|
|
172
|
+
}>(query, params);
|
|
173
|
+
|
|
174
|
+
return result.rows.map((row) => {
|
|
175
|
+
const data = typeof row.data === "string" ? JSON.parse(row.data) : row.data;
|
|
176
|
+
return {
|
|
177
|
+
id: row.id,
|
|
178
|
+
type: row.type as AgentEvent["type"],
|
|
179
|
+
project_key: row.project_key,
|
|
180
|
+
timestamp: parseInt(row.timestamp as string),
|
|
181
|
+
sequence: row.sequence,
|
|
182
|
+
...data,
|
|
183
|
+
} as AgentEvent & { id: number; sequence: number };
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get the latest sequence number
|
|
189
|
+
*/
|
|
190
|
+
export async function getLatestSequence(
|
|
191
|
+
projectKey?: string,
|
|
192
|
+
projectPath?: string,
|
|
193
|
+
): Promise<number> {
|
|
194
|
+
const db = await getDatabase(projectPath);
|
|
195
|
+
|
|
196
|
+
const query = projectKey
|
|
197
|
+
? "SELECT MAX(sequence) as seq FROM events WHERE project_key = $1"
|
|
198
|
+
: "SELECT MAX(sequence) as seq FROM events";
|
|
199
|
+
|
|
200
|
+
const params = projectKey ? [projectKey] : [];
|
|
201
|
+
const result = await db.query<{ seq: number | null }>(query, params);
|
|
202
|
+
|
|
203
|
+
return result.rows[0]?.seq ?? 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Replay events to rebuild materialized views
|
|
208
|
+
*
|
|
209
|
+
* Useful for:
|
|
210
|
+
* - Recovering from corruption
|
|
211
|
+
* - Migrating to new schema
|
|
212
|
+
* - Debugging state issues
|
|
213
|
+
*/
|
|
214
|
+
export async function replayEvents(
|
|
215
|
+
options: {
|
|
216
|
+
projectKey?: string;
|
|
217
|
+
fromSequence?: number;
|
|
218
|
+
clearViews?: boolean;
|
|
219
|
+
} = {},
|
|
220
|
+
projectPath?: string,
|
|
221
|
+
): Promise<{ eventsReplayed: number; duration: number }> {
|
|
222
|
+
const startTime = Date.now();
|
|
223
|
+
const db = await getDatabase(projectPath);
|
|
224
|
+
|
|
225
|
+
// Optionally clear materialized views
|
|
226
|
+
if (options.clearViews) {
|
|
227
|
+
if (options.projectKey) {
|
|
228
|
+
// Use parameterized queries to prevent SQL injection
|
|
229
|
+
await db.query(
|
|
230
|
+
`DELETE FROM message_recipients WHERE message_id IN (
|
|
231
|
+
SELECT id FROM messages WHERE project_key = $1
|
|
232
|
+
)`,
|
|
233
|
+
[options.projectKey],
|
|
234
|
+
);
|
|
235
|
+
await db.query(`DELETE FROM messages WHERE project_key = $1`, [
|
|
236
|
+
options.projectKey,
|
|
237
|
+
]);
|
|
238
|
+
await db.query(`DELETE FROM reservations WHERE project_key = $1`, [
|
|
239
|
+
options.projectKey,
|
|
240
|
+
]);
|
|
241
|
+
await db.query(`DELETE FROM agents WHERE project_key = $1`, [
|
|
242
|
+
options.projectKey,
|
|
243
|
+
]);
|
|
244
|
+
} else {
|
|
245
|
+
await db.exec(`
|
|
246
|
+
DELETE FROM message_recipients;
|
|
247
|
+
DELETE FROM messages;
|
|
248
|
+
DELETE FROM reservations;
|
|
249
|
+
DELETE FROM agents;
|
|
250
|
+
`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Read all events
|
|
255
|
+
const events = await readEvents(
|
|
256
|
+
{
|
|
257
|
+
projectKey: options.projectKey,
|
|
258
|
+
afterSequence: options.fromSequence,
|
|
259
|
+
},
|
|
260
|
+
projectPath,
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
// Replay each event
|
|
264
|
+
for (const event of events) {
|
|
265
|
+
await updateMaterializedViews(db, event);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
eventsReplayed: events.length,
|
|
270
|
+
duration: Date.now() - startTime,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ============================================================================
|
|
275
|
+
// Materialized View Updates
|
|
276
|
+
// ============================================================================
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Update materialized views based on event type
|
|
280
|
+
*
|
|
281
|
+
* This is called after each event is appended.
|
|
282
|
+
* Views are denormalized for fast reads.
|
|
283
|
+
*/
|
|
284
|
+
async function updateMaterializedViews(
|
|
285
|
+
db: Awaited<ReturnType<typeof getDatabase>>,
|
|
286
|
+
event: AgentEvent & { id: number; sequence: number },
|
|
287
|
+
): Promise<void> {
|
|
288
|
+
switch (event.type) {
|
|
289
|
+
case "agent_registered":
|
|
290
|
+
await handleAgentRegistered(
|
|
291
|
+
db,
|
|
292
|
+
event as AgentRegisteredEvent & { id: number; sequence: number },
|
|
293
|
+
);
|
|
294
|
+
break;
|
|
295
|
+
|
|
296
|
+
case "agent_active":
|
|
297
|
+
await db.query(
|
|
298
|
+
`UPDATE agents SET last_active_at = $1 WHERE project_key = $2 AND name = $3`,
|
|
299
|
+
[event.timestamp, event.project_key, event.agent_name],
|
|
300
|
+
);
|
|
301
|
+
break;
|
|
302
|
+
|
|
303
|
+
case "message_sent":
|
|
304
|
+
await handleMessageSent(
|
|
305
|
+
db,
|
|
306
|
+
event as MessageSentEvent & { id: number; sequence: number },
|
|
307
|
+
);
|
|
308
|
+
break;
|
|
309
|
+
|
|
310
|
+
case "message_read":
|
|
311
|
+
await db.query(
|
|
312
|
+
`UPDATE message_recipients SET read_at = $1 WHERE message_id = $2 AND agent_name = $3`,
|
|
313
|
+
[event.timestamp, event.message_id, event.agent_name],
|
|
314
|
+
);
|
|
315
|
+
break;
|
|
316
|
+
|
|
317
|
+
case "message_acked":
|
|
318
|
+
await db.query(
|
|
319
|
+
`UPDATE message_recipients SET acked_at = $1 WHERE message_id = $2 AND agent_name = $3`,
|
|
320
|
+
[event.timestamp, event.message_id, event.agent_name],
|
|
321
|
+
);
|
|
322
|
+
break;
|
|
323
|
+
|
|
324
|
+
case "file_reserved":
|
|
325
|
+
await handleFileReserved(
|
|
326
|
+
db,
|
|
327
|
+
event as FileReservedEvent & { id: number; sequence: number },
|
|
328
|
+
);
|
|
329
|
+
break;
|
|
330
|
+
|
|
331
|
+
case "file_released":
|
|
332
|
+
await handleFileReleased(db, event);
|
|
333
|
+
break;
|
|
334
|
+
|
|
335
|
+
// Task events don't need materialized views (query events directly)
|
|
336
|
+
case "task_started":
|
|
337
|
+
case "task_progress":
|
|
338
|
+
case "task_completed":
|
|
339
|
+
case "task_blocked":
|
|
340
|
+
// No-op for now - could add task tracking table later
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function handleAgentRegistered(
|
|
346
|
+
db: Awaited<ReturnType<typeof getDatabase>>,
|
|
347
|
+
event: AgentRegisteredEvent & { id: number; sequence: number },
|
|
348
|
+
): Promise<void> {
|
|
349
|
+
await db.query(
|
|
350
|
+
`INSERT INTO agents (project_key, name, program, model, task_description, registered_at, last_active_at)
|
|
351
|
+
VALUES ($1, $2, $3, $4, $5, $6, $6)
|
|
352
|
+
ON CONFLICT (project_key, name) DO UPDATE SET
|
|
353
|
+
program = EXCLUDED.program,
|
|
354
|
+
model = EXCLUDED.model,
|
|
355
|
+
task_description = EXCLUDED.task_description,
|
|
356
|
+
last_active_at = EXCLUDED.last_active_at`,
|
|
357
|
+
[
|
|
358
|
+
event.project_key,
|
|
359
|
+
event.agent_name,
|
|
360
|
+
event.program,
|
|
361
|
+
event.model,
|
|
362
|
+
event.task_description || null,
|
|
363
|
+
event.timestamp,
|
|
364
|
+
],
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function handleMessageSent(
|
|
369
|
+
db: Awaited<ReturnType<typeof getDatabase>>,
|
|
370
|
+
event: MessageSentEvent & { id: number; sequence: number },
|
|
371
|
+
): Promise<void> {
|
|
372
|
+
// Insert message
|
|
373
|
+
const result = await db.query<{ id: number }>(
|
|
374
|
+
`INSERT INTO messages (project_key, from_agent, subject, body, thread_id, importance, ack_required, created_at)
|
|
375
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
376
|
+
RETURNING id`,
|
|
377
|
+
[
|
|
378
|
+
event.project_key,
|
|
379
|
+
event.from_agent,
|
|
380
|
+
event.subject,
|
|
381
|
+
event.body,
|
|
382
|
+
event.thread_id || null,
|
|
383
|
+
event.importance,
|
|
384
|
+
event.ack_required,
|
|
385
|
+
event.timestamp,
|
|
386
|
+
],
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
const msgRow = result.rows[0];
|
|
390
|
+
if (!msgRow) {
|
|
391
|
+
throw new Error("Failed to insert message - no row returned");
|
|
392
|
+
}
|
|
393
|
+
const messageId = msgRow.id;
|
|
394
|
+
|
|
395
|
+
// Insert recipients
|
|
396
|
+
for (const agent of event.to_agents) {
|
|
397
|
+
await db.query(
|
|
398
|
+
`INSERT INTO message_recipients (message_id, agent_name)
|
|
399
|
+
VALUES ($1, $2)
|
|
400
|
+
ON CONFLICT DO NOTHING`,
|
|
401
|
+
[messageId, agent],
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function handleFileReserved(
|
|
407
|
+
db: Awaited<ReturnType<typeof getDatabase>>,
|
|
408
|
+
event: FileReservedEvent & { id: number; sequence: number },
|
|
409
|
+
): Promise<void> {
|
|
410
|
+
for (const path of event.paths) {
|
|
411
|
+
await db.query(
|
|
412
|
+
`INSERT INTO reservations (project_key, agent_name, path_pattern, exclusive, reason, created_at, expires_at)
|
|
413
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
|
414
|
+
[
|
|
415
|
+
event.project_key,
|
|
416
|
+
event.agent_name,
|
|
417
|
+
path,
|
|
418
|
+
event.exclusive,
|
|
419
|
+
event.reason || null,
|
|
420
|
+
event.timestamp,
|
|
421
|
+
event.expires_at,
|
|
422
|
+
],
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function handleFileReleased(
|
|
428
|
+
db: Awaited<ReturnType<typeof getDatabase>>,
|
|
429
|
+
event: AgentEvent & { id: number; sequence: number },
|
|
430
|
+
): Promise<void> {
|
|
431
|
+
if (event.type !== "file_released") return;
|
|
432
|
+
|
|
433
|
+
if (event.reservation_ids && event.reservation_ids.length > 0) {
|
|
434
|
+
// Release specific reservations
|
|
435
|
+
await db.query(
|
|
436
|
+
`UPDATE reservations SET released_at = $1 WHERE id = ANY($2)`,
|
|
437
|
+
[event.timestamp, event.reservation_ids],
|
|
438
|
+
);
|
|
439
|
+
} else if (event.paths && event.paths.length > 0) {
|
|
440
|
+
// Release by path
|
|
441
|
+
await db.query(
|
|
442
|
+
`UPDATE reservations SET released_at = $1
|
|
443
|
+
WHERE project_key = $2 AND agent_name = $3 AND path_pattern = ANY($4) AND released_at IS NULL`,
|
|
444
|
+
[event.timestamp, event.project_key, event.agent_name, event.paths],
|
|
445
|
+
);
|
|
446
|
+
} else {
|
|
447
|
+
// Release all for agent
|
|
448
|
+
await db.query(
|
|
449
|
+
`UPDATE reservations SET released_at = $1
|
|
450
|
+
WHERE project_key = $2 AND agent_name = $3 AND released_at IS NULL`,
|
|
451
|
+
[event.timestamp, event.project_key, event.agent_name],
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ============================================================================
|
|
457
|
+
// Convenience Functions
|
|
458
|
+
// ============================================================================
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Register an agent (creates event + updates view)
|
|
462
|
+
*/
|
|
463
|
+
export async function registerAgent(
|
|
464
|
+
projectKey: string,
|
|
465
|
+
agentName: string,
|
|
466
|
+
options: {
|
|
467
|
+
program?: string;
|
|
468
|
+
model?: string;
|
|
469
|
+
taskDescription?: string;
|
|
470
|
+
} = {},
|
|
471
|
+
projectPath?: string,
|
|
472
|
+
): Promise<AgentRegisteredEvent & { id: number; sequence: number }> {
|
|
473
|
+
const event = createEvent("agent_registered", {
|
|
474
|
+
project_key: projectKey,
|
|
475
|
+
agent_name: agentName,
|
|
476
|
+
program: options.program || "opencode",
|
|
477
|
+
model: options.model || "unknown",
|
|
478
|
+
task_description: options.taskDescription,
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
return appendEvent(event, projectPath) as Promise<
|
|
482
|
+
AgentRegisteredEvent & { id: number; sequence: number }
|
|
483
|
+
>;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Send a message (creates event + updates view)
|
|
488
|
+
*/
|
|
489
|
+
export async function sendMessage(
|
|
490
|
+
projectKey: string,
|
|
491
|
+
fromAgent: string,
|
|
492
|
+
toAgents: string[],
|
|
493
|
+
subject: string,
|
|
494
|
+
body: string,
|
|
495
|
+
options: {
|
|
496
|
+
threadId?: string;
|
|
497
|
+
importance?: "low" | "normal" | "high" | "urgent";
|
|
498
|
+
ackRequired?: boolean;
|
|
499
|
+
} = {},
|
|
500
|
+
projectPath?: string,
|
|
501
|
+
): Promise<MessageSentEvent & { id: number; sequence: number }> {
|
|
502
|
+
const event = createEvent("message_sent", {
|
|
503
|
+
project_key: projectKey,
|
|
504
|
+
from_agent: fromAgent,
|
|
505
|
+
to_agents: toAgents,
|
|
506
|
+
subject,
|
|
507
|
+
body,
|
|
508
|
+
thread_id: options.threadId,
|
|
509
|
+
importance: options.importance || "normal",
|
|
510
|
+
ack_required: options.ackRequired || false,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
return appendEvent(event, projectPath) as Promise<
|
|
514
|
+
MessageSentEvent & { id: number; sequence: number }
|
|
515
|
+
>;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Reserve files (creates event + updates view)
|
|
520
|
+
*/
|
|
521
|
+
export async function reserveFiles(
|
|
522
|
+
projectKey: string,
|
|
523
|
+
agentName: string,
|
|
524
|
+
paths: string[],
|
|
525
|
+
options: {
|
|
526
|
+
reason?: string;
|
|
527
|
+
exclusive?: boolean;
|
|
528
|
+
ttlSeconds?: number;
|
|
529
|
+
} = {},
|
|
530
|
+
projectPath?: string,
|
|
531
|
+
): Promise<FileReservedEvent & { id: number; sequence: number }> {
|
|
532
|
+
const ttlSeconds = options.ttlSeconds || 3600;
|
|
533
|
+
const event = createEvent("file_reserved", {
|
|
534
|
+
project_key: projectKey,
|
|
535
|
+
agent_name: agentName,
|
|
536
|
+
paths,
|
|
537
|
+
reason: options.reason,
|
|
538
|
+
exclusive: options.exclusive ?? true,
|
|
539
|
+
ttl_seconds: ttlSeconds,
|
|
540
|
+
expires_at: Date.now() + ttlSeconds * 1000,
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
return appendEvent(event, projectPath) as Promise<
|
|
544
|
+
FileReservedEvent & { id: number; sequence: number }
|
|
545
|
+
>;
|
|
546
|
+
}
|