opencode-swarm-plugin 0.3.0 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm-plugin",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Multi-agent swarm coordination for OpenCode with learning capabilities, beads integration, and Agent Mail",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -18,13 +18,12 @@
18
18
  "test:watch": "bun test --watch",
19
19
  "test:integration": "bun test src/*.integration.test.ts",
20
20
  "test:all": "bun test",
21
- "docker:test": "docker compose up --build --abort-on-container-exit --exit-code-from test-runner",
22
- "docker:down": "docker compose down -v",
23
21
  "typecheck": "tsc --noEmit",
24
22
  "clean": "rm -rf dist"
25
23
  },
26
24
  "dependencies": {
27
25
  "@opencode-ai/plugin": "^1.0.134",
26
+ "ioredis": "^5.4.1",
28
27
  "zod": "4.1.8"
29
28
  },
30
29
  "devDependencies": {
package/src/agent-mail.ts CHANGED
@@ -17,6 +17,7 @@
17
17
  import { tool } from "@opencode-ai/plugin";
18
18
  import { z } from "zod";
19
19
  import { isToolAvailable, warnMissingTool } from "./tool-availability";
20
+ import { getRateLimiter, type RateLimiter } from "./rate-limiter";
20
21
 
21
22
  // ============================================================================
22
23
  // Configuration
@@ -157,6 +158,24 @@ export class FileReservationConflictError extends Error {
157
158
  }
158
159
  }
159
160
 
161
+ export class RateLimitExceededError extends Error {
162
+ constructor(
163
+ public readonly endpoint: string,
164
+ public readonly remaining: number,
165
+ public readonly resetAt: number,
166
+ ) {
167
+ const resetDate = new Date(resetAt);
168
+ const waitMs = Math.max(0, resetAt - Date.now());
169
+ const waitSec = Math.ceil(waitMs / 1000);
170
+ super(
171
+ `Rate limit exceeded for ${endpoint}. ` +
172
+ `${remaining} remaining. ` +
173
+ `Retry in ${waitSec}s (at ${resetDate.toISOString()})`,
174
+ );
175
+ this.name = "RateLimitExceededError";
176
+ }
177
+ }
178
+
160
179
  // ============================================================================
161
180
  // MCP Client
162
181
  // ============================================================================
@@ -190,6 +209,73 @@ export function resetAgentMailCache(): void {
190
209
  agentMailAvailable = null;
191
210
  }
192
211
 
