opencode-swarm-plugin 0.42.5 → 0.42.6

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/src/index.ts CHANGED
@@ -515,6 +515,7 @@ export const allTools = {
515
515
  ...skillsTools,
516
516
  ...mandateTools,
517
517
  ...memoryTools,
518
+ ...observabilityTools,
518
519
  } as const;
519
520
 
520
521
  /**
@@ -122,6 +122,7 @@ export const CellQueryArgsSchema = z.object({
122
122
  status: CellStatusSchema.optional(),
123
123
  type: CellTypeSchema.optional(),
124
124
  ready: z.boolean().optional(),
125
+ parent_id: z.string().optional(),
125
126
  limit: z.number().int().positive().default(20),
126
127
  });
127
128
  export type CellQueryArgs = z.infer<typeof CellQueryArgsSchema>;
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Swarm Insights Data Layer Tests
3
+ *
4
+ * TDD: Red → Green → Refactor
5
+ */
6
+
7
+ import { describe, expect, test, beforeAll, afterAll } from "bun:test";
8
+ import {
9
+ getStrategyInsights,
10
+ getFileInsights,
11
+ getPatternInsights,
12
+ formatInsightsForPrompt,
13
+ type StrategyInsight,
14
+ type FileInsight,
15
+ type PatternInsight,
16
+ } from "./swarm-insights";
17
+ import { createInMemorySwarmMail, type SwarmMailAdapter } from "swarm-mail";
18
+
19
+ describe("swarm-insights data layer", () => {
20
+ let swarmMail: SwarmMailAdapter;
21
+
22
+ beforeAll(async () => {
23
+ swarmMail = await createInMemorySwarmMail("test-insights");
24
+ });
25
+
26
+ afterAll(async () => {
27
+ await swarmMail.close();
28
+ });
29
+
30
+ describe("getStrategyInsights", () => {
31
+ test("returns empty array when no data", async () => {
32
+ const insights = await getStrategyInsights(swarmMail, "test-task");
33
+ expect(insights).toEqual([]);
34
+ });
35
+
36
+ test("returns strategy success rates from outcomes", async () => {
37
+ // Seed some outcome events (id is auto-increment, timestamp is integer)
38
+ const db = await swarmMail.getDatabase();
39
+ const now = Date.now();
40
+ await db.query(
41
+ `INSERT INTO events (type, project_key, timestamp, data) VALUES
42
+ ('subtask_outcome', 'test', ?, ?),
43
+ ('subtask_outcome', 'test', ?, ?),
44
+ ('subtask_outcome', 'test', ?, ?)`,
45
+ [
46
+ now,
47
+ JSON.stringify({ strategy: "file-based", success: "true" }),
48
+ now,
49
+ JSON.stringify({ strategy: "file-based", success: "true" }),
50
+ now,
51
+ JSON.stringify({ strategy: "file-based", success: "false" }),
52
+ ],
53
+ );
54
+
55
+ const insights = await getStrategyInsights(swarmMail, "test-task");
56
+
57
+ expect(insights.length).toBeGreaterThan(0);
58
+ const fileBased = insights.find((i) => i.strategy === "file-based");
59
+ expect(fileBased).toBeDefined();
60
+ expect(fileBased?.successRate).toBeCloseTo(66.67, 0);
61
+ expect(fileBased?.totalAttempts).toBe(3);
62
+ });
63
+
64
+ test("includes recommendation based on success rate", async () => {
65
+ const insights = await getStrategyInsights(swarmMail, "test-task");
66
+ const fileBased = insights.find((i) => i.strategy === "file-based");
67
+
68
+ expect(fileBased?.recommendation).toBeDefined();
69
+ expect(typeof fileBased?.recommendation).toBe("string");
70
+ });
71
+ });
72
+
73
+ describe("getFileInsights", () => {
74
+ test("returns empty array for unknown files", async () => {
75
+ const insights = await getFileInsights(swarmMail, [
76
+ "src/unknown-file.ts",
77
+ ]);
78
+ expect(insights).toEqual([]);
79
+ });
80
+
81
+ test("returns past issues for known files", async () => {
82
+ // Seed some file-related events (id is auto-increment, timestamp is integer)
83
+ const db = await swarmMail.getDatabase();
84
+ const now = Date.now();
85
+ await db.query(
86
+ `INSERT INTO events (type, project_key, timestamp, data) VALUES
87
+ ('subtask_outcome', 'test', ?, ?)`,
88
+ [
89
+ now,
90
+ JSON.stringify({
91
+ files_touched: ["src/auth.ts"],
92
+ success: "false",
93
+ error_count: 2,
94
+ }),
95
+ ],
96
+ );
97
+
98
+ const insights = await getFileInsights(swarmMail, ["src/auth.ts"]);
99
+
100
+ expect(insights.length).toBeGreaterThan(0);
101
+ const authInsight = insights.find((i) => i.file === "src/auth.ts");
102
+ expect(authInsight).toBeDefined();
103
+ expect(authInsight?.failureCount).toBeGreaterThan(0);
104
+ });
105
+
106
+ test("includes gotchas from semantic memory", async () => {
107
+ // This would query semantic memory for file-specific learnings
108
+ const insights = await getFileInsights(swarmMail, ["src/auth.ts"]);
109
+
110
+ // Even if no gotchas, the structure should be correct
111
+ const authInsight = insights.find((i) => i.file === "src/auth.ts");
112
+ expect(authInsight?.gotchas).toBeDefined();
113
+ expect(Array.isArray(authInsight?.gotchas)).toBe(true);
114
+ });
115
+ });
116
+
117
+ describe("getPatternInsights", () => {
118
+ test("returns common failure patterns", async () => {
119
+ const insights = await getPatternInsights(swarmMail);
120
+
121
+ expect(Array.isArray(insights)).toBe(true);
122
+ // Structure check
123
+ if (insights.length > 0) {
124
+ expect(insights[0]).toHaveProperty("pattern");
125
+ expect(insights[0]).toHaveProperty("frequency");
126
+ expect(insights[0]).toHaveProperty("recommendation");
127
+ }
128
+ });
129
+
130
+ test("includes anti-patterns from learning system", async () => {
131
+ const insights = await getPatternInsights(swarmMail);
132
+
133
+ // Should include anti-patterns if any exist
134
+ expect(Array.isArray(insights)).toBe(true);
135
+ });
136
+ });
137
+
138
+ describe("formatInsightsForPrompt", () => {
139
+ test("formats strategy insights concisely", () => {
140
+ const strategies: StrategyInsight[] = [
141
+ {
142
+ strategy: "file-based",
143
+ successRate: 85,
144
+ totalAttempts: 20,
145
+ recommendation: "Preferred for this project",
146
+ },
147
+ {
148
+ strategy: "feature-based",
149
+ successRate: 60,
150
+ totalAttempts: 10,
151
+ recommendation: "Use with caution",
152
+ },
153
+ ];
154
+
155
+ const formatted = formatInsightsForPrompt({ strategies });
156
+
157
+ expect(formatted).toContain("file-based");
158
+ expect(formatted).toContain("85%");
159
+ expect(formatted.length).toBeLessThan(500); // Context-efficient
160
+ });
161
+
162
+ test("formats file insights concisely", () => {
163
+ const files: FileInsight[] = [
164
+ {
165
+ file: "src/auth.ts",
166
+ failureCount: 3,
167
+ lastFailure: "2025-12-25",
168
+ gotchas: ["Watch for race conditions in token refresh"],
169
+ },
170
+ ];
171
+
172
+ const formatted = formatInsightsForPrompt({ files });
173
+
174
+ expect(formatted).toContain("src/auth.ts");
175
+ expect(formatted).toContain("race conditions");
176
+ expect(formatted.length).toBeLessThan(300); // Per-file budget
177
+ });
178
+
179
+ test("formats pattern insights concisely", () => {
180
+ const patterns: PatternInsight[] = [
181
+ {
182
+ pattern: "Missing error handling",
183
+ frequency: 5,
184
+ recommendation: "Add try/catch around async operations",
185
+ },
186
+ ];
187
+
188
+ const formatted = formatInsightsForPrompt({ patterns });
189
+
190
+ expect(formatted).toContain("Missing error handling");
191
+ expect(formatted).toContain("try/catch");
192
+ });
193
+
194
+ test("respects token budget", () => {
195
+ // Create many insights
196
+ const strategies: StrategyInsight[] = Array.from({ length: 10 }, (_, i) => ({
197
+ strategy: `strategy-${i}`,
198
+ successRate: 50 + i * 5,
199
+ totalAttempts: 10,
200
+ recommendation: `Recommendation for strategy ${i}`,
201
+ }));
202
+
203
+ const formatted = formatInsightsForPrompt({ strategies }, { maxTokens: 200 });
204
+
205
+ // Should truncate to fit budget
206
+ expect(formatted.length).toBeLessThan(1000); // ~200 tokens ≈ 800 chars
207
+ });
208
+
209
+ test("returns empty string when no insights", () => {
210
+ const formatted = formatInsightsForPrompt({});
211
+ expect(formatted).toBe("");
212
+ });
213
+ });
214
+ });
@@ -0,0 +1,346 @@
1
+ /**
2
+ * Swarm Insights Data Layer
3
+ *
4
+ * Aggregates insights from swarm coordination for prompt injection.
5
+ * Provides concise, context-efficient summaries for coordinators and workers.
6
+ *
7
+ * Data sources:
8
+ * - Event store (subtask_outcome, eval_finalized)
9
+ * - Semantic memory (file-specific learnings)
10
+ * - Anti-pattern registry
11
+ */
12
+
13
+ import type { SwarmMailAdapter } from "swarm-mail";
14
+
15
+ // ============================================================================
16
+ // Types
17
+ // ============================================================================
18
+
19
+ export interface StrategyInsight {
20
+ strategy: string;
21
+ successRate: number;
22
+ totalAttempts: number;
23
+ recommendation: string;
24
+ }
25
+
26
+ export interface FileInsight {
27
+ file: string;
28
+ failureCount: number;
29
+ lastFailure: string | null;
30
+ gotchas: string[];
31
+ }
32
+
33
+ export interface PatternInsight {
34
+ pattern: string;
35
+ frequency: number;
36
+ recommendation: string;
37
+ }
38
+
39
+ export interface InsightsBundle {
40
+ strategies?: StrategyInsight[];
41
+ files?: FileInsight[];
42
+ patterns?: PatternInsight[];
43
+ }
44
+
45
+ export interface FormatOptions {
46
+ maxTokens?: number;
47
+ }
48
+
49
+ // ============================================================================
50
+ // Strategy Insights
51
+ // ============================================================================
52
+
53
+ /**
54
+ * Get strategy success rates and recommendations for a task.
55
+ *
56
+ * Queries the event store for subtask_outcome events and calculates
57
+ * success rates by strategy. Returns recommendations based on historical data.
58
+ */
59
+ export async function getStrategyInsights(
60
+ swarmMail: SwarmMailAdapter,
61
+ _task: string,
62
+ ): Promise<StrategyInsight[]> {
63
+ const db = await swarmMail.getDatabase();
64
+
65
+ const query = `
66
+ SELECT
67
+ json_extract(data, '$.strategy') as strategy,
68
+ COUNT(*) as total_attempts,
69
+ SUM(CASE WHEN json_extract(data, '$.success') = 'true' THEN 1 ELSE 0 END) as successes
70
+ FROM events
71
+ WHERE type = 'subtask_outcome'
72
+ AND json_extract(data, '$.strategy') IS NOT NULL
73
+ GROUP BY json_extract(data, '$.strategy')
74
+ ORDER BY total_attempts DESC
75
+ `;
76
+
77
+ const result = await db.query(query, []);
78
+ const rows = result.rows as Array<{
79
+ strategy: string;
80
+ total_attempts: number;
81
+ successes: number;
82
+ }>;
83
+
84
+ return rows.map((row) => {
85
+ const successRate = (row.successes / row.total_attempts) * 100;
86
+ return {
87
+ strategy: row.strategy,
88
+ successRate: Math.round(successRate * 100) / 100,
89
+ totalAttempts: row.total_attempts,
90
+ recommendation: getStrategyRecommendation(row.strategy, successRate),
91
+ };
92
+ });
93
+ }
94
+
95
+ /**
96
+ * Generate recommendation based on strategy and success rate.
97
+ */
98
+ function getStrategyRecommendation(strategy: string, successRate: number): string {
99
+ if (successRate >= 80) {
100
+ return `${strategy} is performing well (${successRate.toFixed(0)}% success)`;
101
+ }
102
+ if (successRate >= 60) {
103
+ return `${strategy} is moderate - monitor for issues`;
104
+ }
105
+ if (successRate >= 40) {
106
+ return `${strategy} has low success - consider alternatives`;
107
+ }
108
+ return `AVOID ${strategy} - high failure rate (${successRate.toFixed(0)}%)`;
109
+ }
110
+
111
+ // ============================================================================
112
+ // File Insights
113
+ // ============================================================================
114
+
115
+ /**
116
+ * Get insights for specific files based on historical outcomes.
117
+ *
118
+ * Queries the event store for failures involving these files and
119
+ * semantic memory for file-specific gotchas.
120
+ */
121
+ export async function getFileInsights(
122
+ swarmMail: SwarmMailAdapter,
123
+ files: string[],
124
+ ): Promise<FileInsight[]> {
125
+ if (files.length === 0) return [];
126
+
127
+ const db = await swarmMail.getDatabase();
128
+ const insights: FileInsight[] = [];
129
+
130
+ for (const file of files) {
131
+ // Query for failures involving this file
132
+ const query = `
133
+ SELECT
134
+ COUNT(*) as failure_count,
135
+ MAX(timestamp) as last_failure
136
+ FROM events
137
+ WHERE type = 'subtask_outcome'
138
+ AND json_extract(data, '$.success') = 'false'
139
+ AND json_extract(data, '$.files_touched') LIKE ?
140
+ `;
141
+
142
+ const result = await db.query(query, [`%${file}%`]);
143
+ const row = result.rows[0] as {
144
+ failure_count: number;
145
+ last_failure: string | null;
146
+ };
147
+
148
+ if (row && row.failure_count > 0) {
149
+ // Query semantic memory for gotchas (simplified - would use actual memory search)
150
+ const gotchas = await getFileGotchas(swarmMail, file);
151
+
152
+ insights.push({
153
+ file,
154
+ failureCount: row.failure_count,
155
+ lastFailure: row.last_failure,
156
+ gotchas,
157
+ });
158
+ }
159
+ }
160
+
161
+ return insights;
162
+ }
163
+
164
+ /**
165
+ * Get gotchas for a file from semantic memory.
166
+ *
167
+ * In a full implementation, this would query the semantic memory
168
+ * for file-specific learnings. For now, returns empty array.
169
+ */
170
+ async function getFileGotchas(
171
+ _swarmMail: SwarmMailAdapter,
172
+ _file: string,
173
+ ): Promise<string[]> {
174
+ // TODO: Query semantic memory for file-specific learnings
175
+ // const memories = await semanticMemoryFind({ query: `file:${file}`, limit: 3 });
176
+ // return memories.map(m => m.summary);
177
+ return [];
178
+ }
179
+
180
+ // ============================================================================
181
+ // Pattern Insights
182
+ // ============================================================================
183
+
184
+ /**
185
+ * Get common failure patterns and anti-patterns.
186
+ *
187
+ * Analyzes event store for recurring failure patterns and
188
+ * queries the anti-pattern registry.
189
+ */
190
+ export async function getPatternInsights(
191
+ swarmMail: SwarmMailAdapter,
192
+ ): Promise<PatternInsight[]> {
193
+ const db = await swarmMail.getDatabase();
194
+ const patterns: PatternInsight[] = [];
195
+
196
+ // Query for common error patterns
197
+ const query = `
198
+ SELECT
199
+ json_extract(data, '$.error_type') as error_type,
200
+ COUNT(*) as frequency
201
+ FROM events
202
+ WHERE type = 'subtask_outcome'
203
+ AND json_extract(data, '$.success') = 'false'
204
+ AND json_extract(data, '$.error_type') IS NOT NULL
205
+ GROUP BY json_extract(data, '$.error_type')
206
+ HAVING COUNT(*) >= 2
207
+ ORDER BY frequency DESC
208
+ LIMIT 5
209
+ `;
210
+
211
+ const result = await db.query(query, []);
212
+ const rows = result.rows as Array<{
213
+ error_type: string;
214
+ frequency: number;
215
+ }>;
216
+
217
+ for (const row of rows) {
218
+ patterns.push({
219
+ pattern: row.error_type,
220
+ frequency: row.frequency,
221
+ recommendation: getPatternRecommendation(row.error_type),
222
+ });
223
+ }
224
+
225
+ return patterns;
226
+ }
227
+
228
+ /**
229
+ * Generate recommendation for a failure pattern.
230
+ */
231
+ function getPatternRecommendation(errorType: string): string {
232
+ // Common patterns and their recommendations
233
+ const recommendations: Record<string, string> = {
234
+ type_error: "Add explicit type annotations and null checks",
235
+ timeout: "Consider breaking into smaller tasks",
236
+ conflict: "Check file reservations before editing",
237
+ test_failure: "Run tests incrementally during implementation",
238
+ };
239
+
240
+ return recommendations[errorType] || `Address ${errorType} issues`;
241
+ }
242
+
243
+ // ============================================================================
244
+ // Prompt Formatting
245
+ // ============================================================================
246
+
247
+ /**
248
+ * Format insights bundle for prompt injection.
249
+ *
250
+ * Produces a concise, context-efficient summary suitable for
251
+ * inclusion in coordinator or worker prompts.
252
+ *
253
+ * @param bundle - Insights to format
254
+ * @param options - Formatting options (maxTokens)
255
+ * @returns Formatted string for prompt injection
256
+ */
257
+ export function formatInsightsForPrompt(
258
+ bundle: InsightsBundle,
259
+ options: FormatOptions = {},
260
+ ): string {
261
+ const { maxTokens = 500 } = options;
262
+ const sections: string[] = [];
263
+
264
+ // Format strategy insights
265
+ if (bundle.strategies && bundle.strategies.length > 0) {
266
+ const strategyLines = bundle.strategies
267
+ .slice(0, 3) // Top 3 strategies
268
+ .map(
269
+ (s) =>
270
+ `- ${s.strategy}: ${s.successRate.toFixed(0)}% success (${s.totalAttempts} attempts)`,
271
+ );
272
+ sections.push(`**Strategy Performance:**\n${strategyLines.join("\n")}`);
273
+ }
274
+
275
+ // Format file insights
276
+ if (bundle.files && bundle.files.length > 0) {
277
+ const fileLines = bundle.files.slice(0, 5).map((f) => {
278
+ const gotchaStr =
279
+ f.gotchas.length > 0 ? ` - ${f.gotchas[0]}` : "";
280
+ return `- ${f.file}: ${f.failureCount} past failures${gotchaStr}`;
281
+ });
282
+ sections.push(`**File-Specific Gotchas:**\n${fileLines.join("\n")}`);
283
+ }
284
+
285
+ // Format pattern insights
286
+ if (bundle.patterns && bundle.patterns.length > 0) {
287
+ const patternLines = bundle.patterns
288
+ .slice(0, 3)
289
+ .map((p) => `- ${p.pattern} (${p.frequency}x): ${p.recommendation}`);
290
+ sections.push(`**Common Pitfalls:**\n${patternLines.join("\n")}`);
291
+ }
292
+
293
+ if (sections.length === 0) {
294
+ return "";
295
+ }
296
+
297
+ let result = sections.join("\n\n");
298
+
299
+ // Truncate to fit token budget (rough estimate: 4 chars per token)
300
+ const maxChars = maxTokens * 4;
301
+ if (result.length > maxChars) {
302
+ result = result.slice(0, maxChars - 3) + "...";
303
+ }
304
+
305
+ return result;
306
+ }
307
+
308
+ // ============================================================================
309
+ // Caching (for future optimization)
310
+ // ============================================================================
311
+
312
+ // Simple in-memory cache with TTL
313
+ const insightsCache = new Map<
314
+ string,
315
+ { data: InsightsBundle; expires: number }
316
+ >();
317
+ const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
318
+
319
+ /**
320
+ * Get cached insights or compute fresh ones.
321
+ */
322
+ export async function getCachedInsights(
323
+ _swarmMail: SwarmMailAdapter,
324
+ cacheKey: string,
325
+ computeFn: () => Promise<InsightsBundle>,
326
+ ): Promise<InsightsBundle> {
327
+ const cached = insightsCache.get(cacheKey);
328
+ if (cached && cached.expires > Date.now()) {
329
+ return cached.data;
330
+ }
331
+
332
+ const data = await computeFn();
333
+ insightsCache.set(cacheKey, {
334
+ data,
335
+ expires: Date.now() + CACHE_TTL_MS,
336
+ });
337
+
338
+ return data;
339
+ }
340
+
341
+ /**
342
+ * Clear the insights cache.
343
+ */
344
+ export function clearInsightsCache(): void {
345
+ insightsCache.clear();
346
+ }