opencode-swarm-plugin 0.12.9 → 0.12.11

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.
@@ -131,3 +131,24 @@ export const ValidationResultSchema = z.object({
131
131
  extractionMethod: z.string().optional(),
132
132
  });
133
133
  export type ValidationResult = z.infer<typeof ValidationResultSchema>;
134
+
135
+ /**
136
+ * Failure mode taxonomy for task failures
137
+ *
138
+ * Classifies WHY tasks fail, not just that they failed.
139
+ * Used in outcome tracking to learn from failure patterns.
140
+ *
141
+ * @see src/learning.ts OutcomeSignalsSchema
142
+ * @see "Patterns for Building AI Agents" p.46
143
+ */
144
+ export const FailureModeSchema = z.enum([
145
+ "timeout", // Task exceeded time limit
146
+ "conflict", // File reservation conflict
147
+ "validation", // Output failed schema validation
148
+ "tool_failure", // Tool call returned error
149
+ "context_overflow", // Ran out of context window
150
+ "dependency_blocked", // Waiting on another subtask
151
+ "user_cancelled", // User interrupted
152
+ "unknown", // Unclassified
153
+ ]);
154
+ export type FailureMode = z.infer<typeof FailureModeSchema>;
package/src/swarm.ts CHANGED
@@ -25,7 +25,7 @@ import {
25
25
  type SpawnedAgent,
26
26
  type Bead,
27
27
  } from "./schemas";
28
- import { mcpCall } from "./agent-mail";
28
+ import { mcpCall, requireState } from "./agent-mail";
29
29
  import {
30
30
  OutcomeSignalsSchema,
31
31
  DecompositionStrategySchema,
@@ -344,19 +344,31 @@ export const STRATEGIES: Record<
344
344
  "research",
345
345
  "investigate",
346
346
  "explore",
347
- "find",
347
+ "find out",
348
348
  "discover",
349
349
  "understand",
350
- "learn",
350
+ "learn about",
351
351
  "analyze",
352
- "what",
353
- "how",
354
- "why",
352
+ "what is",
353
+ "what are",
354
+ "how does",
355
+ "how do",
356
+ "why does",
357
+ "why do",
355
358
  "compare",
356
359
  "evaluate",
357
360
  "study",
358
361
  "look up",
359
- "search",
362
+ "look into",
363
+ "search for",
364
+ "dig into",
365
+ "figure out",
366
+ "debug options",
367
+ "debug levers",
368
+ "configuration options",
369
+ "environment variables",
370
+ "available options",
371
+ "documentation",
360
372
  ],
