opencode-swarm-plugin 0.3.0 → 0.5.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 +146 -2
- package/README.md +65 -12
- package/bun.lock +23 -0
- package/dist/index.js +9695 -0
- package/dist/plugin.js +9693 -0
- package/examples/commands/swarm.md +50 -0
- 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 +115 -0
- package/src/swarm.ts +276 -0
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
swarm_complete,
|
|
17
17
|
swarm_subtask_prompt,
|
|
18
18
|
swarm_evaluation_prompt,
|
|
19
|
+
formatSubtaskPromptV2,
|
|
20
|
+
SUBTASK_PROMPT_V2,
|
|
19
21
|
} from "./swarm";
|
|
20
22
|
import { mcpCall, setState, clearState, AGENT_MAIL_URL } from "./agent-mail";
|
|
21
23
|
|
|
@@ -952,3 +954,116 @@ describe("Graceful Degradation", () => {
|
|
|
952
954
|
expect(result).toContain("Report progress");
|
|
953
955
|
});
|
|
954
956
|
});
|
|
957
|
+
|
|
958
|
+
// ============================================================================
|
|
959
|
+
// Coordinator-Centric Swarm Tools (V2)
|
|
960
|
+
// ============================================================================
|
|
961
|
+
|
|
962
|
+
describe("Swarm Prompt V2 (with Agent Mail/Beads)", () => {
|
|
963
|
+
describe("formatSubtaskPromptV2", () => {
|
|
964
|
+
it("generates correct prompt with all fields", () => {
|
|
965
|
+
const result = formatSubtaskPromptV2({
|
|
966
|
+
bead_id: "bd-123.1",
|
|
967
|
+
epic_id: "bd-123",
|
|
968
|
+
subtask_title: "Add OAuth provider",
|
|
969
|
+
subtask_description: "Configure Google OAuth in the auth config",
|
|
970
|
+
files: ["src/auth/google.ts", "src/auth/config.ts"],
|
|
971
|
+
shared_context: "We are using NextAuth.js v5",
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
// Check title is included
|
|
975
|
+
expect(result).toContain("Add OAuth provider");
|
|
976
|
+
|
|
977
|
+
// Check description is included
|
|
978
|
+
expect(result).toContain("Configure Google OAuth in the auth config");
|
|
979
|
+
|
|
980
|
+
// Check files are formatted as list
|
|
981
|
+
expect(result).toContain("- `src/auth/google.ts`");
|
|
982
|
+
expect(result).toContain("- `src/auth/config.ts`");
|
|
983
|
+
|
|
984
|
+
// Check shared context is included
|
|
985
|
+
expect(result).toContain("We are using NextAuth.js v5");
|
|
986
|
+
|
|
987
|
+
// Check bead/epic IDs are substituted
|
|
988
|
+
expect(result).toContain("bd-123.1");
|
|
989
|
+
expect(result).toContain("bd-123");
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
it("handles missing optional fields", () => {
|
|
993
|
+
const result = formatSubtaskPromptV2({
|
|
994
|
+
bead_id: "bd-456.1",
|
|
995
|
+
epic_id: "bd-456",
|
|
996
|
+
subtask_title: "Simple task",
|
|
997
|
+
subtask_description: "",
|
|
998
|
+
files: [],
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
// Check title is included
|
|
1002
|
+
expect(result).toContain("Simple task");
|
|
1003
|
+
|
|
1004
|
+
// Check fallback for empty description
|
|
1005
|
+
expect(result).toContain("(see title)");
|
|
1006
|
+
|
|
1007
|
+
// Check fallback for empty files
|
|
1008
|
+
expect(result).toContain("(no specific files - use judgment)");
|
|
1009
|
+
|
|
1010
|
+
// Check fallback for missing context
|
|
1011
|
+
expect(result).toContain("(none)");
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
it("handles files with special characters", () => {
|
|
1015
|
+
const result = formatSubtaskPromptV2({
|
|
1016
|
+
bead_id: "bd-789.1",
|
|
1017
|
+
epic_id: "bd-789",
|
|
1018
|
+
subtask_title: "Handle paths",
|
|
1019
|
+
subtask_description: "Test file paths",
|
|
1020
|
+
files: [
|
|
1021
|
+
"src/components/[slug]/page.tsx",
|
|
1022
|
+
"src/api/users/[id]/route.ts",
|
|
1023
|
+
],
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
expect(result).toContain("- `src/components/[slug]/page.tsx`");
|
|
1027
|
+
expect(result).toContain("- `src/api/users/[id]/route.ts`");
|
|
1028
|
+
});
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
describe("SUBTASK_PROMPT_V2", () => {
|
|
1032
|
+
it("contains expected sections", () => {
|
|
1033
|
+
// Check all main sections are present in the template
|
|
1034
|
+
expect(SUBTASK_PROMPT_V2).toContain("## Task");
|
|
1035
|
+
expect(SUBTASK_PROMPT_V2).toContain("{subtask_title}");
|
|
1036
|
+
expect(SUBTASK_PROMPT_V2).toContain("{subtask_description}");
|
|
1037
|
+
|
|
1038
|
+
expect(SUBTASK_PROMPT_V2).toContain("## Files");
|
|
1039
|
+
expect(SUBTASK_PROMPT_V2).toContain("{file_list}");
|
|
1040
|
+
|
|
1041
|
+
expect(SUBTASK_PROMPT_V2).toContain("## Context");
|
|
1042
|
+
expect(SUBTASK_PROMPT_V2).toContain("{shared_context}");
|
|
1043
|
+
|
|
1044
|
+
expect(SUBTASK_PROMPT_V2).toContain("## Workflow");
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
it("DOES contain Agent Mail instructions", () => {
|
|
1048
|
+
// V2 prompt tells agents to USE Agent Mail
|
|
1049
|
+
expect(SUBTASK_PROMPT_V2).toContain("Agent Mail");
|
|
1050
|
+
expect(SUBTASK_PROMPT_V2).toContain("agentmail_send");
|
|
1051
|
+
expect(SUBTASK_PROMPT_V2).toContain("thread_id");
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
it("DOES contain beads instructions", () => {
|
|
1055
|
+
// V2 prompt tells agents to USE beads
|
|
1056
|
+
expect(SUBTASK_PROMPT_V2).toContain("{bead_id}");
|
|
1057
|
+
expect(SUBTASK_PROMPT_V2).toContain("{epic_id}");
|
|
1058
|
+
expect(SUBTASK_PROMPT_V2).toContain("beads_update");
|
|
1059
|
+
expect(SUBTASK_PROMPT_V2).toContain("beads_create");
|
|
1060
|
+
expect(SUBTASK_PROMPT_V2).toContain("swarm_complete");
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
it("instructs agents to communicate", () => {
|
|
1064
|
+
expect(SUBTASK_PROMPT_V2).toContain("Never work silently");
|
|
1065
|
+
expect(SUBTASK_PROMPT_V2).toContain("Report progress");
|
|
1066
|
+
expect(SUBTASK_PROMPT_V2).toContain("coordinator");
|
|
1067
|
+
});
|
|
1068
|
+
});
|
|
1069
|
+
});
|
package/src/swarm.ts
CHANGED
|
@@ -359,6 +359,87 @@ Before writing code:
|
|
|
359
359
|
|
|
360
360
|
Begin work on your subtask now.`;
|
|
361
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Streamlined subtask prompt (V2) - still uses Agent Mail and beads
|
|
364
|
+
*
|
|
365
|
+
* This is a cleaner version of SUBTASK_PROMPT that's easier to parse.
|
|
366
|
+
* Agents MUST use Agent Mail for communication and beads for tracking.
|
|
367
|
+
*/
|
|
368
|
+
export const SUBTASK_PROMPT_V2 = `You are a swarm agent working on: **{subtask_title}**
|
|
369
|
+
|
|
370
|
+
## Identity
|
|
371
|
+
- **Bead ID**: {bead_id}
|
|
372
|
+
- **Epic ID**: {epic_id}
|
|
373
|
+
|
|
374
|
+
## Task
|
|
375
|
+
{subtask_description}
|
|
376
|
+
|
|
377
|
+
## Files (exclusive reservation)
|
|
378
|
+
{file_list}
|
|
379
|
+
|
|
380
|
+
Only modify these files. Need others? Message the coordinator.
|
|
381
|
+
|
|
382
|
+
## Context
|
|
383
|
+
{shared_context}
|
|
384
|
+
|
|
385
|
+
## MANDATORY: Use These Tools
|
|
386
|
+
|
|
387
|
+
### Agent Mail - communicate with the swarm
|
|
388
|
+
\`\`\`typescript
|
|
389
|
+
// Report progress, ask questions, announce blockers
|
|
390
|
+
agentmail_send({
|
|
391
|
+
to: ["coordinator"],
|
|
392
|
+
subject: "Progress update",
|
|
393
|
+
body: "What you did or need",
|
|
394
|
+
thread_id: "{epic_id}"
|
|
395
|
+
})
|
|
396
|
+
\`\`\`
|
|
397
|
+
|
|
398
|
+
### Beads - track your work
|
|
399
|
+
- **Blocked?** \`beads_update({ id: "{bead_id}", status: "blocked" })\`
|
|
400
|
+
- **Found bug?** \`beads_create({ title: "Bug description", type: "bug" })\`
|
|
401
|
+
- **Done?** \`swarm_complete({ bead_id: "{bead_id}", summary: "What you did", files_touched: [...] })\`
|
|
402
|
+
|
|
403
|
+
## Workflow
|
|
404
|
+
|
|
405
|
+
1. **Read** the files first
|
|
406
|
+
2. **Plan** your approach (message coordinator if complex)
|
|
407
|
+
3. **Implement** the changes
|
|
408
|
+
4. **Verify** (typecheck, tests)
|
|
409
|
+
5. **Report** progress via Agent Mail
|
|
410
|
+
6. **Complete** with swarm_complete when done
|
|
411
|
+
|
|
412
|
+
**Never work silently.** Communicate progress and blockers immediately.
|
|
413
|
+
|
|
414
|
+
Begin now.`;
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Format the V2 subtask prompt for a specific agent
|
|
418
|
+
*/
|
|
419
|
+
export function formatSubtaskPromptV2(params: {
|
|
420
|
+
bead_id: string;
|
|
421
|
+
epic_id: string;
|
|
422
|
+
subtask_title: string;
|
|
423
|
+
subtask_description: string;
|
|
424
|
+
files: string[];
|
|
425
|
+
shared_context?: string;
|
|
426
|
+
}): string {
|
|
427
|
+
const fileList =
|
|
428
|
+
params.files.length > 0
|
|
429
|
+
? params.files.map((f) => `- \`${f}\``).join("\n")
|
|
430
|
+
: "(no specific files - use judgment)";
|
|
431
|
+
|
|
432
|
+
return SUBTASK_PROMPT_V2.replace(/{bead_id}/g, params.bead_id)
|
|
433
|
+
.replace(/{epic_id}/g, params.epic_id)
|
|
434
|
+
.replace("{subtask_title}", params.subtask_title)
|
|
435
|
+
.replace(
|
|
436
|
+
"{subtask_description}",
|
|
437
|
+
params.subtask_description || "(see title)",
|
|
438
|
+
)
|
|
439
|
+
.replace("{file_list}", fileList)
|
|
440
|
+
.replace("{shared_context}", params.shared_context || "(none)");
|
|
441
|
+
}
|
|
442
|
+
|
|
362
443
|
/**
|
|
363
444
|
* Prompt for self-evaluation before completing a subtask.
|
|
364
445
|
*
|
|
@@ -1418,6 +1499,199 @@ export const swarm_subtask_prompt = tool({
|
|
|
1418
1499
|
},
|
|
1419
1500
|
});
|
|
1420
1501
|
|
|
1502
|
+
/**
|
|
1503
|
+
* Prepare a subtask for spawning with Task tool (V2 prompt)
|
|
1504
|
+
*
|
|
1505
|
+
* Generates a streamlined prompt that tells agents to USE Agent Mail and beads.
|
|
1506
|
+
* Returns JSON that can be directly used with Task tool.
|
|
1507
|
+
*/
|
|
1508
|
+
export const swarm_spawn_subtask = tool({
|
|
1509
|
+
description:
|
|
1510
|
+
"Prepare a subtask for spawning. Returns prompt with Agent Mail/beads instructions.",
|
|
1511
|
+
args: {
|
|
1512
|
+
bead_id: tool.schema.string().describe("Subtask bead ID"),
|
|
1513
|
+
epic_id: tool.schema.string().describe("Parent epic bead ID"),
|
|
1514
|
+
subtask_title: tool.schema.string().describe("Subtask title"),
|
|
1515
|
+
subtask_description: tool.schema
|
|
1516
|
+
.string()
|
|
1517
|
+
.optional()
|
|
1518
|
+
.describe("Detailed subtask instructions"),
|
|
1519
|
+
files: tool.schema
|
|
1520
|
+
.array(tool.schema.string())
|
|
1521
|
+
.describe("Files assigned to this subtask"),
|
|
1522
|
+
shared_context: tool.schema
|
|
1523
|
+
.string()
|
|
1524
|
+
.optional()
|
|
1525
|
+
.describe("Context shared across all agents"),
|
|
1526
|
+
},
|
|
1527
|
+
async execute(args) {
|
|
1528
|
+
const prompt = formatSubtaskPromptV2({
|
|
1529
|
+
bead_id: args.bead_id,
|
|
1530
|
+
epic_id: args.epic_id,
|
|
1531
|
+
subtask_title: args.subtask_title,
|
|
1532
|
+
subtask_description: args.subtask_description || "",
|
|
1533
|
+
files: args.files,
|
|
1534
|
+
shared_context: args.shared_context,
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
return JSON.stringify(
|
|
1538
|
+
{
|
|
1539
|
+
prompt,
|
|
1540
|
+
bead_id: args.bead_id,
|
|
1541
|
+
epic_id: args.epic_id,
|
|
1542
|
+
files: args.files,
|
|
1543
|
+
},
|
|
1544
|
+
null,
|
|
1545
|
+
2,
|
|
1546
|
+
);
|
|
1547
|
+
},
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1550
|
+
/**
|
|
1551
|
+
* Schema for task agent result
|
|
1552
|
+
*/
|
|
1553
|
+
const TaskResultSchema = z.object({
|
|
1554
|
+
success: z.boolean(),
|
|
1555
|
+
summary: z.string(),
|
|
1556
|
+
files_modified: z.array(z.string()).optional().default([]),
|
|
1557
|
+
files_created: z.array(z.string()).optional().default([]),
|
|
1558
|
+
issues_found: z.array(z.string()).optional().default([]),
|
|
1559
|
+
tests_passed: z.boolean().optional(),
|
|
1560
|
+
notes: z.string().optional(),
|
|
1561
|
+
blocker: z.string().optional(),
|
|
1562
|
+
suggestions: z.array(z.string()).optional(),
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
type TaskResult = z.infer<typeof TaskResultSchema>;
|
|
1566
|
+
|
|
1567
|
+
/**
|
|
1568
|
+
* Handle subtask completion from a Task agent
|
|
1569
|
+
*
|
|
1570
|
+
* This tool is for coordinators to process the result after a Task subagent
|
|
1571
|
+
* returns. It parses the JSON result, closes the bead on success, and
|
|
1572
|
+
* creates new beads for any issues discovered.
|
|
1573
|
+
*
|
|
1574
|
+
* @example
|
|
1575
|
+
* // Task agent returns JSON:
|
|
1576
|
+
* // { "success": true, "summary": "Added auth", "files_modified": ["src/auth.ts"], "issues_found": ["Missing tests"] }
|
|
1577
|
+
* //
|
|
1578
|
+
* // Coordinator calls:
|
|
1579
|
+
* swarm_complete_subtask(bead_id="bd-123.1", task_result=<agent_response>)
|
|
1580
|
+
*/
|
|
1581
|
+
export const swarm_complete_subtask = tool({
|
|
1582
|
+
description:
|
|
1583
|
+
"Handle subtask completion after Task agent returns. Parses result JSON, closes bead on success, creates new beads for issues found.",
|
|
1584
|
+
args: {
|
|
1585
|
+
bead_id: z.string().describe("Subtask bead ID to close"),
|
|
1586
|
+
task_result: z
|
|
1587
|
+
.string()
|
|
1588
|
+
.describe("JSON result from the Task agent (TaskResult schema)"),
|
|
1589
|
+
files_touched: z
|
|
1590
|
+
.array(z.string())
|
|
1591
|
+
.optional()
|
|
1592
|
+
.describe(
|
|
1593
|
+
"Override files touched (uses task_result.files_modified if not provided)",
|
|
1594
|
+
),
|
|
1595
|
+
},
|
|
1596
|
+
async execute(args) {
|
|
1597
|
+
// Parse the task result JSON
|
|
1598
|
+
let result: TaskResult;
|
|
1599
|
+
try {
|
|
1600
|
+
const parsed = JSON.parse(args.task_result);
|
|
1601
|
+
result = TaskResultSchema.parse(parsed);
|
|
1602
|
+
} catch (error) {
|
|
1603
|
+
// Handle parse errors gracefully
|
|
1604
|
+
const errorMessage =
|
|
1605
|
+
error instanceof SyntaxError
|
|
1606
|
+
? `Invalid JSON: ${error.message}`
|
|
1607
|
+
: error instanceof z.ZodError
|
|
1608
|
+
? `Schema validation failed: ${error.issues.map((i) => i.message).join(", ")}`
|
|
1609
|
+
: String(error);
|
|
1610
|
+
|
|
1611
|
+
return JSON.stringify(
|
|
1612
|
+
{
|
|
1613
|
+
success: false,
|
|
1614
|
+
error: "Failed to parse task result",
|
|
1615
|
+
details: errorMessage,
|
|
1616
|
+
hint: "Task agent should return JSON matching TaskResult schema: { success, summary, files_modified?, issues_found?, ... }",
|
|
1617
|
+
},
|
|
1618
|
+
null,
|
|
1619
|
+
2,
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
const filesTouched = args.files_touched ?? [
|
|
1624
|
+
...result.files_modified,
|
|
1625
|
+
...result.files_created,
|
|
1626
|
+
];
|
|
1627
|
+
const issuesCreated: Array<{ title: string; id?: string }> = [];
|
|
1628
|
+
|
|
1629
|
+
// If task failed, don't close the bead - return info for coordinator to handle
|
|
1630
|
+
if (!result.success) {
|
|
1631
|
+
return JSON.stringify(
|
|
1632
|
+
{
|
|
1633
|
+
success: false,
|
|
1634
|
+
bead_id: args.bead_id,
|
|
1635
|
+
task_failed: true,
|
|
1636
|
+
summary: result.summary,
|
|
1637
|
+
blocker: result.blocker,
|
|
1638
|
+
suggestions: result.suggestions,
|
|
1639
|
+
files_touched: filesTouched,
|
|
1640
|
+
action_needed:
|
|
1641
|
+
"Task failed - review blocker and decide whether to retry or close as failed",
|
|
1642
|
+
},
|
|
1643
|
+
null,
|
|
1644
|
+
2,
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
// Task succeeded - close the bead
|
|
1649
|
+
const closeReason = result.summary.slice(0, 200); // Truncate for safety
|
|
1650
|
+
await Bun.$`bd close ${args.bead_id} -r "${closeReason}"`.quiet().nothrow();
|
|
1651
|
+
|
|
1652
|
+
// Create new beads for each issue found
|
|
1653
|
+
if (result.issues_found.length > 0) {
|
|
1654
|
+
for (const issue of result.issues_found) {
|
|
1655
|
+
const issueTitle = issue.slice(0, 100); // Truncate long titles
|
|
1656
|
+
const createResult = await Bun.$`bd create "${issueTitle}" -t bug`
|
|
1657
|
+
.quiet()
|
|
1658
|
+
.nothrow();
|
|
1659
|
+
|
|
1660
|
+
if (createResult.exitCode === 0) {
|
|
1661
|
+
// Try to parse the bead ID from output
|
|
1662
|
+
const output = createResult.stdout.toString();
|
|
1663
|
+
const idMatch = output.match(/bd-[a-z0-9]+/);
|
|
1664
|
+
issuesCreated.push({
|
|
1665
|
+
title: issueTitle,
|
|
1666
|
+
id: idMatch?.[0],
|
|
1667
|
+
});
|
|
1668
|
+
} else {
|
|
1669
|
+
issuesCreated.push({
|
|
1670
|
+
title: issueTitle,
|
|
1671
|
+
id: undefined, // Failed to create
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
return JSON.stringify(
|
|
1678
|
+
{
|
|
1679
|
+
success: true,
|
|
1680
|
+
bead_id: args.bead_id,
|
|
1681
|
+
bead_closed: true,
|
|
1682
|
+
summary: result.summary,
|
|
1683
|
+
files_touched: filesTouched,
|
|
1684
|
+
tests_passed: result.tests_passed,
|
|
1685
|
+
notes: result.notes,
|
|
1686
|
+
issues_created: issuesCreated.length > 0 ? issuesCreated : undefined,
|
|
1687
|
+
issues_count: issuesCreated.length,
|
|
1688
|
+
},
|
|
1689
|
+
null,
|
|
1690
|
+
2,
|
|
1691
|
+
);
|
|
1692
|
+
},
|
|
1693
|
+
});
|
|
1694
|
+
|
|
1421
1695
|
/**
|
|
1422
1696
|
* Generate self-evaluation prompt
|
|
1423
1697
|
*/
|
|
@@ -1560,5 +1834,7 @@ export const swarmTools = {
|
|
|
1560
1834
|
swarm_complete: swarm_complete,
|
|
1561
1835
|
swarm_record_outcome: swarm_record_outcome,
|
|
1562
1836
|
swarm_subtask_prompt: swarm_subtask_prompt,
|
|
1837
|
+
swarm_spawn_subtask: swarm_spawn_subtask,
|
|
1838
|
+
swarm_complete_subtask: swarm_complete_subtask,
|
|
1563
1839
|
swarm_evaluation_prompt: swarm_evaluation_prompt,
|
|
1564
1840
|
};
|