opencode-swarm-plugin 0.29.0 → 0.30.2
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/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +94 -0
- package/README.md +3 -6
- package/bin/swarm.test.ts +163 -0
- package/bin/swarm.ts +304 -72
- package/dist/hive.d.ts.map +1 -1
- package/dist/index.d.ts +94 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18825 -3469
- package/dist/memory-tools.d.ts +209 -0
- package/dist/memory-tools.d.ts.map +1 -0
- package/dist/memory.d.ts +124 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/plugin.js +18775 -3430
- package/dist/schemas/index.d.ts +7 -0
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/worker-handoff.d.ts +78 -0
- package/dist/schemas/worker-handoff.d.ts.map +1 -0
- package/dist/swarm-orchestrate.d.ts +50 -0
- package/dist/swarm-orchestrate.d.ts.map +1 -1
- package/dist/swarm-prompts.d.ts +1 -1
- package/dist/swarm-prompts.d.ts.map +1 -1
- package/dist/swarm-review.d.ts +4 -0
- package/dist/swarm-review.d.ts.map +1 -1
- package/docs/planning/ADR-008-worker-handoff-protocol.md +293 -0
- package/examples/plugin-wrapper-template.ts +157 -28
- package/package.json +3 -1
- package/src/hive.integration.test.ts +114 -0
- package/src/hive.ts +33 -22
- package/src/index.ts +41 -8
- package/src/memory-tools.test.ts +111 -0
- package/src/memory-tools.ts +273 -0
- package/src/memory.integration.test.ts +266 -0
- package/src/memory.test.ts +334 -0
- package/src/memory.ts +441 -0
- package/src/schemas/index.ts +18 -0
- package/src/schemas/worker-handoff.test.ts +271 -0
- package/src/schemas/worker-handoff.ts +131 -0
- package/src/swarm-orchestrate.ts +262 -24
- package/src/swarm-prompts.ts +48 -5
- package/src/swarm-review.ts +7 -0
- package/src/swarm.integration.test.ts +386 -9
package/src/memory.ts
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Module - Semantic Memory Adapter
|
|
3
|
+
*
|
|
4
|
+
* Provides a high-level adapter around swarm-mail's MemoryStore + Ollama.
|
|
5
|
+
* Used by semantic-memory_* tools in the plugin.
|
|
6
|
+
*
|
|
7
|
+
* ## Design
|
|
8
|
+
* - Wraps MemoryStore (vector storage) + Ollama (embeddings)
|
|
9
|
+
* - Handles ID generation, metadata parsing, error handling
|
|
10
|
+
* - Tool-friendly API (string inputs/outputs, no Effect-TS in signatures)
|
|
11
|
+
*
|
|
12
|
+
* ## Usage
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const adapter = await createMemoryAdapter(swarmMail.db);
|
|
15
|
+
*
|
|
16
|
+
* // Store memory
|
|
17
|
+
* const { id } = await adapter.store({
|
|
18
|
+
* information: "OAuth tokens need 5min buffer",
|
|
19
|
+
* tags: "auth,tokens",
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* // Search memories
|
|
23
|
+
* const results = await adapter.find({
|
|
24
|
+
* query: "token refresh",
|
|
25
|
+
* limit: 5,
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { Effect } from "effect";
|
|
31
|
+
import {
|
|
32
|
+
type DatabaseAdapter,
|
|
33
|
+
createMemoryStore,
|
|
34
|
+
getDefaultConfig,
|
|
35
|
+
makeOllamaLive,
|
|
36
|
+
Ollama,
|
|
37
|
+
type Memory,
|
|
38
|
+
type SearchResult,
|
|
39
|
+
legacyDatabaseExists,
|
|
40
|
+
migrateLegacyMemories,
|
|
41
|
+
} from "swarm-mail";
|
|
42
|
+
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// Auto-Migration State
|
|
45
|
+
// ============================================================================
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Module-level flag to track if migration has been checked.
|
|
49
|
+
* After first check, we skip the expensive legacy DB check.
|
|
50
|
+
*/
|
|
51
|
+
let migrationChecked = false;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Reset migration check flag (for testing)
|
|
55
|
+
* @internal
|
|
56
|
+
*/
|
|
57
|
+
export function resetMigrationCheck(): void {
|
|
58
|
+
migrationChecked = false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// Types
|
|
63
|
+
// ============================================================================
|
|
64
|
+
|
|
65
|
+
/** Arguments for store operation */
|
|
66
|
+
export interface StoreArgs {
|
|
67
|
+
readonly information: string;
|
|
68
|
+
readonly collection?: string;
|
|
69
|
+
readonly tags?: string;
|
|
70
|
+
readonly metadata?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Arguments for find operation */
|
|
74
|
+
export interface FindArgs {
|
|
75
|
+
readonly query: string;
|
|
76
|
+
readonly limit?: number;
|
|
77
|
+
readonly collection?: string;
|
|
78
|
+
readonly expand?: boolean;
|
|
79
|
+
readonly fts?: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Arguments for get/remove/validate operations */
|
|
83
|
+
export interface IdArgs {
|
|
84
|
+
readonly id: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Arguments for list operation */
|
|
88
|
+
export interface ListArgs {
|
|
89
|
+
readonly collection?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Result from store operation */
|
|
93
|
+
export interface StoreResult {
|
|
94
|
+
readonly id: string;
|
|
95
|
+
readonly message: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Result from find operation */
|
|
99
|
+
export interface FindResult {
|
|
100
|
+
readonly results: Array<{
|
|
101
|
+
readonly id: string;
|
|
102
|
+
readonly content: string;
|
|
103
|
+
readonly score: number;
|
|
104
|
+
readonly collection: string;
|
|
105
|
+
readonly metadata: Record<string, unknown>;
|
|
106
|
+
readonly createdAt: string;
|
|
107
|
+
}>;
|
|
108
|
+
readonly count: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Result from stats operation */
|
|
112
|
+
export interface StatsResult {
|
|
113
|
+
readonly memories: number;
|
|
114
|
+
readonly embeddings: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Result from health check */
|
|
118
|
+
export interface HealthResult {
|
|
119
|
+
readonly ollama: boolean;
|
|
120
|
+
readonly message?: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Result from validate/remove operations */
|
|
124
|
+
export interface OperationResult {
|
|
125
|
+
readonly success: boolean;
|
|
126
|
+
readonly message?: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ============================================================================
|
|
130
|
+
// Auto-Migration Logic
|
|
131
|
+
// ============================================================================
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check and auto-migrate legacy memories if conditions are met
|
|
135
|
+
*
|
|
136
|
+
* Conditions:
|
|
137
|
+
* 1. Legacy database exists
|
|
138
|
+
* 2. Target database has 0 memories (first use)
|
|
139
|
+
*
|
|
140
|
+
* @param db - Target database adapter
|
|
141
|
+
*/
|
|
142
|
+
async function maybeAutoMigrate(db: DatabaseAdapter): Promise<void> {
|
|
143
|
+
try {
|
|
144
|
+
// Check if legacy database exists
|
|
145
|
+
if (!legacyDatabaseExists()) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check if target database is empty
|
|
150
|
+
const countResult = await db.query<{ count: string }>(
|
|
151
|
+
"SELECT COUNT(*) as count FROM memories",
|
|
152
|
+
);
|
|
153
|
+
const memoryCount = parseInt(countResult.rows[0]?.count || "0");
|
|
154
|
+
|
|
155
|
+
if (memoryCount > 0) {
|
|
156
|
+
// Target already has memories, skip migration
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log("[memory] Legacy database detected, starting auto-migration...");
|
|
161
|
+
|
|
162
|
+
// Run migration
|
|
163
|
+
const result = await migrateLegacyMemories({
|
|
164
|
+
targetDb: db,
|
|
165
|
+
dryRun: false,
|
|
166
|
+
onProgress: console.log,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (result.migrated > 0) {
|
|
170
|
+
console.log(
|
|
171
|
+
`[memory] Auto-migrated ${result.migrated} memories from legacy database`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (result.failed > 0) {
|
|
176
|
+
console.warn(
|
|
177
|
+
`[memory] ${result.failed} memories failed to migrate. See errors above.`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
} catch (error) {
|
|
181
|
+
// Graceful degradation - log but don't throw
|
|
182
|
+
console.warn(
|
|
183
|
+
`[memory] Auto-migration failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// Memory Adapter
|
|
190
|
+
// ============================================================================
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Memory Adapter Interface
|
|
194
|
+
*
|
|
195
|
+
* High-level API for semantic memory operations.
|
|
196
|
+
*/
|
|
197
|
+
export interface MemoryAdapter {
|
|
198
|
+
readonly store: (args: StoreArgs) => Promise<StoreResult>;
|
|
199
|
+
readonly find: (args: FindArgs) => Promise<FindResult>;
|
|
200
|
+
readonly get: (args: IdArgs) => Promise<Memory | null>;
|
|
201
|
+
readonly remove: (args: IdArgs) => Promise<OperationResult>;
|
|
202
|
+
readonly validate: (args: IdArgs) => Promise<OperationResult>;
|
|
203
|
+
readonly list: (args: ListArgs) => Promise<Memory[]>;
|
|
204
|
+
readonly stats: () => Promise<StatsResult>;
|
|
205
|
+
readonly checkHealth: () => Promise<HealthResult>;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Create Memory Adapter
|
|
210
|
+
*
|
|
211
|
+
* @param db - DatabaseAdapter (from SwarmMail)
|
|
212
|
+
* @returns Memory adapter with high-level operations
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* ```typescript
|
|
216
|
+
* import { getSwarmMail } from 'swarm-mail';
|
|
217
|
+
* import { createMemoryAdapter } from './memory';
|
|
218
|
+
*
|
|
219
|
+
* const swarmMail = await getSwarmMail('/path/to/project');
|
|
220
|
+
* const adapter = await createMemoryAdapter(swarmMail.db);
|
|
221
|
+
*
|
|
222
|
+
* await adapter.store({ information: "Learning X" });
|
|
223
|
+
* const results = await adapter.find({ query: "X" });
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
export async function createMemoryAdapter(
|
|
227
|
+
db: DatabaseAdapter,
|
|
228
|
+
): Promise<MemoryAdapter> {
|
|
229
|
+
// Auto-migrate legacy memories on first use
|
|
230
|
+
if (!migrationChecked) {
|
|
231
|
+
migrationChecked = true;
|
|
232
|
+
await maybeAutoMigrate(db);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const store = createMemoryStore(db);
|
|
236
|
+
const config = getDefaultConfig();
|
|
237
|
+
const ollamaLayer = makeOllamaLive(config);
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Generate unique memory ID
|
|
241
|
+
*/
|
|
242
|
+
const generateId = (): string => {
|
|
243
|
+
const timestamp = Date.now().toString(36);
|
|
244
|
+
const random = Math.random().toString(36).substring(2, 9);
|
|
245
|
+
return `mem_${timestamp}_${random}`;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Parse tags string to metadata object
|
|
250
|
+
*/
|
|
251
|
+
const parseTags = (tags?: string): string[] => {
|
|
252
|
+
if (!tags) return [];
|
|
253
|
+
return tags
|
|
254
|
+
.split(",")
|
|
255
|
+
.map((t) => t.trim())
|
|
256
|
+
.filter(Boolean);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Truncate content for preview
|
|
261
|
+
*/
|
|
262
|
+
const truncateContent = (content: string, maxLength = 200): string => {
|
|
263
|
+
if (content.length <= maxLength) return content;
|
|
264
|
+
return `${content.substring(0, maxLength)}...`;
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
/**
|
|
269
|
+
* Store a memory with embedding
|
|
270
|
+
*/
|
|
271
|
+
async store(args: StoreArgs): Promise<StoreResult> {
|
|
272
|
+
const id = generateId();
|
|
273
|
+
const tags = parseTags(args.tags);
|
|
274
|
+
const collection = args.collection ?? "default";
|
|
275
|
+
|
|
276
|
+
// Parse metadata if provided
|
|
277
|
+
let metadata: Record<string, unknown> = {};
|
|
278
|
+
if (args.metadata) {
|
|
279
|
+
try {
|
|
280
|
+
metadata = JSON.parse(args.metadata);
|
|
281
|
+
} catch {
|
|
282
|
+
metadata = { raw: args.metadata };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Add tags to metadata
|
|
287
|
+
if (tags.length > 0) {
|
|
288
|
+
metadata.tags = tags;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const memory: Memory = {
|
|
292
|
+
id,
|
|
293
|
+
content: args.information,
|
|
294
|
+
metadata,
|
|
295
|
+
collection,
|
|
296
|
+
createdAt: new Date(),
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// Generate embedding
|
|
300
|
+
const program = Effect.gen(function* () {
|
|
301
|
+
const ollama = yield* Ollama;
|
|
302
|
+
return yield* ollama.embed(args.information);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const embedding = await Effect.runPromise(
|
|
306
|
+
program.pipe(Effect.provide(ollamaLayer)),
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
// Store memory
|
|
310
|
+
await store.store(memory, embedding);
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
id,
|
|
314
|
+
message: `Stored memory ${id} in collection: ${collection}`,
|
|
315
|
+
};
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Find memories by semantic similarity or full-text search
|
|
320
|
+
*/
|
|
321
|
+
async find(args: FindArgs): Promise<FindResult> {
|
|
322
|
+
const limit = args.limit ?? 10;
|
|
323
|
+
|
|
324
|
+
let results: SearchResult[];
|
|
325
|
+
|
|
326
|
+
if (args.fts) {
|
|
327
|
+
// Full-text search
|
|
328
|
+
results = await store.ftsSearch(args.query, {
|
|
329
|
+
limit,
|
|
330
|
+
collection: args.collection,
|
|
331
|
+
});
|
|
332
|
+
} else {
|
|
333
|
+
// Vector search - generate query embedding
|
|
334
|
+
const program = Effect.gen(function* () {
|
|
335
|
+
const ollama = yield* Ollama;
|
|
336
|
+
return yield* ollama.embed(args.query);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const queryEmbedding = await Effect.runPromise(
|
|
340
|
+
program.pipe(Effect.provide(ollamaLayer)),
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
results = await store.search(queryEmbedding, {
|
|
344
|
+
limit,
|
|
345
|
+
threshold: 0.3,
|
|
346
|
+
collection: args.collection,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
results: results.map((r) => ({
|
|
352
|
+
id: r.memory.id,
|
|
353
|
+
content: args.expand
|
|
354
|
+
? r.memory.content
|
|
355
|
+
: truncateContent(r.memory.content),
|
|
356
|
+
score: r.score,
|
|
357
|
+
collection: r.memory.collection,
|
|
358
|
+
metadata: r.memory.metadata,
|
|
359
|
+
createdAt: r.memory.createdAt.toISOString(),
|
|
360
|
+
})),
|
|
361
|
+
count: results.length,
|
|
362
|
+
};
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Get a single memory by ID
|
|
367
|
+
*/
|
|
368
|
+
async get(args: IdArgs): Promise<Memory | null> {
|
|
369
|
+
return store.get(args.id);
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Remove a memory
|
|
374
|
+
*/
|
|
375
|
+
async remove(args: IdArgs): Promise<OperationResult> {
|
|
376
|
+
await store.delete(args.id);
|
|
377
|
+
return {
|
|
378
|
+
success: true,
|
|
379
|
+
message: `Removed memory ${args.id}`,
|
|
380
|
+
};
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Validate a memory (reset decay timer)
|
|
385
|
+
*
|
|
386
|
+
* TODO: Implement decay tracking in MemoryStore
|
|
387
|
+
* For now, this is a no-op placeholder.
|
|
388
|
+
*/
|
|
389
|
+
async validate(args: IdArgs): Promise<OperationResult> {
|
|
390
|
+
const memory = await store.get(args.id);
|
|
391
|
+
if (!memory) {
|
|
392
|
+
return {
|
|
393
|
+
success: false,
|
|
394
|
+
message: `Memory ${args.id} not found`,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// TODO: Implement decay reset in MemoryStore
|
|
399
|
+
// For now, just verify it exists
|
|
400
|
+
return {
|
|
401
|
+
success: true,
|
|
402
|
+
message: `Memory ${args.id} validated`,
|
|
403
|
+
};
|
|
404
|
+
},
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* List memories
|
|
408
|
+
*/
|
|
409
|
+
async list(args: ListArgs): Promise<Memory[]> {
|
|
410
|
+
return store.list(args.collection);
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Get statistics
|
|
415
|
+
*/
|
|
416
|
+
async stats(): Promise<StatsResult> {
|
|
417
|
+
return store.getStats();
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Check Ollama health
|
|
422
|
+
*/
|
|
423
|
+
async checkHealth(): Promise<HealthResult> {
|
|
424
|
+
const program = Effect.gen(function* () {
|
|
425
|
+
const ollama = yield* Ollama;
|
|
426
|
+
return yield* ollama.checkHealth();
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
try {
|
|
430
|
+
await Effect.runPromise(program.pipe(Effect.provide(ollamaLayer)));
|
|
431
|
+
return { ollama: true };
|
|
432
|
+
} catch (error) {
|
|
433
|
+
return {
|
|
434
|
+
ollama: false,
|
|
435
|
+
message:
|
|
436
|
+
error instanceof Error ? error.message : "Ollama not available",
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
}
|
package/src/schemas/index.ts
CHANGED
|
@@ -26,6 +26,12 @@
|
|
|
26
26
|
* - `AgentProgressSchema` - Individual agent status
|
|
27
27
|
* - `SpawnedAgentSchema` - Spawned agent metadata
|
|
28
28
|
*
|
|
29
|
+
* ## Worker Handoff Schemas (Swarm Contracts)
|
|
30
|
+
* - `WorkerHandoffSchema` - Complete structured handoff contract
|
|
31
|
+
* - `WorkerHandoffContractSchema` - Task contract (files, criteria)
|
|
32
|
+
* - `WorkerHandoffContextSchema` - Narrative context (epic summary, role)
|
|
33
|
+
* - `WorkerHandoffEscalationSchema` - Escalation protocols
|
|
34
|
+
*
|
|
29
35
|
* @module schemas
|
|
30
36
|
*/
|
|
31
37
|
|
|
@@ -168,6 +174,18 @@ export {
|
|
|
168
174
|
type QuerySwarmContextsArgs,
|
|
169
175
|
} from "./swarm-context";
|
|
170
176
|
|
|
177
|
+
// Worker handoff schemas
|
|
178
|
+
export {
|
|
179
|
+
WorkerHandoffContractSchema,
|
|
180
|
+
WorkerHandoffContextSchema,
|
|
181
|
+
WorkerHandoffEscalationSchema,
|
|
182
|
+
WorkerHandoffSchema,
|
|
183
|
+
type WorkerHandoff,
|
|
184
|
+
type WorkerHandoffContract,
|
|
185
|
+
type WorkerHandoffContext,
|
|
186
|
+
type WorkerHandoffEscalation,
|
|
187
|
+
} from "./worker-handoff";
|
|
188
|
+
|
|
171
189
|
// Cell event schemas (PRIMARY)
|
|
172
190
|
export {
|
|
173
191
|
BaseCellEventSchema,
|