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.
Files changed (41) hide show
  1. package/.beads/issues.jsonl +213 -0
  2. package/INTEGRATION_EXAMPLE.md +66 -0
  3. package/README.md +352 -522
  4. package/dist/index.js +2046 -984
  5. package/dist/plugin.js +2051 -1017
  6. package/docs/analysis/subagent-coordination-patterns.md +2 -0
  7. package/docs/semantic-memory-cli-syntax.md +123 -0
  8. package/docs/swarm-mail-architecture.md +1147 -0
  9. package/evals/README.md +116 -0
  10. package/evals/evalite.config.ts +15 -0
  11. package/evals/example.eval.ts +32 -0
  12. package/evals/fixtures/decomposition-cases.ts +105 -0
  13. package/evals/lib/data-loader.test.ts +288 -0
  14. package/evals/lib/data-loader.ts +111 -0
  15. package/evals/lib/llm.ts +115 -0
  16. package/evals/scorers/index.ts +200 -0
  17. package/evals/scorers/outcome-scorers.test.ts +27 -0
  18. package/evals/scorers/outcome-scorers.ts +349 -0
  19. package/evals/swarm-decomposition.eval.ts +112 -0
  20. package/package.json +8 -1
  21. package/scripts/cleanup-test-memories.ts +346 -0
  22. package/src/beads.ts +49 -0
  23. package/src/eval-capture.ts +487 -0
  24. package/src/index.ts +45 -3
  25. package/src/learning.integration.test.ts +19 -4
  26. package/src/output-guardrails.test.ts +438 -0
  27. package/src/output-guardrails.ts +381 -0
  28. package/src/schemas/index.ts +18 -0
  29. package/src/schemas/swarm-context.ts +115 -0
  30. package/src/storage.ts +117 -5
  31. package/src/streams/events.test.ts +296 -0
  32. package/src/streams/events.ts +93 -0
  33. package/src/streams/migrations.test.ts +24 -20
  34. package/src/streams/migrations.ts +51 -0
  35. package/src/streams/projections.ts +187 -0
  36. package/src/streams/store.ts +275 -0
  37. package/src/swarm-orchestrate.ts +771 -189
  38. package/src/swarm-prompts.ts +84 -12
  39. package/src/swarm.integration.test.ts +124 -0
  40. package/vitest.integration.config.ts +6 -0
  41. package/vitest.integration.setup.ts +48 -0
