opencode-swarm-plugin 0.18.0 → 0.20.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/src/structured.ts CHANGED
@@ -1,14 +1,17 @@
1
1
  /**
2
- * Structured Output Module - Validate and parse agent responses
2
+ * Structured Output Module - JSON extraction and schema validation
3
3
  *
4
- * Agents frequently return malformed JSON, especially when streaming
5
- * or under high load. This module provides robust extraction and
6
- * validation with detailed error feedback for retry loops.
4
+ * Handles parsing agent responses that contain JSON, with multiple fallback
5
+ * strategies for malformed or wrapped content.
7
6
  *
8
- * Key patterns:
9
- * - Multiple JSON extraction strategies (direct, code blocks, brace matching)
10
- * - Zod schema validation with formatted error messages
11
- * - Structured error types for programmatic handling
7
+ * ## Usage
8
+ * 1. `structured_extract_json` - Raw JSON extraction from text (no validation)
9
+ * 2. `structured_validate` - Extract + validate against named schema
10
+ * 3. `structured_parse_evaluation` - Typed parsing for agent self-evaluations
11
+ * 4. `structured_parse_decomposition` - Typed parsing for task breakdowns
12
+ * 5. `structured_parse_bead_tree` - Typed parsing for epic + subtasks
13
+ *
14
+ * @module structured
12
15
  */
13
16
  import { tool } from "@opencode-ai/plugin";
14
17
  import { z, type ZodSchema } from "zod";
