opencode-swarm-plugin 0.12.6 → 0.12.8

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/agent-mail.ts CHANGED
@@ -38,10 +38,10 @@ const RETRY_CONFIG = {
38
38
 
39
39
  // Server recovery configuration
40
40
  const RECOVERY_CONFIG = {
41
- /** Max consecutive failures before attempting restart */
42
- failureThreshold: 2,
43
- /** Cooldown between restart attempts (ms) */
44
- restartCooldownMs: 30000,
41
+ /** Max consecutive failures before attempting restart (1 = restart on first "unexpected error") */
42
+ failureThreshold: 1,
43
+ /** Cooldown between restart attempts (ms) - 10 seconds */
44
+ restartCooldownMs: 10000,
45
45
  /** Whether auto-restart is enabled */
46
46
  enabled: process.env.OPENCODE_AGENT_MAIL_AUTO_RESTART !== "false",
47
47
  };
@@ -704,6 +704,7 @@ export async function mcpCall<T>(
704
704
  args: Record<string, unknown>,
705
705
  ): Promise<T> {
706
706
  let lastError: Error | null = null;
707
+ let restartAttempted = false;
707
708
 
708
709
  for (let attempt = 0; attempt <= RETRY_CONFIG.maxRetries; attempt++) {
709
710
  // Apply backoff delay (except first attempt)
@@ -723,17 +724,39 @@ export async function mcpCall<T>(
723
724
  return result;
724
725
  } catch (error) {
725
726
  lastError = error instanceof Error ? error : new Error(String(error));
727
+ const errorMessage = lastError.message.toLowerCase();
726
728
 
727
729
  // Track consecutive failures
728
730
  consecutiveFailures++;
729
731
 
730
- // Check if error is retryable FIRST
732
+ // Check if error is retryable
731
733
  const retryable = isRetryableError(error);
732
734
 
733
- // Check if we should attempt server restart
735
+ // AGGRESSIVE: If it's an "unexpected error", restart immediately (once per call)
736
+ const isUnexpectedError = errorMessage.includes("unexpected error");
737
+ if (isUnexpectedError && !restartAttempted && RECOVERY_CONFIG.enabled) {
738
+ console.warn(
739
+ `[agent-mail] "${toolName}" got unexpected error, restarting server immediately...`,
740
+ );
741
+ restartAttempted = true;
742
+ const restarted = await restartServer();
743
+ if (restarted) {
744
+ agentMailAvailable = null;
745
+ consecutiveFailures = 0;
746
+ // Small delay to let server stabilize
747
+ await new Promise((resolve) => setTimeout(resolve, 1000));
748
+ // Don't count this attempt - retry immediately
749
+ attempt--;
750
+ continue;
751
+ }
752
+ }
753
+
754
+ // Standard retry logic for other retryable errors
734
755
  if (
756
+ !isUnexpectedError &&
735
757
  consecutiveFailures >= RECOVERY_CONFIG.failureThreshold &&
736
- RECOVERY_CONFIG.enabled
758
+ RECOVERY_CONFIG.enabled &&
759
+ !restartAttempted
737
760
  ) {
738
761
  console.warn(
739
762
  `[agent-mail] ${consecutiveFailures} consecutive failures, checking server health...`,
@@ -742,13 +765,11 @@ export async function mcpCall<T>(
742
765
  const healthy = await isServerFunctional();
743
766
  if (!healthy) {
744
767
  console.warn("[agent-mail] Server unhealthy, attempting restart...");
768
+ restartAttempted = true;
745
769
  const restarted = await restartServer();
746
770
  if (restarted) {
747
- // Reset availability cache since server restarted
748
771
  agentMailAvailable = null;
749
- // Only retry if the error was retryable in the first place
750
772
  if (retryable) {
751
- // Don't count this attempt against retries - try again
752
773
  attempt--;
753
774
  continue;
754
775
  }
@@ -880,30 +901,88 @@ export const agentmail_init = tool({
880
901
  );
881
902
  }
882
903
 
883
- // 1. Ensure project exists
884
- const project = await mcpCall<ProjectInfo>("ensure_project", {
885
- human_key: args.project_path,
886
- });
904
+ // Retry loop with restart on failure
905
+ const MAX_INIT_RETRIES = 3;
906
+ let lastError: Error | null = null;
887
907
 
888
- // 2. Register agent
889
- const agent = await mcpCall<AgentInfo>("register_agent", {
890
- project_key: args.project_path,
891
- program: "opencode",
892
- model: "claude-opus-4",
893
- name: args.agent_name, // undefined = auto-generate
894
- task_description: args.task_description || "",
895
- });
908
+ for (let attempt = 1; attempt <= MAX_INIT_RETRIES; attempt++) {
909
+ try {
910
+ // 1. Ensure project exists
911
+ const project = await mcpCall<ProjectInfo>("ensure_project", {
912
+ human_key: args.project_path,
913
+ });
914
+
915
+ // 2. Register agent
916
+ const agent = await mcpCall<AgentInfo>("register_agent", {
917
+ project_key: args.project_path,
918
+ program: "opencode",
919
+ model: "claude-opus-4",
920
+ name: args.agent_name, // undefined = auto-generate
921
+ task_description: args.task_description || "",
922
+ });
923
+
924
+ // 3. Store state using sessionID
925
+ const state: AgentMailState = {
926
+ projectKey: args.project_path,
927
+ agentName: agent.name,
928
+ reservations: [],
929
+ startedAt: new Date().toISOString(),
930
+ };
931
+ setState(ctx.sessionID, state);
932
+
933
+ // Success - if we retried, log it
934
+ if (attempt > 1) {
935
+ console.warn(
936
+ `[agent-mail] Init succeeded on attempt ${attempt} after restart`,
937
+ );
938
+ }
896
939
 
897
- // 3. Store state using sessionID
898
- const state: AgentMailState = {
899
- projectKey: args.project_path,
900
- agentName: agent.name,
901
- reservations: [],
902
- startedAt: new Date().toISOString(),
903
- };
904
- setState(ctx.sessionID, state);
940
+ return JSON.stringify({ project, agent, available: true }, null, 2);
941
+ } catch (error) {
942
+ lastError = error instanceof Error ? error : new Error(String(error));
943
+ const isUnexpectedError = lastError.message
944
+ .toLowerCase()
945
+ .includes("unexpected error");
946
+
947
+ console.warn(
948
+ `[agent-mail] Init attempt ${attempt}/${MAX_INIT_RETRIES} failed: ${lastError.message}`,
949
+ );
950
+
951
+ // If it's an "unexpected error" and we have retries left, restart and retry
952
+ if (isUnexpectedError && attempt < MAX_INIT_RETRIES) {
953
+ console.warn(
954
+ "[agent-mail] Detected 'unexpected error', restarting server...",
955
+ );
956
+ const restarted = await restartServer();
957
+ if (restarted) {
958
+ // Clear cache and retry
959
+ agentMailAvailable = null;
960
+ consecutiveFailures = 0;
961
+ // Small delay to let server stabilize
962
+ await new Promise((resolve) => setTimeout(resolve, 1000));
963
+ continue;
964
+ }
965
+ }
966
+
967
+ // For non-unexpected errors or if restart failed, don't retry
968
+ if (!isUnexpectedError) {
969
+ break;
970
+ }
971
+ }
972
+ }
905
973
 
906
- return JSON.stringify({ project, agent, available: true }, null, 2);
974
+ // All retries exhausted
975
+ return JSON.stringify(
976
+ {
977
+ error: `Agent Mail init failed after ${MAX_INIT_RETRIES} attempts`,
978
+ available: false,
979
+ lastError: lastError?.message,
980
+ hint: "Manually restart Agent Mail: pkill -f agent-mail && agent-mail serve",
981
+ fallback: "Swarm will continue without multi-agent coordination.",
982
+ },
983
+ null,
984
+ 2,
985
+ );
907
986
  },
908
987
  });
909
988
 
@@ -1270,7 +1349,12 @@ export const agentmail_health = tool({
1270
1349
  try {
1271
1350
  const response = await fetch(`${AGENT_MAIL_URL}/health/liveness`);
1272
1351
  if (response.ok) {
1273
- return "Agent Mail is running";
1352
+ // Also check if MCP is functional
1353
+ const functional = await isServerFunctional();
1354
+ if (functional) {
1355
+ return "Agent Mail is running and functional";
1356
+ }
1357
+ return "Agent Mail health OK but MCP not responding - consider restart";
1274
1358
  }
1275
1359
  return `Agent Mail returned status ${response.status}`;
1276
1360
  } catch (error) {
@@ -1279,6 +1363,73 @@ export const agentmail_health = tool({
1279
1363
  },
1280
1364
  });
1281
1365
 
1366
+ /**
1367
+ * Manually restart Agent Mail server
1368
+ *
1369
+ * Use when server is in bad state (health OK but MCP failing).
1370
+ * This kills the existing process and starts a fresh one.
1371
+ */
1372
+ export const agentmail_restart = tool({
1373
+ description:
1374
+ "Manually restart Agent Mail server (use when getting 'unexpected error')",
1375
+ args: {
1376
+ force: tool.schema
1377
+ .boolean()
1378
+ .optional()
1379
+ .describe(
1380
+ "Force restart even if server appears healthy (default: false)",
1381
+ ),
1382
+ },
1383
+ async execute(args) {
1384
+ // Check if restart is needed
1385
+ if (!args.force) {
1386
+ const functional = await isServerFunctional();
1387
+ if (functional) {
1388
+ return JSON.stringify(
1389
+ {
1390
+ restarted: false,
1391
+ reason: "Server is functional, no restart needed",
1392
+ hint: "Use force=true to restart anyway",
1393
+ },
1394
+ null,
1395
+ 2,
1396
+ );
1397
+ }
1398
+ }
1399
+
1400
+ // Attempt restart
1401
+ console.warn("[agent-mail] Manual restart requested...");
1402
+ const success = await restartServer();
1403
+
1404
+ // Clear caches
1405
+ agentMailAvailable = null;
1406
+ consecutiveFailures = 0;
1407
+
1408
+ if (success) {
1409
+ return JSON.stringify(
1410
+ {
1411
+ restarted: true,
1412
+ success: true,
1413
+ message: "Agent Mail server restarted successfully",
1414
+ },
1415
+ null,
1416
+ 2,
1417
+ );
1418
+ }
1419
+
1420
+ return JSON.stringify(
1421
+ {
1422
+ restarted: true,
1423
+ success: false,
1424
+ error: "Restart attempted but server did not come back up",
1425
+ hint: "Check server logs or manually start: agent-mail serve",
1426
+ },
1427
+ null,
1428
+ 2,
1429
+ );
1430
+ },
1431
+ });
1432
+
1282
1433
  // ============================================================================
1283
1434
  // Export all tools
1284
1435
  // ============================================================================
@@ -1294,6 +1445,7 @@ export const agentMailTools = {
1294
1445
  agentmail_ack: agentmail_ack,
1295
1446
  agentmail_search: agentmail_search,
1296
1447
  agentmail_health: agentmail_health,
1448
+ agentmail_restart: agentmail_restart,
1297
1449
  };
1298
1450
 
1299
1451
  // ============================================================================
package/src/learning.ts CHANGED
@@ -64,6 +64,46 @@ export const CriterionWeightSchema = z.object({
64
64
  });
65
65
  export type CriterionWeight = z.infer<typeof CriterionWeightSchema>;
66
66
 
67
+ /**
68
+ * Error types that can occur during subtask execution
69
+ */
70
+ export const ErrorTypeSchema = z.enum([
71
+ "validation",
72
+ "timeout",
73
+ "conflict",
74
+ "tool_failure",
75
+ "unknown",
76
+ ]);
77
+ export type ErrorType = z.infer<typeof ErrorTypeSchema>;
78
+
79
+ /**
80
+ * An error entry in the error accumulator
81
+ *
82
+ * Errors are accumulated during subtask execution and can be fed
83
+ * into retry prompts to help agents learn from past failures.
84
+ */
85
+ export const ErrorEntrySchema = z.object({
86
+ /** Unique ID for this error entry */
87
+ id: z.string(),
88
+ /** The bead ID this error relates to */
89
+ bead_id: z.string(),
90
+ /** Type of error encountered */
91
+ error_type: ErrorTypeSchema,
92
+ /** Human-readable error message */
93
+ message: z.string(),
94
+ /** Optional stack trace for debugging */
95
+ stack_trace: z.string().optional(),
96
+ /** Tool that failed, if applicable */
97
+ tool_name: z.string().optional(),
98
+ /** When this error occurred */
99
+ timestamp: z.string(), // ISO-8601
100
+ /** Whether this error was resolved */
101
+ resolved: z.boolean().default(false),
102
+ /** Context about what was happening when error occurred */
103
+ context: z.string().optional(),
104
+ });
105
+ export type ErrorEntry = z.infer<typeof ErrorEntrySchema>;
106
+
67
107
  /**
68
108
  * Decomposition strategies for tracking which approach was used
69
109
  */
@@ -437,6 +477,241 @@ export class InMemoryFeedbackStorage implements FeedbackStorage {
437
477
  }
438
478
  }
439
479
 
480
+ // ============================================================================
481
+ // Error Accumulator
482
+ // ============================================================================
483
+
484
+ /**
485
+ * Storage interface for error entries
486
+ *
487
+ * Similar to FeedbackStorage but for tracking errors during execution.
488
+ */
489
+ export interface ErrorStorage {
490
+ /** Store an error entry */
491
+ store(entry: ErrorEntry): Promise<void>;
492
+ /** Get all errors for a bead */
493
+ getByBead(beadId: string): Promise<ErrorEntry[]>;
494
+ /** Get unresolved errors for a bead */
495
+ getUnresolvedByBead(beadId: string): Promise<ErrorEntry[]>;
496
+ /** Mark an error as resolved */
497
+ markResolved(id: string): Promise<void>;
498
+ /** Get all errors */
499
+ getAll(): Promise<ErrorEntry[]>;
500
+ }
501
+
502
+ /**
503
+ * In-memory error storage
504
+ *
505
+ * Accumulates errors during subtask execution for feeding into retry prompts.
506
+ */
507
+ export class InMemoryErrorStorage implements ErrorStorage {
508
+ private errors: ErrorEntry[] = [];
509
+
510
+ async store(entry: ErrorEntry): Promise<void> {
511
+ this.errors.push(entry);
512
+ }
513
+
514
+ async getByBead(beadId: string): Promise<ErrorEntry[]> {
515
+ return this.errors.filter((e) => e.bead_id === beadId);
516
+ }
517
+
518
+ async getUnresolvedByBead(beadId: string): Promise<ErrorEntry[]> {
519
+ return this.errors.filter((e) => e.bead_id === beadId && !e.resolved);
520
+ }
521
+
522
+ async markResolved(id: string): Promise<void> {
523
+ const error = this.errors.find((e) => e.id === id);
524
+ if (error) {
525
+ error.resolved = true;
526
+ }
527
+ }
528
+
529
+ async getAll(): Promise<ErrorEntry[]> {
530
+ return [...this.errors];
531
+ }
532
+ }
533
+
534
+ /**
535
+ * Error accumulator for tracking errors during subtask execution
536
+ *
537
+ * Implements patterns from "Patterns for Building AI Agents" p.40:
538
+ * - Examines and corrects errors when something goes wrong
539
+ * - Feeds error context into retry prompts
540
+ * - Tracks error patterns for learning
541
+ */
542
+ export class ErrorAccumulator {
543
+ private storage: ErrorStorage;
544
+
545
+ constructor(storage?: ErrorStorage) {
546
+ this.storage = storage ?? new InMemoryErrorStorage();
547
+ }
548
+
549
+ /**
550
+ * Record an error during subtask execution
551
+ *
552
+ * @param beadId - Bead ID where error occurred
553
+ * @param errorType - Category of error
554
+ * @param message - Human-readable error message
555
+ * @param options - Additional context (stack trace, tool name, etc.)
556
+ * @returns The created error entry
557
+ */
558
+ async recordError(
559
+ beadId: string,
560
+ errorType: ErrorType,
561
+ message: string,
562
+ options?: {
563
+ stack_trace?: string;
564
+ tool_name?: string;
565
+ context?: string;
566
+ },
567
+ ): Promise<ErrorEntry> {
568
+ const entry: ErrorEntry = {
569
+ id: `${beadId}-${errorType}-${Date.now()}`,
570
+ bead_id: beadId,
571
+ error_type: errorType,
572
+ message,
573
+ stack_trace: options?.stack_trace,
574
+ tool_name: options?.tool_name,
575
+ timestamp: new Date().toISOString(),
576
+ resolved: false,
577
+ context: options?.context,
578
+ };
579
+
580
+ const validated = ErrorEntrySchema.parse(entry);
581
+ await this.storage.store(validated);
582
+
583
+ return validated;
584
+ }
585
+
586
+ /**
587
+ * Get all errors for a bead (resolved and unresolved)
588
+ */
589
+ async getErrors(beadId: string): Promise<ErrorEntry[]> {
590
+ return this.storage.getByBead(beadId);
591
+ }
592
+
593
+ /**
594
+ * Get only unresolved errors for a bead
595
+ */
596
+ async getUnresolvedErrors(beadId: string): Promise<ErrorEntry[]> {
597
+ return this.storage.getUnresolvedByBead(beadId);
598
+ }
599
+
600
+ /**
601
+ * Mark an error as resolved
602
+ */
603
+ async resolveError(errorId: string): Promise<void> {
604
+ await this.storage.markResolved(errorId);
605
+ }
606
+
607
+ /**
608
+ * Format errors as context for retry prompts
609
+ *
610
+ * Groups errors by type and provides structured feedback
611
+ * for the agent to learn from.
612
+ *
613
+ * @param beadId - Bead to get error context for
614
+ * @param includeResolved - Include resolved errors (default: false)
615
+ * @returns Formatted error context string
616
+ */
617
+ async getErrorContext(
618
+ beadId: string,
619
+ includeResolved = false,
620
+ ): Promise<string> {
621
+ const errors = includeResolved
622
+ ? await this.getErrors(beadId)
623
+ : await this.getUnresolvedErrors(beadId);
624
+
625
+ if (errors.length === 0) {
626
+ return "";
627
+ }
628
+
629
+ // Group errors by type
630
+ const byType = errors.reduce(
631
+ (acc, err) => {
632
+ const type = err.error_type;
633
+ if (!acc[type]) {
634
+ acc[type] = [];
635
+ }
636
+ acc[type].push(err);
637
+ return acc;
638
+ },
639
+ {} as Record<ErrorType, ErrorEntry[]>,
640
+ );
641
+
642
+ // Format as structured feedback
643
+ const lines = [
644
+ "## Previous Errors",
645
+ "",
646
+ "The following errors were encountered during execution:",
647
+ "",
648
+ ];
649
+
650
+ for (const [type, typeErrors] of Object.entries(byType)) {
651
+ lines.push(
652
+ `### ${type} (${typeErrors.length} error${typeErrors.length > 1 ? "s" : ""})`,
653
+ );
654
+ lines.push("");
655
+
656
+ for (const err of typeErrors) {
657
+ lines.push(`- **${err.message}**`);
658
+ if (err.context) {
659
+ lines.push(` - Context: ${err.context}`);
660
+ }
661
+ if (err.tool_name) {
662
+ lines.push(` - Tool: ${err.tool_name}`);
663
+ }
664
+ if (err.stack_trace) {
665
+ lines.push(` - Stack: \`${err.stack_trace.slice(0, 100)}...\``);
666
+ }
667
+ lines.push(
668
+ ` - Time: ${new Date(err.timestamp).toLocaleString()}${err.resolved ? " (resolved)" : ""}`,
669
+ );
670
+ lines.push("");
671
+ }
672
+ }
673
+
674
+ lines.push(
675
+ "**Action Required**: Address these errors before proceeding. Consider:",
676
+ );
677
+ lines.push("- What caused each error?");
678
+ lines.push("- How can you prevent similar errors?");
679
+ lines.push("- Are there patterns across error types?");
680
+ lines.push("");
681
+
682
+ return lines.join("\n");
683
+ }
684
+
685
+ /**
686
+ * Get error statistics for outcome tracking
687
+ *
688
+ * @param beadId - Bead to get stats for
689
+ * @returns Error counts and patterns
690
+ */
691
+ async getErrorStats(beadId: string): Promise<{
692
+ total: number;
693
+ unresolved: number;
694
+ by_type: Record<ErrorType, number>;
695
+ }> {
696
+ const allErrors = await this.getErrors(beadId);
697
+ const unresolved = await this.getUnresolvedErrors(beadId);
698
+
699
+ const byType = allErrors.reduce(
700
+ (acc, err) => {
701
+ acc[err.error_type] = (acc[err.error_type] || 0) + 1;
702
+ return acc;
703
+ },
704
+ {} as Record<ErrorType, number>,
705
+ );
706
+
707
+ return {
708
+ total: allErrors.length,
709
+ unresolved: unresolved.length,
710
+ by_type: byType,
711
+ };
712
+ }
713
+ }
714
+
440
715
  // ============================================================================
441
716
  // Exports
442
717
  // ============================================================================
@@ -448,4 +723,6 @@ export const learningSchemas = {
448
723
  OutcomeSignalsSchema,
449
724
  ScoredOutcomeSchema,
450
725
  DecompositionStrategySchema,
726
+ ErrorTypeSchema,
727
+ ErrorEntrySchema,
451
728
  };