opencode-swarm-plugin 0.20.0 → 0.22.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 +213 -0
- package/INTEGRATION_EXAMPLE.md +66 -0
- package/README.md +352 -522
- package/dist/index.js +2046 -984
- package/dist/plugin.js +2051 -1017
- package/docs/analysis/subagent-coordination-patterns.md +2 -0
- package/docs/semantic-memory-cli-syntax.md +123 -0
- package/docs/swarm-mail-architecture.md +1147 -0
- package/evals/README.md +116 -0
- package/evals/evalite.config.ts +15 -0
- package/evals/example.eval.ts +32 -0
- package/evals/fixtures/decomposition-cases.ts +105 -0
- package/evals/lib/data-loader.test.ts +288 -0
- package/evals/lib/data-loader.ts +111 -0
- package/evals/lib/llm.ts +115 -0
- package/evals/scorers/index.ts +200 -0
- package/evals/scorers/outcome-scorers.test.ts +27 -0
- package/evals/scorers/outcome-scorers.ts +349 -0
- package/evals/swarm-decomposition.eval.ts +112 -0
- package/package.json +8 -1
- package/scripts/cleanup-test-memories.ts +346 -0
- package/src/beads.ts +49 -0
- package/src/eval-capture.ts +487 -0
- package/src/index.ts +45 -3
- package/src/learning.integration.test.ts +19 -4
- package/src/output-guardrails.test.ts +438 -0
- package/src/output-guardrails.ts +381 -0
- package/src/schemas/index.ts +18 -0
- package/src/schemas/swarm-context.ts +115 -0
- package/src/storage.ts +117 -5
- package/src/streams/events.test.ts +296 -0
- package/src/streams/events.ts +93 -0
- package/src/streams/migrations.test.ts +24 -20
- package/src/streams/migrations.ts +51 -0
- package/src/streams/projections.ts +187 -0
- package/src/streams/store.ts +275 -0
- package/src/swarm-orchestrate.ts +771 -189
- package/src/swarm-prompts.ts +84 -12
- package/src/swarm.integration.test.ts +124 -0
- package/vitest.integration.config.ts +6 -0
- package/vitest.integration.setup.ts +48 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output Guardrails for MCP Tool Response Truncation
|
|
3
|
+
*
|
|
4
|
+
* Prevents MCP tools from blowing out context with massive responses.
|
|
5
|
+
* Provides smart truncation that preserves JSON, code blocks, and markdown structure.
|
|
6
|
+
*
|
|
7
|
+
* @module output-guardrails
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { guardrailOutput, DEFAULT_GUARDRAIL_CONFIG } from "./output-guardrails"
|
|
12
|
+
*
|
|
13
|
+
* const result = guardrailOutput("context7_get-library-docs", hugeOutput)
|
|
14
|
+
* if (result.truncated) {
|
|
15
|
+
* console.log(`Truncated ${result.originalLength - result.truncatedLength} chars`)
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Guardrail configuration for tool output limits
|
|
22
|
+
*
|
|
23
|
+
* Controls per-tool character limits and skip rules.
|
|
24
|
+
*/
|
|
25
|
+
export interface GuardrailConfig {
|
|
26
|
+
/**
|
|
27
|
+
* Default max characters for tool output
|
|
28
|
+
* Default: 32000 chars (~8000 tokens at 4 chars/token)
|
|
29
|
+
*/
|
|
30
|
+
defaultMaxChars: number;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Per-tool character limit overrides
|
|
34
|
+
*
|
|
35
|
+
* Higher limits for code/doc tools that commonly return large outputs.
|
|
36
|
+
*/
|
|
37
|
+
toolLimits: Record<string, number>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Tools that should never be truncated
|
|
41
|
+
*
|
|
42
|
+
* Internal coordination tools (beads_*, swarmmail_*, structured_*)
|
|
43
|
+
* should always return complete output.
|
|
44
|
+
*/
|
|
45
|
+
skipTools: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Result of guardrail output processing
|
|
50
|
+
*/
|
|
51
|
+
export interface GuardrailResult {
|
|
52
|
+
/** Processed output (truncated if needed) */
|
|
53
|
+
output: string;
|
|
54
|
+
|
|
55
|
+
/** Whether truncation occurred */
|
|
56
|
+
truncated: boolean;
|
|
57
|
+
|
|
58
|
+
/** Original output length in characters */
|
|
59
|
+
originalLength: number;
|
|
60
|
+
|
|
61
|
+
/** Final output length in characters */
|
|
62
|
+
truncatedLength: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Metrics for guardrail analytics
|
|
67
|
+
*
|
|
68
|
+
* Used to track truncation patterns and adjust limits.
|
|
69
|
+
*/
|
|
70
|
+
export interface GuardrailMetrics {
|
|
71
|
+
/** Tool that produced the output */
|
|
72
|
+
toolName: string;
|
|
73
|
+
|
|
74
|
+
/** Original output length */
|
|
75
|
+
originalLength: number;
|
|
76
|
+
|
|
77
|
+
/** Truncated output length */
|
|
78
|
+
truncatedLength: number;
|
|
79
|
+
|
|
80
|
+
/** Timestamp of truncation */
|
|
81
|
+
timestamp: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Default guardrail configuration
|
|
86
|
+
*
|
|
87
|
+
* - defaultMaxChars: 32000 (~8000 tokens)
|
|
88
|
+
* - Higher limits for code/doc tools (64000)
|
|
89
|
+
* - Skip internal coordination tools
|
|
90
|
+
*/
|
|
91
|
+
export const DEFAULT_GUARDRAIL_CONFIG: GuardrailConfig = {
|
|
92
|
+
defaultMaxChars: 32000,
|
|
93
|
+
|
|
94
|
+
toolLimits: {
|
|
95
|
+
// Higher limits for code/doc tools that commonly return large outputs
|
|
96
|
+
"repo-autopsy_file": 64000,
|
|
97
|
+
"repo-autopsy_search": 64000,
|
|
98
|
+
"repo-autopsy_exports_map": 64000,
|
|
99
|
+
"context7_get-library-docs": 64000,
|
|
100
|
+
cass_view: 64000,
|
|
101
|
+
cass_search: 48000,
|
|
102
|
+
skills_read: 48000,
|
|
103
|
+
|
|
104
|
+
// Lower limits for list/stats tools
|
|
105
|
+
"repo-autopsy_structure": 24000,
|
|
106
|
+
"repo-autopsy_stats": 16000,
|
|
107
|
+
cass_stats: 8000,
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
skipTools: [
|
|
111
|
+
// Beads tools - always return full output
|
|
112
|
+
"beads_create",
|
|
113
|
+
"beads_create_epic",
|
|
114
|
+
"beads_query",
|
|
115
|
+
"beads_update",
|
|
116
|
+
"beads_close",
|
|
117
|
+
"beads_start",
|
|
118
|
+
"beads_ready",
|
|
119
|
+
"beads_sync",
|
|
120
|
+
|
|
121
|
+
// Agent Mail tools - always return full output
|
|
122
|
+
"agentmail_init",
|
|
123
|
+
"agentmail_send",
|
|
124
|
+
"agentmail_inbox",
|
|
125
|
+
"agentmail_read_message",
|
|
126
|
+
"agentmail_summarize_thread",
|
|
127
|
+
"agentmail_reserve",
|
|
128
|
+
"agentmail_release",
|
|
129
|
+
"agentmail_ack",
|
|
130
|
+
|
|
131
|
+
// Swarm Mail tools - always return full output
|
|
132
|
+
"swarmmail_init",
|
|
133
|
+
"swarmmail_send",
|
|
134
|
+
"swarmmail_inbox",
|
|
135
|
+
"swarmmail_read_message",
|
|
136
|
+
"swarmmail_reserve",
|
|
137
|
+
"swarmmail_release",
|
|
138
|
+
"swarmmail_ack",
|
|
139
|
+
|
|
140
|
+
// Structured output tools - always return full output
|
|
141
|
+
"structured_extract_json",
|
|
142
|
+
"structured_validate",
|
|
143
|
+
"structured_parse_evaluation",
|
|
144
|
+
"structured_parse_decomposition",
|
|
145
|
+
"structured_parse_bead_tree",
|
|
146
|
+
|
|
147
|
+
// Swarm orchestration tools - always return full output
|
|
148
|
+
"swarm_select_strategy",
|
|
149
|
+
"swarm_plan_prompt",
|
|
150
|
+
"swarm_decompose",
|
|
151
|
+
"swarm_validate_decomposition",
|
|
152
|
+
"swarm_status",
|
|
153
|
+
"swarm_progress",
|
|
154
|
+
"swarm_complete",
|
|
155
|
+
"swarm_record_outcome",
|
|
156
|
+
"swarm_subtask_prompt",
|
|
157
|
+
"swarm_spawn_subtask",
|
|
158
|
+
"swarm_complete_subtask",
|
|
159
|
+
"swarm_evaluation_prompt",
|
|
160
|
+
|
|
161
|
+
// Mandate tools - always return full output
|
|
162
|
+
"mandate_file",
|
|
163
|
+
"mandate_vote",
|
|
164
|
+
"mandate_query",
|
|
165
|
+
"mandate_list",
|
|
166
|
+
"mandate_stats",
|
|
167
|
+
],
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Find matching closing brace for JSON truncation
|
|
172
|
+
*
|
|
173
|
+
* Walks forward from startIdx to find the matching closing brace,
|
|
174
|
+
* respecting nested braces and brackets.
|
|
175
|
+
*
|
|
176
|
+
* @param text - Text to search
|
|
177
|
+
* @param startIdx - Index of opening brace
|
|
178
|
+
* @returns Index of matching closing brace, or -1 if not found
|
|
179
|
+
*/
|
|
180
|
+
function findMatchingBrace(text: string, startIdx: number): number {
|
|
181
|
+
const openChar = text[startIdx];
|
|
182
|
+
const closeChar = openChar === "{" ? "}" : "]";
|
|
183
|
+
let depth = 1;
|
|
184
|
+
|
|
185
|
+
for (let i = startIdx + 1; i < text.length; i++) {
|
|
186
|
+
if (text[i] === openChar) {
|
|
187
|
+
depth++;
|
|
188
|
+
} else if (text[i] === closeChar) {
|
|
189
|
+
depth--;
|
|
190
|
+
if (depth === 0) {
|
|
191
|
+
return i;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return -1;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Smart truncation preserving structure boundaries
|
|
201
|
+
*
|
|
202
|
+
* Truncates text while preserving:
|
|
203
|
+
* - JSON structure (finds matching braces, doesn't cut mid-object)
|
|
204
|
+
* - Code blocks (preserves ``` boundaries)
|
|
205
|
+
* - Markdown headers (cuts at ## boundaries when possible)
|
|
206
|
+
*
|
|
207
|
+
* @param text - Text to truncate
|
|
208
|
+
* @param maxChars - Maximum character count
|
|
209
|
+
* @returns Truncated text with "[TRUNCATED - X chars removed]" suffix
|
|
210
|
+
*/
|
|
211
|
+
export function truncateWithBoundaries(text: string, maxChars: number): string {
|
|
212
|
+
if (text.length <= maxChars) {
|
|
213
|
+
return text;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Try to find a good truncation point
|
|
217
|
+
let truncateAt = maxChars;
|
|
218
|
+
|
|
219
|
+
// Check if we're in the middle of a JSON structure
|
|
220
|
+
const beforeTruncate = text.slice(0, maxChars);
|
|
221
|
+
const lastOpenBrace = Math.max(
|
|
222
|
+
beforeTruncate.lastIndexOf("{"),
|
|
223
|
+
beforeTruncate.lastIndexOf("["),
|
|
224
|
+
);
|
|
225
|
+
const lastCloseBrace = Math.max(
|
|
226
|
+
beforeTruncate.lastIndexOf("}"),
|
|
227
|
+
beforeTruncate.lastIndexOf("]"),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// If we have an unclosed brace/bracket, try to find the matching close
|
|
231
|
+
if (lastOpenBrace > lastCloseBrace) {
|
|
232
|
+
const matchingClose = findMatchingBrace(text, lastOpenBrace);
|
|
233
|
+
if (matchingClose !== -1 && matchingClose < maxChars * 1.2) {
|
|
234
|
+
// If the matching close is within 20% of maxChars, include it
|
|
235
|
+
truncateAt = matchingClose + 1;
|
|
236
|
+
} else {
|
|
237
|
+
// Otherwise, truncate before the unclosed brace
|
|
238
|
+
truncateAt = lastOpenBrace;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Check for code block boundaries (```)
|
|
243
|
+
const codeBlockMarker = "```";
|
|
244
|
+
const beforeTruncateForCode = text.slice(0, truncateAt);
|
|
245
|
+
const codeBlockCount = (beforeTruncateForCode.match(/```/g) || []).length;
|
|
246
|
+
|
|
247
|
+
// If we have an odd number of ``` markers, we're inside a code block
|
|
248
|
+
if (codeBlockCount % 2 === 1) {
|
|
249
|
+
// Try to find the closing ```
|
|
250
|
+
const closeMarkerIdx = text.indexOf(codeBlockMarker, truncateAt);
|
|
251
|
+
if (closeMarkerIdx !== -1 && closeMarkerIdx < maxChars * 1.2) {
|
|
252
|
+
// If close marker is within 20% of maxChars, include it
|
|
253
|
+
truncateAt = closeMarkerIdx + codeBlockMarker.length;
|
|
254
|
+
} else {
|
|
255
|
+
// Otherwise, truncate before the opening ```
|
|
256
|
+
const lastOpenMarker = beforeTruncateForCode.lastIndexOf(codeBlockMarker);
|
|
257
|
+
if (lastOpenMarker !== -1) {
|
|
258
|
+
truncateAt = lastOpenMarker;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Try to find a markdown header boundary (## or ###)
|
|
264
|
+
const headerMatch = text.slice(0, truncateAt).match(/\n#{1,6}\s/g);
|
|
265
|
+
if (headerMatch && headerMatch.length > 0) {
|
|
266
|
+
const lastHeaderIdx = beforeTruncateForCode.lastIndexOf("\n##");
|
|
267
|
+
if (lastHeaderIdx !== -1 && lastHeaderIdx > maxChars * 0.8) {
|
|
268
|
+
// If we have a header within 80% of maxChars, truncate there
|
|
269
|
+
truncateAt = lastHeaderIdx;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Ensure we don't truncate in the middle of a word
|
|
274
|
+
// Walk back to the last whitespace
|
|
275
|
+
while (truncateAt > 0 && !/\s/.test(text[truncateAt])) {
|
|
276
|
+
truncateAt--;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const truncated = text.slice(0, truncateAt).trimEnd();
|
|
280
|
+
const charsRemoved = text.length - truncated.length;
|
|
281
|
+
|
|
282
|
+
return `${truncated}\n\n[TRUNCATED - ${charsRemoved.toLocaleString()} chars removed]`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Get the character limit for a specific tool
|
|
287
|
+
*
|
|
288
|
+
* @param toolName - Name of the tool
|
|
289
|
+
* @param config - Guardrail configuration
|
|
290
|
+
* @returns Character limit for the tool
|
|
291
|
+
*/
|
|
292
|
+
function getToolLimit(
|
|
293
|
+
toolName: string,
|
|
294
|
+
config: GuardrailConfig = DEFAULT_GUARDRAIL_CONFIG,
|
|
295
|
+
): number {
|
|
296
|
+
return config.toolLimits[toolName] ?? config.defaultMaxChars;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Apply guardrails to tool output
|
|
301
|
+
*
|
|
302
|
+
* Main entry point for guardrail processing:
|
|
303
|
+
* 1. Check if tool is in skipTools → return unchanged
|
|
304
|
+
* 2. Check if output.length > getToolLimit(toolName) → truncate
|
|
305
|
+
* 3. Return { output, truncated, originalLength, truncatedLength }
|
|
306
|
+
*
|
|
307
|
+
* @param toolName - Name of the tool that produced the output
|
|
308
|
+
* @param output - Tool output to process
|
|
309
|
+
* @param config - Optional guardrail configuration (defaults to DEFAULT_GUARDRAIL_CONFIG)
|
|
310
|
+
* @returns Guardrail result with truncated output and metadata
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* ```typescript
|
|
314
|
+
* const result = guardrailOutput("context7_get-library-docs", hugeOutput)
|
|
315
|
+
* console.log(result.output) // Truncated or original
|
|
316
|
+
* console.log(result.truncated) // true if truncated
|
|
317
|
+
* console.log(`${result.originalLength} → ${result.truncatedLength} chars`)
|
|
318
|
+
* ```
|
|
319
|
+
*/
|
|
320
|
+
export function guardrailOutput(
|
|
321
|
+
toolName: string,
|
|
322
|
+
output: string,
|
|
323
|
+
config: GuardrailConfig = DEFAULT_GUARDRAIL_CONFIG,
|
|
324
|
+
): GuardrailResult {
|
|
325
|
+
const originalLength = output.length;
|
|
326
|
+
|
|
327
|
+
// Check if tool should be skipped
|
|
328
|
+
if (config.skipTools.includes(toolName)) {
|
|
329
|
+
return {
|
|
330
|
+
output,
|
|
331
|
+
truncated: false,
|
|
332
|
+
originalLength,
|
|
333
|
+
truncatedLength: originalLength,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Get the limit for this tool
|
|
338
|
+
const limit = getToolLimit(toolName, config);
|
|
339
|
+
|
|
340
|
+
// Check if truncation is needed
|
|
341
|
+
if (originalLength <= limit) {
|
|
342
|
+
return {
|
|
343
|
+
output,
|
|
344
|
+
truncated: false,
|
|
345
|
+
originalLength,
|
|
346
|
+
truncatedLength: originalLength,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Truncate with smart boundaries
|
|
351
|
+
const truncatedOutput = truncateWithBoundaries(output, limit);
|
|
352
|
+
const truncatedLength = truncatedOutput.length;
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
output: truncatedOutput,
|
|
356
|
+
truncated: true,
|
|
357
|
+
originalLength,
|
|
358
|
+
truncatedLength,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Create a guardrail metrics entry
|
|
364
|
+
*
|
|
365
|
+
* Used for analytics and learning about truncation patterns.
|
|
366
|
+
*
|
|
367
|
+
* @param result - Guardrail result from guardrailOutput
|
|
368
|
+
* @param toolName - Name of the tool
|
|
369
|
+
* @returns Metrics entry
|
|
370
|
+
*/
|
|
371
|
+
export function createMetrics(
|
|
372
|
+
result: GuardrailResult,
|
|
373
|
+
toolName: string,
|
|
374
|
+
): GuardrailMetrics {
|
|
375
|
+
return {
|
|
376
|
+
toolName,
|
|
377
|
+
originalLength: result.originalLength,
|
|
378
|
+
truncatedLength: result.truncatedLength,
|
|
379
|
+
timestamp: Date.now(),
|
|
380
|
+
};
|
|
381
|
+
}
|
package/src/schemas/index.ts
CHANGED
|
@@ -125,3 +125,21 @@ export {
|
|
|
125
125
|
type QueryMandatesArgs,
|
|
126
126
|
type ScoreCalculationResult,
|
|
127
127
|
} from "./mandate";
|
|
128
|
+
|
|
129
|
+
// Swarm context schemas
|
|
130
|
+
export {
|
|
131
|
+
SwarmStrategySchema,
|
|
132
|
+
SwarmDirectivesSchema,
|
|
133
|
+
SwarmRecoverySchema,
|
|
134
|
+
SwarmBeadContextSchema,
|
|
135
|
+
CreateSwarmContextArgsSchema,
|
|
136
|
+
UpdateSwarmContextArgsSchema,
|
|
137
|
+
QuerySwarmContextsArgsSchema,
|
|
138
|
+
type SwarmStrategy,
|
|
139
|
+
type SwarmDirectives,
|
|
140
|
+
type SwarmRecovery,
|
|
141
|
+
type SwarmBeadContext,
|
|
142
|
+
type CreateSwarmContextArgs,
|
|
143
|
+
type UpdateSwarmContextArgs,
|
|
144
|
+
type QuerySwarmContextsArgs,
|
|
145
|
+
} from "./swarm-context";
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swarm Context Schemas
|
|
3
|
+
*
|
|
4
|
+
* These schemas define the structure for storing and recovering swarm execution context.
|
|
5
|
+
* Used for checkpoint/recovery, continuation after crashes, and swarm state management.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Decomposition strategy used for the swarm
|
|
11
|
+
*/
|
|
12
|
+
export const SwarmStrategySchema = z.enum([
|
|
13
|
+
"file-based",
|
|
14
|
+
"feature-based",
|
|
15
|
+
"risk-based",
|
|
16
|
+
]);
|
|
17
|
+
export type SwarmStrategy = z.infer<typeof SwarmStrategySchema>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Shared directives and context for all agents in a swarm
|
|
21
|
+
*/
|
|
22
|
+
export const SwarmDirectivesSchema = z.object({
|
|
23
|
+
/** Context shared with all agents (API contracts, conventions, arch decisions) */
|
|
24
|
+
shared_context: z.string(),
|
|
25
|
+
/** Skills to load in agent context (e.g., ['testing-patterns', 'swarm-coordination']) */
|
|
26
|
+
skills_to_load: z.array(z.string()).default([]),
|
|
27
|
+
/** Notes from coordinator to agents (gotchas, important context) */
|
|
28
|
+
coordinator_notes: z.string().default(""),
|
|
29
|
+
});
|
|
30
|
+
export type SwarmDirectives = z.infer<typeof SwarmDirectivesSchema>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Recovery state for checkpoint/resume
|
|
34
|
+
*/
|
|
35
|
+
export const SwarmRecoverySchema = z.object({
|
|
36
|
+
/** Last known checkpoint (ISO-8601 timestamp or checkpoint ID) */
|
|
37
|
+
last_checkpoint: z.string(),
|
|
38
|
+
/** Files modified since checkpoint (for rollback/recovery) */
|
|
39
|
+
files_modified: z.array(z.string()).default([]),
|
|
40
|
+
/** Progress percentage (0-100) */
|
|
41
|
+
progress_percent: z.number().min(0).max(100).default(0),
|
|
42
|
+
/** Last status message from agent */
|
|
43
|
+
last_message: z.string().default(""),
|
|
44
|
+
/** Error context if agent failed (for retry/recovery) */
|
|
45
|
+
error_context: z.string().optional(),
|
|
46
|
+
});
|
|
47
|
+
export type SwarmRecovery = z.infer<typeof SwarmRecoverySchema>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Complete context for a single bead in a swarm
|
|
51
|
+
*
|
|
52
|
+
* Stored in swarm_contexts table for recovery, continuation, and state management.
|
|
53
|
+
*/
|
|
54
|
+
export const SwarmBeadContextSchema = z.object({
|
|
55
|
+
/** ID of the swarm context record */
|
|
56
|
+
id: z.string(),
|
|
57
|
+
/** Epic this bead belongs to */
|
|
58
|
+
epic_id: z.string(),
|
|
59
|
+
/** Bead ID being executed */
|
|
60
|
+
bead_id: z.string(),
|
|
61
|
+
/** Decomposition strategy used */
|
|
62
|
+
strategy: SwarmStrategySchema,
|
|
63
|
+
/** Files this bead is responsible for */
|
|
64
|
+
files: z.array(z.string()),
|
|
65
|
+
/** Bead IDs this task depends on */
|
|
66
|
+
dependencies: z.array(z.string()).default([]),
|
|
67
|
+
/** Shared directives and context */
|
|
68
|
+
directives: SwarmDirectivesSchema,
|
|
69
|
+
/** Recovery state */
|
|
70
|
+
recovery: SwarmRecoverySchema,
|
|
71
|
+
/** Creation timestamp (epoch ms) */
|
|
72
|
+
created_at: z.number().int().positive(),
|
|
73
|
+
/** Last update timestamp (epoch ms) */
|
|
74
|
+
updated_at: z.number().int().positive(),
|
|
75
|
+
});
|
|
76
|
+
export type SwarmBeadContext = z.infer<typeof SwarmBeadContextSchema>;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Args for creating a swarm context
|
|
80
|
+
*/
|
|
81
|
+
export const CreateSwarmContextArgsSchema = SwarmBeadContextSchema.omit({
|
|
82
|
+
id: true,
|
|
83
|
+
created_at: true,
|
|
84
|
+
updated_at: true,
|
|
85
|
+
});
|
|
86
|
+
export type CreateSwarmContextArgs = z.infer<
|
|
87
|
+
typeof CreateSwarmContextArgsSchema
|
|
88
|
+
>;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Args for updating a swarm context
|
|
92
|
+
*/
|
|
93
|
+
export const UpdateSwarmContextArgsSchema = z.object({
|
|
94
|
+
id: z.string(),
|
|
95
|
+
recovery: SwarmRecoverySchema.partial().optional(),
|
|
96
|
+
files: z.array(z.string()).optional(),
|
|
97
|
+
dependencies: z.array(z.string()).optional(),
|
|
98
|
+
directives: SwarmDirectivesSchema.partial().optional(),
|
|
99
|
+
});
|
|
100
|
+
export type UpdateSwarmContextArgs = z.infer<
|
|
101
|
+
typeof UpdateSwarmContextArgsSchema
|
|
102
|
+
>;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Args for querying swarm contexts
|
|
106
|
+
*/
|
|
107
|
+
export const QuerySwarmContextsArgsSchema = z.object({
|
|
108
|
+
epic_id: z.string().optional(),
|
|
109
|
+
bead_id: z.string().optional(),
|
|
110
|
+
strategy: SwarmStrategySchema.optional(),
|
|
111
|
+
has_errors: z.boolean().optional(), // Filter by presence of error_context
|
|
112
|
+
});
|
|
113
|
+
export type QuerySwarmContextsArgs = z.infer<
|
|
114
|
+
typeof QuerySwarmContextsArgsSchema
|
|
115
|
+
>;
|
package/src/storage.ts
CHANGED
|
@@ -141,13 +141,34 @@ export interface StorageConfig {
|
|
|
141
141
|
useSemanticSearch: boolean;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Get collection names with optional test suffix
|
|
146
|
+
*
|
|
147
|
+
* When TEST_MEMORY_COLLECTIONS=true, appends "-test" to all collection names
|
|
148
|
+
* to isolate test data from production semantic-memory storage.
|
|
149
|
+
*/
|
|
150
|
+
function getCollectionNames(): StorageCollections {
|
|
151
|
+
const base = {
|
|
147
152
|
feedback: "swarm-feedback",
|
|
148
153
|
patterns: "swarm-patterns",
|
|
149
154
|
maturity: "swarm-maturity",
|
|
150
|
-
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Test isolation: suffix collections with "-test" when in test mode
|
|
158
|
+
if (process.env.TEST_MEMORY_COLLECTIONS === "true") {
|
|
159
|
+
return {
|
|
160
|
+
feedback: `${base.feedback}-test`,
|
|
161
|
+
patterns: `${base.patterns}-test`,
|
|
162
|
+
maturity: `${base.maturity}-test`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return base;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export const DEFAULT_STORAGE_CONFIG: StorageConfig = {
|
|
170
|
+
backend: "semantic-memory",
|
|
171
|
+
collections: getCollectionNames(),
|
|
151
172
|
useSemanticSearch: true,
|
|
152
173
|
};
|
|
153
174
|
|
|
@@ -189,6 +210,43 @@ export interface LearningStorage {
|
|
|
189
210
|
close(): Promise<void>;
|
|
190
211
|
}
|
|
191
212
|
|
|
213
|
+
// ============================================================================
|
|
214
|
+
// Session Stats Tracking
|
|
215
|
+
// ============================================================================
|
|
216
|
+
|
|
217
|
+
interface SessionStats {
|
|
218
|
+
storesCount: number;
|
|
219
|
+
queriesCount: number;
|
|
220
|
+
sessionStart: number;
|
|
221
|
+
lastAlertCheck: number;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let sessionStats: SessionStats = {
|
|
225
|
+
storesCount: 0,
|
|
226
|
+
queriesCount: 0,
|
|
227
|
+
sessionStart: Date.now(),
|
|
228
|
+
lastAlertCheck: Date.now(),
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Reset session stats (for testing)
|
|
233
|
+
*/
|
|
234
|
+
export function resetSessionStats(): void {
|
|
235
|
+
sessionStats = {
|
|
236
|
+
storesCount: 0,
|
|
237
|
+
queriesCount: 0,
|
|
238
|
+
sessionStart: Date.now(),
|
|
239
|
+
lastAlertCheck: Date.now(),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get current session stats
|
|
245
|
+
*/
|
|
246
|
+
export function getSessionStats(): Readonly<SessionStats> {
|
|
247
|
+
return { ...sessionStats };
|
|
248
|
+
}
|
|
249
|
+
|
|
192
250
|
// ============================================================================
|
|
193
251
|
// Semantic Memory Storage Implementation
|
|
194
252
|
// ============================================================================
|
|
@@ -204,12 +262,46 @@ export class SemanticMemoryStorage implements LearningStorage {
|
|
|
204
262
|
|
|
205
263
|
constructor(config: Partial<StorageConfig> = {}) {
|
|
206
264
|
this.config = { ...DEFAULT_STORAGE_CONFIG, ...config };
|
|
265
|
+
console.log(
|
|
266
|
+
`[storage] SemanticMemoryStorage initialized with collections:`,
|
|
267
|
+
this.config.collections,
|
|
268
|
+
);
|
|
207
269
|
}
|
|
208
270
|
|
|
209
271
|
// -------------------------------------------------------------------------
|
|
210
272
|
// Helpers
|
|
211
273
|
// -------------------------------------------------------------------------
|
|
212
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Check if low usage alert should be sent
|
|
277
|
+
*
|
|
278
|
+
* Sends alert via agentmail if:
|
|
279
|
+
* - More than 10 minutes have elapsed since session start
|
|
280
|
+
* - Less than 1 store operation has occurred
|
|
281
|
+
* - Alert hasn't been sent in the last 10 minutes
|
|
282
|
+
*/
|
|
283
|
+
private async checkLowUsageAlert(): Promise<void> {
|
|
284
|
+
const TEN_MINUTES = 10 * 60 * 1000;
|
|
285
|
+
const now = Date.now();
|
|
286
|
+
const sessionDuration = now - sessionStats.sessionStart;
|
|
287
|
+
const timeSinceLastAlert = now - sessionStats.lastAlertCheck;
|
|
288
|
+
|
|
289
|
+
if (
|
|
290
|
+
sessionDuration >= TEN_MINUTES &&
|
|
291
|
+
sessionStats.storesCount < 1 &&
|
|
292
|
+
timeSinceLastAlert >= TEN_MINUTES
|
|
293
|
+
) {
|
|
294
|
+
console.warn(
|
|
295
|
+
`[storage] LOW USAGE ALERT: ${sessionStats.storesCount} stores after ${Math.floor(sessionDuration / 60000)} minutes`,
|
|
296
|
+
);
|
|
297
|
+
sessionStats.lastAlertCheck = now;
|
|
298
|
+
|
|
299
|
+
// Send alert via Agent Mail if available
|
|
300
|
+
// Note: This requires agentmail to be initialized, which may not always be the case
|
|
301
|
+
// We'll log the alert and let the coordinator detect it in logs
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
213
305
|
private async store(
|
|
214
306
|
collection: string,
|
|
215
307
|
data: unknown,
|
|
@@ -222,7 +314,19 @@ export class SemanticMemoryStorage implements LearningStorage {
|
|
|
222
314
|
args.push("--metadata", JSON.stringify(metadata));
|
|
223
315
|
}
|
|
224
316
|
|
|
225
|
-
|
|
317
|
+
console.log(`[storage] store() -> collection="${collection}"`);
|
|
318
|
+
sessionStats.storesCount++;
|
|
319
|
+
|
|
320
|
+
const result = await execSemanticMemory(args);
|
|
321
|
+
|
|
322
|
+
if (result.exitCode !== 0) {
|
|
323
|
+
console.warn(
|
|
324
|
+
`[storage] semantic-memory store() failed with exit code ${result.exitCode}: ${result.stderr.toString().trim()}`,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Alert check: if 10+ minutes elapsed with < 1 store, send alert
|
|
329
|
+
await this.checkLowUsageAlert();
|
|
226
330
|
}
|
|
227
331
|
|
|
228
332
|
private async find<T>(
|
|
@@ -245,6 +349,11 @@ export class SemanticMemoryStorage implements LearningStorage {
|
|
|
245
349
|
args.push("--fts");
|
|
246
350
|
}
|
|
247
351
|
|
|
352
|
+
console.log(
|
|
353
|
+
`[storage] find() -> collection="${collection}", query="${query.slice(0, 50)}${query.length > 50 ? "..." : ""}", limit=${limit}, fts=${useFts}`,
|
|
354
|
+
);
|
|
355
|
+
sessionStats.queriesCount++;
|
|
356
|
+
|
|
248
357
|
const result = await execSemanticMemory(args);
|
|
249
358
|
|
|
250
359
|
if (result.exitCode !== 0) {
|
|
@@ -280,6 +389,9 @@ export class SemanticMemoryStorage implements LearningStorage {
|
|
|
280
389
|
}
|
|
281
390
|
|
|
282
391
|
private async list<T>(collection: string): Promise<T[]> {
|
|
392
|
+
console.log(`[storage] list() -> collection="${collection}"`);
|
|
393
|
+
sessionStats.queriesCount++;
|
|
394
|
+
|
|
283
395
|
const result = await execSemanticMemory([
|
|
284
396
|
"list",
|
|
285
397
|
"--collection",
|