@@ -17,6 +20,7 @@ import {
17
20
  TaskDecompositionSchema,
18
21
  BeadTreeSchema,
19
22
  ValidationResultSchema,
23
+ CriterionEvaluationSchema,
20
24
  type Evaluation,
21
25
  type TaskDecomposition,
22
26
  type BeadTree,
@@ -109,11 +113,18 @@ function getSchemaByName(name: string): ZodSchema {
109
113
  }
110
114
 
111
115
  /**
112
- * Try to extract JSON from text using multiple strategies
116
+ * Extract JSON from text using multiple strategies.
117
+ *
118
+ * Strategies tried in priority order:
119
+ * 1. Direct parse - fastest, works for clean JSON
120
+ * 2. JSON code block - common in markdown responses
121
+ * 3. Generic code block - fallback for unlabeled blocks
122
+ * 4. First brace match - finds outermost {...}
123
+ * 5. Last brace match - handles trailing content
124
+ * 6. Repair attempt - fixes common issues (quotes, trailing commas)
113
125
  *
114
- * @param text - Raw text that may contain JSON
115
- * @returns Tuple of [parsed object, extraction method used]
116
- * @throws JsonExtractionError if no JSON can be extracted
126
+ * @param text Raw text potentially containing JSON
127
+ * @returns Parsed JSON object or null if all strategies fail
117
128
  */
118
129
  function extractJsonFromText(text: string): [unknown, string] {
119
130
  const trimmed = text.trim();
@@ -189,6 +200,9 @@ function extractJsonFromText(text: string): [unknown, string] {
189
200
  );
190
201
  }
191
202
 
203
+ /** Maximum nesting depth before aborting (prevents stack overflow on malformed input) */
204
+ const MAX_BRACE_DEPTH = 100;
205
+
192
206
  /**
193
207
  * Find a balanced pair of braces/brackets
194
208
  */
@@ -226,8 +240,14 @@ function findBalancedBraces(
226
240
 
227
241
  if (char === open) {
228
242
  depth++;
243
+ if (depth > MAX_BRACE_DEPTH) {
244
+ return null; // Malformed input - too deeply nested
245
+ }
229
246
  } else if (char === close) {
230
247
  depth--;
248
+ if (depth < 0) {
249
+ return null; // Malformed input - unbalanced braces
250
+ }
231
251
  if (depth === 0) {
232
252
  return text.slice(startIdx, i + 1);
233
253
  }
@@ -238,13 +258,18 @@ function findBalancedBraces(
238
258
  }
239
259
 
240
260
  /**
241
- * Attempt common JSON repairs
261
+ * Attempt to repair common JSON issues.
242
262
  *
243
- * Handles:
244
- * - Trailing commas
245
- * - Single quotes instead of double quotes
246
- * - Unquoted keys
247
- * - Newlines in strings
263
+ * This is a simple heuristic - won't work for all cases.
264
+ *
265
+ * **Known Limitations:**
266
+ * - Single quotes in nested objects may not be handled correctly
267
+ * - Escaped quotes in keys can confuse the regex
268
+ * - Multiline strings are not detected
269
+ * - Trailing commas in nested arrays may be missed
270
+ *
271
+ * @param text Potentially malformed JSON string
272
+ * @returns Repaired JSON string (may still be invalid)
248
273
  */
249
274
  function attemptJsonRepair(text: string): string {
250
275
  let repaired = text;
@@ -275,6 +300,9 @@ function attemptJsonRepair(text: string): string {
275
300
  // Validation Result Types
276
301
  // ============================================================================
277
302
 
303
+ /** Maximum characters to show in raw input previews */
304
+ const RAW_INPUT_PREVIEW_LENGTH = 200;
305
+
278
306
  /**
279
307
  * Result of a structured validation attempt
280
308
  */
@@ -326,7 +354,7 @@ export const structured_extract_json = tool({
326
354
  success: false,
327
355
  error: error.message,
328
356
  attempted_strategies: error.attemptedStrategies,
329
- raw_input_preview: args.text.slice(0, 200),
357
+ raw_input_preview: args.text.slice(0, RAW_INPUT_PREVIEW_LENGTH),
330
358
  },
331
359
  null,
332
360
  2,
@@ -350,7 +378,12 @@ export const structured_validate = tool({
350
378
  response: tool.schema.string().describe("Agent response to validate"),
351
379
  schema_name: tool.schema
352
380
  .enum(["evaluation", "task_decomposition", "bead_tree"])
353
- .describe("Schema to validate against"),
381
+ .describe(
382
+ "Schema to validate against: " +
383
+ "evaluation = agent self-eval with criteria, " +
384
+ "task_decomposition = swarm task breakdown, " +
385
+ "bead_tree = epic with subtasks",
386
+ ),
354
387
  max_retries: tool.schema
355
388
  .number()
356
389
  .min(1)
@@ -366,6 +399,15 @@ export const structured_validate = tool({
366
399
  errors: [],
367
400
  };
368
401
 
402
+ // Check for empty response before attempting extraction
403
+ if (!args.response || args.response.trim().length === 0) {
404
+ return JSON.stringify({
405
+ valid: false,
406
+ error: "Response is empty or contains only whitespace",
407
+ raw_input: "(empty)",
408
+ });
409
+ }
410
+
369
411
  // Step 1: Extract JSON
370
412
  let extracted: unknown;
371
413
  let extractionMethod: string;
@@ -377,7 +419,7 @@ export const structured_validate = tool({
377
419
  if (error instanceof JsonExtractionError) {
378
420
  result.errors = [
379
421
  `JSON extraction failed after trying: ${error.attemptedStrategies.join(", ")}`,
380
- `Input preview: ${args.response.slice(0, 100)}...`,
422
+ `Input preview: ${args.response.slice(0, RAW_INPUT_PREVIEW_LENGTH)}...`,
381
423
  ];
382
424
  return JSON.stringify(result, null, 2);
383
425
  }
@@ -441,7 +483,12 @@ export const structured_parse_evaluation = tool({
441
483
  passed: validated.passed,
442
484
  criteria_count: Object.keys(validated.criteria).length,
443
485
  failed_criteria: Object.entries(validated.criteria)
444
- .filter(([_, v]) => !(v as { passed: boolean }).passed)
486
+ .filter(([_, v]) => {
487
+ const criterion = v as z.infer<
488
+ typeof CriterionEvaluationSchema
489
+ >;
490
+ return !criterion.passed;
491
+ })
445
492
  .map(([k]) => k),
446
493
  },
447
494
  },
@@ -700,9 +747,9 @@ export { extractJsonFromText, formatZodErrors, getSchemaByName };
700
747
  // ============================================================================
701
748
 
702
749
  export const structuredTools = {
703
- "structured_extract_json": structured_extract_json,
704
- "structured_validate": structured_validate,
705
- "structured_parse_evaluation": structured_parse_evaluation,
706
- "structured_parse_decomposition": structured_parse_decomposition,
707
- "structured_parse_bead_tree": structured_parse_bead_tree,
750
+ structured_extract_json: structured_extract_json,
751
+ structured_validate: structured_validate,
752
+ structured_parse_evaluation: structured_parse_evaluation,
753
+ structured_parse_decomposition: structured_parse_decomposition,
754
+ structured_parse_bead_tree: structured_parse_bead_tree,
708
755
  };