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/.hive/issues.jsonl +8 -1
- package/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +18 -0
- package/dist/hive.d.ts +12 -0
- package/dist/hive.d.ts.map +1 -1
- package/dist/index.d.ts +86 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +236 -42
- package/dist/plugin.js +236 -42
- package/dist/schemas/cell.d.ts +2 -0
- package/dist/schemas/cell.d.ts.map +1 -1
- package/dist/swarm-insights.d.ts +78 -0
- package/dist/swarm-insights.d.ts.map +1 -0
- package/dist/swarm-prompts.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/hive.integration.test.ts +105 -0
- package/src/hive.ts +8 -0
- package/src/index.ts +1 -0
- package/src/schemas/cell.ts +1 -0
- package/src/swarm-insights.test.ts +214 -0
- package/src/swarm-insights.ts +346 -0
- package/src/swarm-prompts.test.ts +165 -0
- package/src/swarm-prompts.ts +74 -56
package/src/index.ts
CHANGED
package/src/schemas/cell.ts
CHANGED
|
@@ -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
|
+
}
|