@@ -0,0 +1,438 @@
1
+ /**
2
+ * Tests for output-guardrails module
3
+ *
4
+ * Validates smart truncation preserves structure boundaries and respects tool limits.
5
+ */
6
+
7
+ import { describe, expect, test } from "bun:test";
8
+ import {
9
+ truncateWithBoundaries,
10
+ guardrailOutput,
11
+ createMetrics,
12
+ DEFAULT_GUARDRAIL_CONFIG,
13
+ type GuardrailConfig,
14
+ } from "./output-guardrails";
15
+
16
+ describe("truncateWithBoundaries", () => {
17
+ test("returns unchanged text when under limit", () => {
18
+ const text = "Hello, world!";
19
+ const result = truncateWithBoundaries(text, 100);
20
+ expect(result).toBe(text);
21
+ });
22
+
23
+ test("preserves complete JSON objects", () => {
24
+ const text = `{"users": [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]}`;
25
+ const result = truncateWithBoundaries(text, 40);
26
+
27
+ // Should truncate before the unclosed object
28
+ expect(result).toContain("[TRUNCATED");
29
+ expect(result).not.toContain('{"id": 2'); // Don't cut mid-object
30
+ });
31
+
32
+ test("preserves nested JSON structure", () => {
33
+ const text = JSON.stringify(
34
+ {
35
+ level1: {
36
+ level2: {
37
+ level3: {
38
+ data: "This is deeply nested data that will be truncated",
39
+ },
40
+ },
41
+ },
42
+ },
43
+ null,
44
+ 2,
45
+ );
46
+
47
+ const result = truncateWithBoundaries(text, 50);
48
+
49
+ // Should find matching braces or truncate before unclosed structure
50
+ expect(result).toContain("[TRUNCATED");
51
+
52
+ // Truncation logic will try to preserve structure when possible
53
+ // Main requirement: it should truncate and add the marker
54
+ const truncatedLength = result.split("[TRUNCATED")[0].length;
55
+ expect(truncatedLength).toBeLessThan(text.length);
56
+ });
57
+
58
+ test("preserves code block boundaries", () => {
59
+ const text = `
60
+ Here's some code:
61
+
62
+ \`\`\`typescript
63
+ function example() {
64
+ console.log("This is a long function");
65
+ console.log("With multiple lines");
66
+ console.log("That should be preserved");
67
+ }
68
+ \`\`\`
69
+
70
+ More text after.
71
+ `;
72
+
73
+ const result = truncateWithBoundaries(text, 80);
74
+
75
+ // Should either include closing ``` or truncate before opening ```
76
+ const backtickCount = (result.split("[TRUNCATED")[0].match(/```/g) || [])
77
+ .length;
78
+ expect(backtickCount % 2).toBe(0); // Even number of backticks
79
+ });
80
+
81
+ test("preserves markdown header boundaries", () => {
82
+ const text = `
83
+ # Main Title
84
+
85
+ Some intro text.
86
+
87
+ ## Section 1
88
+
89
+ Content for section 1 with lots of detail that will eventually get truncated.
90
+
91
+ ## Section 2
92
+
93
+ Content for section 2.
94
+
95
+ ### Subsection 2.1
96
+
97
+ More content here.
98
+ `;
99
+
100
+ const result = truncateWithBoundaries(text, 120);
101
+
102
+ // Should truncate at a header boundary when possible
103
+ expect(result).toContain("[TRUNCATED");
104
+
105
+ // Check if it truncated at a header boundary
106
+ const beforeTruncate = result.split("[TRUNCATED")[0];
107
+
108
+ // Either ends with a header or doesn't have headers in the range
109
+ const hasHeaders = beforeTruncate.includes("##");
110
+ if (hasHeaders && beforeTruncate.length > 100) {
111
+ // If we have headers and enough content, should end near a header
112
+ expect(beforeTruncate).toMatch(/\n\n$/); // Should end at paragraph boundary at least
113
+ }
114
+ });
115
+
116
+ test("handles text without structure boundaries", () => {
117
+ const text = "a".repeat(1000);
118
+ const result = truncateWithBoundaries(text, 100);
119
+
120
+ expect(result).toContain("[TRUNCATED");
121
+ expect(result.length).toBeLessThan(200); // Much shorter than original
122
+ });
123
+
124
+ test("adds truncation suffix with character count", () => {
125
+ const text = "a".repeat(1000);
126
+ const result = truncateWithBoundaries(text, 100);
127
+
128
+ expect(result).toMatch(/\[TRUNCATED - \d{1,3}(,\d{3})* chars removed\]/);
129
+ });
130
+
131
+ test("avoids truncating mid-word", () => {
132
+ const text = "The quick brown fox jumps over the lazy dog ".repeat(20);
133
+ const result = truncateWithBoundaries(text, 100);
134
+
135
+ const beforeTruncate = result.split("[TRUNCATED")[0];
136
+
137
+ // Should try to truncate at whitespace boundary when possible
138
+ // At minimum, it should truncate
139
+ expect(result).toContain("[TRUNCATED");
140
+ expect(beforeTruncate.length).toBeLessThan(text.length);
141
+ });
142
+
143
+ test("handles empty string", () => {
144
+ const result = truncateWithBoundaries("", 100);
145
+ expect(result).toBe("");
146
+ });
147
+
148
+ test("handles exact limit length", () => {
149
+ const text = "a".repeat(100);
150
+ const result = truncateWithBoundaries(text, 100);
151
+ expect(result).toBe(text);
152
+ });
153
+
154
+ test("handles just over limit", () => {
155
+ const text = "a".repeat(101);
156
+ const result = truncateWithBoundaries(text, 100);
157
+
158
+ expect(result).toContain("[TRUNCATED");
159
+ // Should remove at least some characters
160
+ const charsRemoved = text.length - result.split("[TRUNCATED")[0].length;
161
+ expect(charsRemoved).toBeGreaterThan(0);
162
+ });
163
+
164
+ test("extends limit by 20% to include matching braces", () => {
165
+ // Create a JSON object that ends just after the limit
166
+ const shortContent = '{"data": "x"}';
167
+ const padding = "x".repeat(85);
168
+ const text = padding + shortContent; // Total ~98 chars
169
+
170
+ const result = truncateWithBoundaries(text, 100);
171
+
172
+ // If the closing brace is within the 20% buffer (120 chars), should try to include it
173
+ // At minimum, the function should handle this gracefully
174
+ expect(result.length).toBeGreaterThan(0);
175
+
176
+ // If it truncated, should have the marker
177
+ if (result.length < text.length) {
178
+ expect(result).toContain("[TRUNCATED");
179
+ }
180
+ });
181
+
182
+ test("extends limit by 20% to include closing code block", () => {
183
+ const text = `${"x".repeat(85)}\n\`\`\`\ncode\n\`\`\``; // ~98 chars
184
+
185
+ const result = truncateWithBoundaries(text, 100);
186
+
187
+ // If the closing ``` is within the 20% buffer, should try to include it
188
+ // At minimum, should handle gracefully
189
+ expect(result.length).toBeGreaterThan(0);
190
+
191
+ // If it did truncate and we're within the buffer, backticks should be balanced
192
+ if (result.length < text.length && text.length <= 120) {
193
+ const beforeTruncate = result.split("[TRUNCATED")[0];
194
+ const backtickCount = (beforeTruncate.match(/```/g) || []).length;
195
+ // Should either have no backticks or balanced backticks
196
+ expect(backtickCount === 0 || backtickCount % 2 === 0).toBe(true);
197
+ }
198
+ });
199
+ });
200
+
201
+ describe("guardrailOutput", () => {
202
+ test("skips configured tools", () => {
203
+ const longOutput = "a".repeat(50000);
204
+
205
+ // Beads tools should never be truncated
206
+ const result = guardrailOutput("beads_create", longOutput);
207
+
208
+ expect(result.truncated).toBe(false);
209
+ expect(result.output).toBe(longOutput);
210
+ expect(result.originalLength).toBe(50000);
211
+ expect(result.truncatedLength).toBe(50000);
212
+ });
213
+
214
+ test("truncates oversized output for non-skip tools", () => {
215
+ const longOutput = "a".repeat(50000);
216
+
217
+ // Random tool should be truncated at default limit
218
+ const result = guardrailOutput("some_random_tool", longOutput);
219
+
220
+ expect(result.truncated).toBe(true);
221
+ expect(result.output).toContain("[TRUNCATED");
222
+ expect(result.originalLength).toBe(50000);
223
+ expect(result.truncatedLength).toBeLessThan(50000);
224
+ });
225
+
226
+ test("respects per-tool limits", () => {
227
+ const mediumOutput = "a".repeat(40000);
228
+
229
+ // repo-autopsy_file has 64000 char limit
230
+ const result1 = guardrailOutput("repo-autopsy_file", mediumOutput);
231
+ expect(result1.truncated).toBe(false);
232
+
233
+ // cass_stats has 8000 char limit
234
+ const result2 = guardrailOutput("cass_stats", mediumOutput);
235
+ expect(result2.truncated).toBe(true);
236
+ });
237
+
238
+ test("uses custom config when provided", () => {
239
+ const customConfig: GuardrailConfig = {
240
+ defaultMaxChars: 100,
241
+ toolLimits: {
242
+ custom_tool: 200,
243
+ },
244
+ skipTools: ["never_truncate"],
245
+ };
246
+
247
+ const text150 = "a".repeat(150);
248
+
249
+ // Should truncate at default 100
250
+ const result1 = guardrailOutput("random_tool", text150, customConfig);
251
+ expect(result1.truncated).toBe(true);
252
+
253
+ // Should not truncate at custom limit 200
254
+ const result2 = guardrailOutput("custom_tool", text150, customConfig);
255
+ expect(result2.truncated).toBe(false);
256
+
257
+ // Should skip configured tool
258
+ const text500 = "a".repeat(500);
259
+ const result3 = guardrailOutput("never_truncate", text500, customConfig);
260
+ expect(result3.truncated).toBe(false);
261
+ });
262
+
263
+ test("returns complete metadata", () => {
264
+ const output = "a".repeat(50000);
265
+ const result = guardrailOutput("test_tool", output);
266
+
267
+ expect(result).toHaveProperty("output");
268
+ expect(result).toHaveProperty("truncated");
269
+ expect(result).toHaveProperty("originalLength");
270
+ expect(result).toHaveProperty("truncatedLength");
271
+
272
+ expect(typeof result.truncated).toBe("boolean");
273
+ expect(typeof result.originalLength).toBe("number");
274
+ expect(typeof result.truncatedLength).toBe("number");
275
+ });
276
+
277
+ test("handles all skip tools from DEFAULT_GUARDRAIL_CONFIG", () => {
278
+ const longOutput = "a".repeat(100000);
279
+
280
+ const skipTools = DEFAULT_GUARDRAIL_CONFIG.skipTools;
281
+
282
+ // Test a sample of skip tools
283
+ const samplesToTest = [
284
+ "beads_create",
285
+ "agentmail_send",
286
+ "swarmmail_inbox",
287
+ "structured_validate",
288
+ "swarm_complete",
289
+ "mandate_query",
290
+ ];
291
+
292
+ for (const toolName of samplesToTest) {
293
+ expect(skipTools).toContain(toolName);
294
+ const result = guardrailOutput(toolName, longOutput);
295
+ expect(result.truncated).toBe(false);
296
+ }
297
+ });
298
+ });
299
+
300
+ describe("createMetrics", () => {
301
+ test("creates metrics entry from guardrail result", () => {
302
+ const result = {
303
+ output: "truncated output",
304
+ truncated: true,
305
+ originalLength: 50000,
306
+ truncatedLength: 32000,
307
+ };
308
+
309
+ const metrics = createMetrics(result, "test_tool");
310
+
311
+ expect(metrics).toEqual({
312
+ toolName: "test_tool",
313
+ originalLength: 50000,
314
+ truncatedLength: 32000,
315
+ timestamp: expect.any(Number),
316
+ });
317
+ });
318
+
319
+ test("timestamp is reasonable", () => {
320
+ const result = {
321
+ output: "output",
322
+ truncated: false,
323
+ originalLength: 100,
324
+ truncatedLength: 100,
325
+ };
326
+
327
+ const before = Date.now();
328
+ const metrics = createMetrics(result, "test_tool");
329
+ const after = Date.now();
330
+
331
+ expect(metrics.timestamp).toBeGreaterThanOrEqual(before);
332
+ expect(metrics.timestamp).toBeLessThanOrEqual(after);
333
+ });
334
+ });
335
+
336
+ describe("DEFAULT_GUARDRAIL_CONFIG", () => {
337
+ test("has sensible defaults", () => {
338
+ expect(DEFAULT_GUARDRAIL_CONFIG.defaultMaxChars).toBe(32000);
339
+ expect(DEFAULT_GUARDRAIL_CONFIG.toolLimits).toBeDefined();
340
+ expect(DEFAULT_GUARDRAIL_CONFIG.skipTools).toBeDefined();
341
+ expect(DEFAULT_GUARDRAIL_CONFIG.skipTools.length).toBeGreaterThan(0);
342
+ });
343
+
344
+ test("includes higher limits for code/doc tools", () => {
345
+ const config = DEFAULT_GUARDRAIL_CONFIG;
346
+
347
+ expect(config.toolLimits["repo-autopsy_file"]).toBe(64000);
348
+ expect(config.toolLimits["context7_get-library-docs"]).toBe(64000);
349
+ expect(config.toolLimits["cass_view"]).toBe(64000);
350
+ });
351
+
352
+ test("includes lower limits for stats tools", () => {
353
+ const config = DEFAULT_GUARDRAIL_CONFIG;
354
+
355
+ expect(config.toolLimits["cass_stats"]).toBe(8000);
356
+ expect(config.toolLimits["repo-autopsy_stats"]).toBe(16000);
357
+ });
358
+
359
+ test("skips all internal coordination tools", () => {
360
+ const config = DEFAULT_GUARDRAIL_CONFIG;
361
+
362
+ // Sample of tools that should be in skipTools
363
+ const expectedSkips = [
364
+ "beads_create",
365
+ "beads_sync",
366
+ "agentmail_init",
367
+ "swarmmail_send",
368
+ "structured_parse_evaluation",
369
+ "swarm_decompose",
370
+ "mandate_file",
371
+ ];
372
+
373
+ for (const tool of expectedSkips) {
374
+ expect(config.skipTools).toContain(tool);
375
+ }
376
+ });
377
+ });
378
+
379
+ describe("edge cases", () => {
380
+ test("handles JSON array at truncation boundary", () => {
381
+ const text = `[
382
+ {"id": 1, "data": "item1"},
383
+ {"id": 2, "data": "item2"},
384
+ {"id": 3, "data": "item3"}
385
+ ]`;
386
+
387
+ const result = truncateWithBoundaries(text, 50);
388
+
389
+ expect(result).toContain("[TRUNCATED");
390
+
391
+ // Should not cut mid-item
392
+ const beforeTruncate = result.split("[TRUNCATED")[0];
393
+ const openBrackets = (beforeTruncate.match(/\[/g) || []).length;
394
+ const closeBrackets = (beforeTruncate.match(/\]/g) || []).length;
395
+
396
+ expect(openBrackets).toBeLessThanOrEqual(closeBrackets + 1);
397
+ });
398
+
399
+ test("handles mixed code blocks and JSON", () => {
400
+ const text = `
401
+ \`\`\`json
402
+ {"data": "This is JSON inside a code block"}
403
+ \`\`\`
404
+
405
+ And then some JSON outside:
406
+ {"more": "data"}
407
+ `;
408
+
409
+ const result = truncateWithBoundaries(text, 80);
410
+
411
+ // Should respect both code block and JSON boundaries
412
+ expect(result).toContain("[TRUNCATED");
413
+
414
+ const beforeTruncate = result.split("[TRUNCATED")[0];
415
+ const backtickCount = (beforeTruncate.match(/```/g) || []).length;
416
+
417
+ // Backticks should be balanced
418
+ expect(backtickCount % 2).toBe(0);
419
+ });
420
+
421
+ test("handles unicode characters correctly", () => {
422
+ const text = "Hello 世界! 🌍 ".repeat(100);
423
+ const result = truncateWithBoundaries(text, 100);
424
+
425
+ expect(result).toContain("[TRUNCATED");
426
+ // Length should be reasonable (not corrupted by unicode)
427
+ expect(result.length).toBeLessThan(text.length);
428
+ });
429
+
430
+ test("handles CRLF line endings", () => {
431
+ const text = "Line 1\r\nLine 2\r\nLine 3\r\n".repeat(50);
432
+ const result = truncateWithBoundaries(text, 100);
433
+
434
+ expect(result).toContain("[TRUNCATED");
435
+ // Should handle line endings gracefully
436
+ expect(result).not.toContain("\r\n[TRUNCATED");
437
+ });
438
+ });