361
373
  guidelines: [
362
374
  "Split by information source (PDFs, repos, history, web)",
@@ -408,8 +420,18 @@ export function selectStrategy(task: string): {
408
420
  for (const [strategyName, definition] of Object.entries(STRATEGIES)) {
409
421
  const name = strategyName as Exclude<DecompositionStrategy, "auto">;
410
422
  for (const keyword of definition.keywords) {
411
- if (taskLower.includes(keyword)) {
412
- scores[name] += 1;
423
+ // Use word boundary matching to avoid "debug" matching "bug"
424
+ // For multi-word keywords, just check includes (they're specific enough)
425
+ if (keyword.includes(" ")) {
426
+ if (taskLower.includes(keyword)) {
427
+ scores[name] += 1;
428
+ }
429
+ } else {
430
+ // Single word: use word boundary regex
431
+ const regex = new RegExp(`\\b${keyword}\\b`, "i");
432
+ if (regex.test(taskLower)) {
433
+ scores[name] += 1;
434
+ }
413
435
  }
414
436
  }
415
437
  }
@@ -656,51 +678,46 @@ Begin work on your subtask now.`;
656
678
  */
657
679
  export const SUBTASK_PROMPT_V2 = `You are a swarm agent working on: **{subtask_title}**
658
680
 
659
- ## Identity
660
- - **Bead ID**: {bead_id}
661
- - **Epic ID**: {epic_id}
681
+ ## [IDENTITY]
682
+ Agent: (assigned at spawn)
683
+ Bead: {bead_id}
684
+ Epic: {epic_id}
662
685
 
663
- ## Task
686
+ ## [TASK]
664
687
  {subtask_description}
665
688
 
666
- ## Files (exclusive reservation)
689
+ ## [FILES]
690
+ Reserved (exclusive):
667
691
  {file_list}
668
692
 
669
693
  Only modify these files. Need others? Message the coordinator.
670
694
 
671
- ## Context
695
+ ## [CONTEXT]
672
696
  {shared_context}
673
697
 
674
698
  {compressed_context}
675
699
 
676
700
  {error_context}
677
701
 
678
- ## MANDATORY: Use These Tools
702
+ ## [TOOLS]
703
+ ### Beads
704
+ - beads_update (status: blocked)
705
+ - beads_create (new bugs)
706
+ - beads_close (via swarm_complete)
679
707
 
680
- ### Agent Mail - communicate with the swarm
681
- \`\`\`typescript
682
- // Report progress, ask questions, announce blockers
683
- agentmail_send({
684
- to: ["coordinator"],
685
- subject: "Progress update",
686
- body: "What you did or need",
687
- thread_id: "{epic_id}"
688
- })
689
- \`\`\`
708
+ ### Agent Mail
709
+ - agentmail_send (thread_id: {epic_id})
690
710
 
691
- ### Beads - track your work
692
- - **Blocked?** \`beads_update({ id: "{bead_id}", status: "blocked" })\`
693
- - **Found bug?** \`beads_create({ title: "Bug description", type: "bug" })\`
694
- - **Done?** \`swarm_complete({ bead_id: "{bead_id}", summary: "What you did", files_touched: [...] })\`
711
+ ### Completion
712
+ - swarm_complete (REQUIRED when done)
695
713
 
696
- ## Workflow
714
+ ## [OUTPUT]
715
+ 1. Read files first
716
+ 2. Implement changes
717
+ 3. Verify (typecheck)
718
+ 4. Complete with swarm_complete
697
719
 
698
- 1. **Read** the files first
699
- 2. **Plan** your approach (message coordinator if complex)
700
- 3. **Implement** the changes
701
- 4. **Verify** (typecheck, tests)
702
- 5. **Report** progress via Agent Mail
703
- 6. **Complete** with swarm_complete when done
720
+ Return: Summary of changes made
704
721
 
705
722
  **Never work silently.** Communicate progress and blockers immediately.
706
723
 
@@ -1733,6 +1750,92 @@ async function runUbsScan(files: string[]): Promise<UbsScanResult | null> {
1733
1750
  }
1734
1751
  }
1735
1752
 
1753
+ /**
1754
+ * Broadcast context updates to all agents in the epic
1755
+ *
1756
+ * Enables mid-task coordination by sharing discoveries, warnings, or blockers
1757
+ * with all agents working on the same epic. Agents can broadcast without
1758
+ * waiting for task completion.
1759
+ *
1760
+ * Based on "Patterns for Building AI Agents" p.31: "Ensure subagents can share context along the way"
1761
+ */
1762
+ export const swarm_broadcast = tool({
1763
+ description:
1764
+ "Broadcast context update to all agents working on the same epic",
1765
+ args: {
1766
+ epic_id: tool.schema.string().describe("Epic ID (e.g., bd-abc123)"),
1767
+ message: tool.schema
1768
+ .string()
1769
+ .describe("Context update to share (what changed, what was learned)"),
1770
+ importance: tool.schema
1771
+ .enum(["info", "warning", "blocker"])
1772
+ .default("info")
1773
+ .describe("Priority level (default: info)"),
1774
+ files_affected: tool.schema
1775
+ .array(tool.schema.string())
1776
+ .optional()
1777
+ .describe("Files this context relates to"),
1778
+ },
1779
+ async execute(args, ctx) {
1780
+ // Get agent state - requires prior initialization
1781
+ const state = requireState(ctx.sessionID);
1782
+
1783
+ // Extract bead_id from context if available (for traceability)
1784
+ // In the swarm flow, ctx might have the current bead being worked on
1785
+ const beadId = (ctx as { beadId?: string }).beadId || "unknown";
1786
+
1787
+ // Format the broadcast message
1788
+ const body = [
1789
+ `## Context Update`,
1790
+ "",
1791
+ `**From**: ${state.agentName} (${beadId})`,
1792
+ `**Priority**: ${args.importance.toUpperCase()}`,
1793
+ "",
1794
+ args.message,
1795
+ "",
1796
+ args.files_affected && args.files_affected.length > 0
1797
+ ? `**Files affected**:\n${args.files_affected.map((f) => `- \`${f}\``).join("\n")}`
1798
+ : "",
1799
+ ]
1800
+ .filter(Boolean)
1801
+ .join("\n");
1802
+
1803
+ // Map importance to Agent Mail importance
1804
+ const mailImportance =
1805
+ args.importance === "blocker"
1806
+ ? "urgent"
1807
+ : args.importance === "warning"
1808
+ ? "high"
1809
+ : "normal";
1810
+
1811
+ // Send as broadcast to thread (empty 'to' = all agents in thread)
1812
+ await mcpCall("send_message", {
1813
+ project_key: state.projectKey,
1814
+ sender_name: state.agentName,
1815
+ to: [], // Broadcast to thread
1816
+ subject: `[${args.importance.toUpperCase()}] Context update from ${state.agentName}`,
1817
+ body_md: body,
1818
+ thread_id: args.epic_id,
1819
+ importance: mailImportance,
1820
+ ack_required: args.importance === "blocker", // Require ack for blockers
1821
+ });
1822
+
1823
+ return JSON.stringify(
1824
+ {
1825
+ broadcast: true,
1826
+ epic_id: args.epic_id,
1827
+ from: state.agentName,
1828
+ bead_id: beadId,
1829
+ importance: args.importance,
1830
+ recipients: "all agents in epic",
1831
+ ack_required: args.importance === "blocker",
1832
+ },
1833
+ null,
1834
+ 2,
1835
+ );
1836
+ },
1837
+ });
1838
+
1736
1839
  /**
1737
1840
  * Mark a subtask as complete
1738
1841
  *
@@ -1897,6 +2000,40 @@ export const swarm_complete = tool({
1897
2000
  },
1898
2001
  });
1899
2002
 
2003
+ /**
2004
+ * Classify failure based on error message heuristics
2005
+ *
2006
+ * Simple pattern matching to categorize why a task failed.
2007
+ * Used when failure_mode is not explicitly provided.
2008
+ *
2009
+ * @param error - Error object or message
2010
+ * @returns FailureMode classification
2011
+ */
2012
+ function classifyFailure(error: Error | string): string {
2013
+ const msg = (typeof error === "string" ? error : error.message).toLowerCase();
2014
+
2015
+ if (msg.includes("timeout")) return "timeout";
2016
+ if (msg.includes("conflict") || msg.includes("reservation"))
2017
+ return "conflict";
2018
+ if (msg.includes("validation") || msg.includes("schema")) return "validation";
2019
+ if (msg.includes("context") || msg.includes("token"))
2020
+ return "context_overflow";
2021
+ if (msg.includes("blocked") || msg.includes("dependency"))
2022
+ return "dependency_blocked";
2023
+ if (msg.includes("cancel")) return "user_cancelled";
2024
+
2025
+ // Check for tool failure patterns
2026
+ if (
2027
+ msg.includes("tool") ||
2028
+ msg.includes("command") ||
2029
+ msg.includes("failed to execute")
2030
+ ) {
2031
+ return "tool_failure";
2032
+ }
2033
+
2034
+ return "unknown";
2035
+ }
2036
+
1900
2037
  /**
1901
2038
  * Record outcome signals from a completed subtask
1902
2039
  *
@@ -1946,6 +2083,25 @@ export const swarm_record_outcome = tool({
1946
2083
  .enum(["file-based", "feature-based", "risk-based", "research-based"])
1947
2084
  .optional()
1948
2085
  .describe("Decomposition strategy used for this task"),
2086
+ failure_mode: tool.schema
2087
+ .enum([
2088
+ "timeout",
2089
+ "conflict",
2090
+ "validation",
2091
+ "tool_failure",
2092
+ "context_overflow",
2093
+ "dependency_blocked",
2094
+ "user_cancelled",
2095
+ "unknown",
2096
+ ])
2097
+ .optional()
2098
+ .describe(
2099
+ "Failure classification (only when success=false). Auto-classified if not provided.",
2100
+ ),
2101
+ failure_details: tool.schema
2102
+ .string()
2103
+ .optional()
2104
+ .describe("Detailed failure context (error message, stack trace, etc.)"),
1949
2105
  },
1950
2106
  async execute(args) {
1951
2107
  // Build outcome signals
@@ -1958,8 +2114,15 @@ export const swarm_record_outcome = tool({
1958
2114
  files_touched: args.files_touched ?? [],
1959
2115
  timestamp: new Date().toISOString(),
1960
2116
  strategy: args.strategy as LearningDecompositionStrategy | undefined,
2117
+ failure_mode: args.failure_mode,
2118
+ failure_details: args.failure_details,
1961
2119
  };
1962
2120
 
2121
+ // If task failed but no failure_mode provided, try to classify from failure_details
2122
+ if (!args.success && !args.failure_mode && args.failure_details) {
2123
+ signals.failure_mode = classifyFailure(args.failure_details) as any;
2124
+ }
2125
+
1963
2126
  // Validate signals
1964
2127
  const validated = OutcomeSignalsSchema.parse(signals);
1965
2128
 
@@ -2017,6 +2180,8 @@ export const swarm_record_outcome = tool({
2017
2180
  retry_count: args.retry_count ?? 0,
2018
2181
  success: args.success,
2019
2182
  strategy: args.strategy,
2183
+ failure_mode: validated.failure_mode,
2184
+ failure_details: validated.failure_details,
2020
2185
  accumulated_errors: errorStats.total,
2021
2186
  unresolved_errors: errorStats.unresolved,
2022
2187
  },
@@ -2537,6 +2702,7 @@ export const swarmTools = {
2537
2702
  swarm_validate_decomposition: swarm_validate_decomposition,
2538
2703
  swarm_status: swarm_status,
2539
2704
  swarm_progress: swarm_progress,
2705
+ swarm_broadcast: swarm_broadcast,
2540
2706
  swarm_complete: swarm_complete,
2541
2707
  swarm_record_outcome: swarm_record_outcome,
2542
2708
  swarm_subtask_prompt: swarm_subtask_prompt,