212
+ /** Cached rate limiter instance */
213
+ let rateLimiter: RateLimiter | null = null;
214
+
215
+ /** Whether rate limiting is enabled (can be disabled via env var) */
216
+ const RATE_LIMITING_ENABLED =
217
+ process.env.OPENCODE_RATE_LIMIT_DISABLED !== "true";
218
+
219
+ /**
220
+ * Check rate limit for an endpoint and throw if exceeded
221
+ *
222
+ * @param agentName - The agent making the request
223
+ * @param endpoint - The endpoint being accessed (e.g., "send", "inbox")
224
+ * @throws RateLimitExceededError if rate limit is exceeded
225
+ */
226
+ async function checkRateLimit(
227
+ agentName: string,
228
+ endpoint: string,
229
+ ): Promise<void> {
230
+ if (!RATE_LIMITING_ENABLED) {
231
+ return;
232
+ }
233
+
234
+ if (!rateLimiter) {
235
+ rateLimiter = await getRateLimiter();
236
+ }
237
+
238
+ const result = await rateLimiter.checkLimit(agentName, endpoint);
239
+ if (!result.allowed) {
240
+ throw new RateLimitExceededError(
241
+ endpoint,
242
+ result.remaining,
243
+ result.resetAt,
244
+ );
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Record a request against the rate limit (call after successful request)
250
+ *
251
+ * @param agentName - The agent making the request
252
+ * @param endpoint - The endpoint being accessed
253
+ */
254
+ async function recordRateLimitedRequest(
255
+ agentName: string,
256
+ endpoint: string,
257
+ ): Promise<void> {
258
+ if (!RATE_LIMITING_ENABLED) {
259
+ return;
260
+ }
261
+
262
+ if (!rateLimiter) {
263
+ rateLimiter = await getRateLimiter();
264
+ }
265
+
266
+ await rateLimiter.recordRequest(agentName, endpoint);
267
+ }
268
+
269
+ /**
270
+ * Reset rate limiter (for testing)
271
+ */
272
+ export async function resetRateLimiterCache(): Promise<void> {
273
+ if (rateLimiter) {
274
+ await rateLimiter.close();
275
+ rateLimiter = null;
276
+ }
277
+ }
278
+
193
279
  /**
194
280
  * Call an Agent Mail MCP tool
195
281
  *
@@ -379,6 +465,9 @@ export const agentmail_send = tool({
379
465
  async execute(args, ctx) {
380
466
  const state = requireState(ctx.sessionID);
381
467
 
468
+ // Check rate limit before sending
469
+ await checkRateLimit(state.agentName, "send");
470
+
382
471
  await mcpCall("send_message", {
383
472
  project_key: state.projectKey,
384
473
  sender_name: state.agentName,
@@ -390,6 +479,9 @@ export const agentmail_send = tool({
390
479
  ack_required: args.ack_required || false,
391
480
  });
392
481
 
482
+ // Record successful request
483
+ await recordRateLimitedRequest(state.agentName, "send");
484
+
393
485
  return `Message sent to ${args.to.join(", ")}`;
394
486
  },
395
487
  });
@@ -417,6 +509,9 @@ export const agentmail_inbox = tool({
417
509
  async execute(args, ctx) {
418
510
  const state = requireState(ctx.sessionID);
419
511
 
512
+ // Check rate limit
513
+ await checkRateLimit(state.agentName, "inbox");
514
+
420
515
  // CRITICAL: Enforce context-safe defaults
421
516
  const limit = Math.min(args.limit || MAX_INBOX_LIMIT, MAX_INBOX_LIMIT);
422
517
 
@@ -429,6 +524,9 @@ export const agentmail_inbox = tool({
429
524
  since_ts: args.since_ts,
430
525
  });
431
526
 
527
+ // Record successful request
528
+ await recordRateLimitedRequest(state.agentName, "inbox");
529
+
432
530
  return JSON.stringify(messages, null, 2);
433
531
  },
434
532
  });
@@ -444,6 +542,9 @@ export const agentmail_read_message = tool({
444
542
  async execute(args, ctx) {
445
543
  const state = requireState(ctx.sessionID);
446
544
 
545
+ // Check rate limit
546
+ await checkRateLimit(state.agentName, "read_message");
547
+
447
548
  // Mark as read
448
549
  await mcpCall("mark_message_read", {
449
550
  project_key: state.projectKey,
@@ -465,6 +566,9 @@ export const agentmail_read_message = tool({
465
566
  return `Message ${args.message_id} not found`;
466
567
  }
467
568
 
569
+ // Record successful request
570
+ await recordRateLimitedRequest(state.agentName, "read_message");
571
+
468
572
  return JSON.stringify(message, null, 2);
469
573
  },
470
574
  });
@@ -484,6 +588,9 @@ export const agentmail_summarize_thread = tool({
484
588
  async execute(args, ctx) {
485
589
  const state = requireState(ctx.sessionID);
486
590
 
591
+ // Check rate limit
592
+ await checkRateLimit(state.agentName, "summarize_thread");
593
+
487
594
  const summary = await mcpCall<ThreadSummary>("summarize_thread", {
488
595
  project_key: state.projectKey,
489
596
  thread_id: args.thread_id,
@@ -491,6 +598,9 @@ export const agentmail_summarize_thread = tool({
491
598
  llm_mode: true, // Use LLM for better summaries
492
599
  });
493
600
 
601
+ // Record successful request
602
+ await recordRateLimitedRequest(state.agentName, "summarize_thread");
603
+
494
604
  return JSON.stringify(summary, null, 2);
495
605
  },
496
606
  });
@@ -520,6 +630,9 @@ export const agentmail_reserve = tool({
520
630
  async execute(args, ctx) {
521
631
  const state = requireState(ctx.sessionID);
522
632
 
633
+ // Check rate limit
634
+ await checkRateLimit(state.agentName, "reserve");
635
+
523
636
  const result = await mcpCall<ReservationResult>("file_reservation_paths", {
524
637
  project_key: state.projectKey,
525
638
  agent_name: state.agentName,
@@ -563,6 +676,9 @@ export const agentmail_reserve = tool({
563
676
  state.reservations = [...state.reservations, ...reservationIds];
564
677
  setState(ctx.sessionID, state);
565
678
 
679
+ // Record successful request
680
+ await recordRateLimitedRequest(state.agentName, "reserve");
681
+
566
682
  if (granted.length === 0) {
567
683
  return "No paths were reserved (empty granted list)";
568
684
  }
@@ -591,6 +707,9 @@ export const agentmail_release = tool({
591
707
  async execute(args, ctx) {
592
708
  const state = requireState(ctx.sessionID);
593
709
 
710
+ // Check rate limit
711
+ await checkRateLimit(state.agentName, "release");
712
+
594
713
  const result = await mcpCall<{ released: number; released_at: string }>(
595
714
  "release_file_reservations",
596
715
  {
@@ -605,6 +724,9 @@ export const agentmail_release = tool({
605
724
  state.reservations = [];
606
725
  setState(ctx.sessionID, state);
607
726
 
727
+ // Record successful request
728
+ await recordRateLimitedRequest(state.agentName, "release");
729
+
608
730
  return `Released ${result.released} reservation(s)`;
609
731
  },
610
732
  });
@@ -620,12 +742,18 @@ export const agentmail_ack = tool({
620
742
  async execute(args, ctx) {
621
743
  const state = requireState(ctx.sessionID);
622
744
 
745
+ // Check rate limit
746
+ await checkRateLimit(state.agentName, "ack");
747
+
623
748
  await mcpCall("acknowledge_message", {
624
749
  project_key: state.projectKey,
625
750
  agent_name: state.agentName,
626
751
  message_id: args.message_id,
627
752
  });
628
753
 
754
+ // Record successful request
755
+ await recordRateLimitedRequest(state.agentName, "ack");
756
+
629
757
  return `Acknowledged message ${args.message_id}`;
630
758
  },
631
759
  });
@@ -647,12 +775,18 @@ export const agentmail_search = tool({
647
775
  async execute(args, ctx) {
648
776
  const state = requireState(ctx.sessionID);
649
777
 
778
+ // Check rate limit
779
+ await checkRateLimit(state.agentName, "search");
780
+
650
781
  const results = await mcpCall<MessageHeader[]>("search_messages", {
651
782
  project_key: state.projectKey,
652
783
  query: args.query,
653
784
  limit: args.limit || 20,
654
785
  });
655
786
 
787
+ // Record successful request
788
+ await recordRateLimitedRequest(state.agentName, "search");
789
+
656
790
  return JSON.stringify(results, null, 2);
657
791
  },
658
792
  });
package/src/index.ts CHANGED
@@ -263,7 +263,9 @@ export {
263
263
  SwarmError,
264
264
  DecompositionError,
265
265
  formatSubtaskPrompt,
266
+ formatSubtaskPromptV2,
266
267
  formatEvaluationPrompt,
268
+ SUBTASK_PROMPT_V2,
267
269
  } from "./swarm";
268
270
 
269
271
  /**