opencode-swarm-plugin 0.2.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/.beads/issues.jsonl +115 -5
- package/README.md +38 -1
- package/bun.lock +23 -0
- package/dist/index.js +9697 -2
- package/dist/plugin.js +9695 -2
- package/package.json +2 -3
- package/src/agent-mail.ts +134 -0
- package/src/index.ts +2 -0
- package/src/rate-limiter.integration.test.ts +466 -0
- package/src/rate-limiter.ts +656 -0
- package/src/swarm.integration.test.ts +126 -0
- package/src/swarm.ts +278 -0
- package/src/tool-availability.ts +3 -2
- package/Dockerfile +0 -30
- package/docker/agent-mail/Dockerfile +0 -23
- package/docker/agent-mail/__pycache__/server.cpython-314.pyc +0 -0
- package/docker/agent-mail/requirements.txt +0 -3
- package/docker/agent-mail/server.py +0 -879
- package/docker-compose.yml +0 -45
- package/scripts/docker-entrypoint.sh +0 -54
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-swarm-plugin",
|
|
3
|
-
"version": "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
|
});
|