opencode-swarm-plugin 0.35.0 → 0.36.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/.turbo/turbo-test.log +333 -333
- package/CHANGELOG.md +62 -0
- package/examples/plugin-wrapper-template.ts +447 -33
- package/package.json +1 -1
- package/src/compaction-hook.test.ts +226 -280
- package/src/compaction-hook.ts +190 -42
- package/src/eval-capture.ts +5 -6
- package/src/index.ts +21 -1
- package/src/learning.integration.test.ts +0 -2
- package/src/schemas/task.ts +0 -1
- package/src/swarm-decompose.ts +1 -8
- package/src/swarm.integration.test.ts +0 -40
package/src/compaction-hook.ts
CHANGED
|
@@ -236,29 +236,30 @@ interface ToolPart {
|
|
|
236
236
|
/**
|
|
237
237
|
* Tool state (completed tools have input/output we need)
|
|
238
238
|
*/
|
|
239
|
-
type ToolState =
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
239
|
+
type ToolState =
|
|
240
|
+
| {
|
|
241
|
+
status: "completed";
|
|
242
|
+
input: { [key: string]: unknown };
|
|
243
|
+
output: string;
|
|
244
|
+
title: string;
|
|
245
|
+
metadata: { [key: string]: unknown };
|
|
246
|
+
time: { start: number; end: number };
|
|
247
|
+
}
|
|
248
|
+
| {
|
|
249
|
+
status: string;
|
|
250
|
+
[key: string]: unknown;
|
|
251
|
+
};
|
|
250
252
|
|
|
251
253
|
/**
|
|
252
254
|
* SDK Client type (minimal interface for scanSessionMessages)
|
|
255
|
+
*
|
|
256
|
+
* The actual SDK client uses a more complex Options-based API:
|
|
257
|
+
* client.session.messages({ path: { id: sessionID }, query: { limit } })
|
|
258
|
+
*
|
|
259
|
+
* We accept `unknown` and handle the type internally to avoid
|
|
260
|
+
* tight coupling to SDK internals.
|
|
253
261
|
*/
|
|
254
|
-
|
|
255
|
-
session: {
|
|
256
|
-
messages: (opts: { sessionID: string; limit?: number }) => Promise<{
|
|
257
|
-
info: { id: string; sessionID: string };
|
|
258
|
-
parts: ToolPart[];
|
|
259
|
-
}[]>;
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
+
export type OpencodeClient = unknown;
|
|
262
263
|
|
|
263
264
|
/**
|
|
264
265
|
* Scanned swarm state extracted from session messages
|
|
@@ -268,29 +269,32 @@ export interface ScannedSwarmState {
|
|
|
268
269
|
epicTitle?: string;
|
|
269
270
|
projectPath?: string;
|
|
270
271
|
agentName?: string;
|
|
271
|
-
subtasks: Map<
|
|
272
|
+
subtasks: Map<
|
|
273
|
+
string,
|
|
274
|
+
{ title: string; status: string; worker?: string; files?: string[] }
|
|
275
|
+
>;
|
|
272
276
|
lastAction?: { tool: string; args: unknown; timestamp: number };
|
|
273
277
|
}
|
|
274
278
|
|
|
275
279
|
/**
|
|
276
280
|
* Scan session messages for swarm state using SDK client
|
|
277
|
-
*
|
|
281
|
+
*
|
|
278
282
|
* Extracts swarm coordination state from actual tool calls:
|
|
279
283
|
* - swarm_spawn_subtask → subtask tracking
|
|
280
284
|
* - swarmmail_init → agent name, project path
|
|
281
285
|
* - hive_create_epic → epic ID and title
|
|
282
286
|
* - swarm_status → epic reference
|
|
283
287
|
* - swarm_complete → subtask completion
|
|
284
|
-
*
|
|
288
|
+
*
|
|
285
289
|
* @param client - OpenCode SDK client (undefined if not available)
|
|
286
290
|
* @param sessionID - Session to scan
|
|
287
291
|
* @param limit - Max messages to fetch (default 100)
|
|
288
292
|
* @returns Extracted swarm state
|
|
289
293
|
*/
|
|
290
294
|
export async function scanSessionMessages(
|
|
291
|
-
client: OpencodeClient
|
|
295
|
+
client: OpencodeClient,
|
|
292
296
|
sessionID: string,
|
|
293
|
-
limit: number = 100
|
|
297
|
+
limit: number = 100,
|
|
294
298
|
): Promise<ScannedSwarmState> {
|
|
295
299
|
const state: ScannedSwarmState = {
|
|
296
300
|
subtasks: new Map(),
|
|
@@ -301,7 +305,22 @@ export async function scanSessionMessages(
|
|
|
301
305
|
}
|
|
302
306
|
|
|
303
307
|
try {
|
|
304
|
-
|
|
308
|
+
// SDK client uses Options-based API: { path: { id }, query: { limit } }
|
|
309
|
+
const sdkClient = client as {
|
|
310
|
+
session: {
|
|
311
|
+
messages: (opts: {
|
|
312
|
+
path: { id: string };
|
|
313
|
+
query?: { limit?: number };
|
|
314
|
+
}) => Promise<{ data?: Array<{ info: unknown; parts: ToolPart[] }> }>;
|
|
315
|
+
};
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const response = await sdkClient.session.messages({
|
|
319
|
+
path: { id: sessionID },
|
|
320
|
+
query: { limit },
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const messages = response.data || [];
|
|
305
324
|
|
|
306
325
|
for (const message of messages) {
|
|
307
326
|
for (const part of message.parts) {
|
|
@@ -310,7 +329,10 @@ export async function scanSessionMessages(
|
|
|
310
329
|
}
|
|
311
330
|
|
|
312
331
|
const { tool, state: toolState } = part;
|
|
313
|
-
const { input, output, time } = toolState as Extract<
|
|
332
|
+
const { input, output, time } = toolState as Extract<
|
|
333
|
+
ToolState,
|
|
334
|
+
{ status: "completed" }
|
|
335
|
+
>;
|
|
314
336
|
|
|
315
337
|
// Track last action
|
|
316
338
|
state.lastAction = {
|
|
@@ -407,12 +429,102 @@ export async function scanSessionMessages(
|
|
|
407
429
|
}
|
|
408
430
|
}
|
|
409
431
|
} catch (error) {
|
|
432
|
+
getLog().debug(
|
|
433
|
+
{
|
|
434
|
+
error: error instanceof Error ? error.message : String(error),
|
|
435
|
+
},
|
|
436
|
+
"SDK message scanning failed",
|
|
437
|
+
);
|
|
410
438
|
// SDK not available or error fetching messages - return what we have
|
|
411
439
|
}
|
|
412
440
|
|
|
413
441
|
return state;
|
|
414
442
|
}
|
|
415
443
|
|
|
444
|
+
/**
|
|
445
|
+
* Build dynamic swarm state from scanned messages (more precise than hive detection)
|
|
446
|
+
*/
|
|
447
|
+
function buildDynamicSwarmStateFromScanned(
|
|
448
|
+
scanned: ScannedSwarmState,
|
|
449
|
+
detected: SwarmState,
|
|
450
|
+
): string {
|
|
451
|
+
const parts: string[] = [];
|
|
452
|
+
|
|
453
|
+
parts.push("## 🐝 Current Swarm State\n");
|
|
454
|
+
|
|
455
|
+
// Prefer scanned data over detected
|
|
456
|
+
const epicId = scanned.epicId || detected.epicId;
|
|
457
|
+
const epicTitle = scanned.epicTitle || detected.epicTitle;
|
|
458
|
+
const projectPath = scanned.projectPath || detected.projectPath;
|
|
459
|
+
|
|
460
|
+
if (epicId) {
|
|
461
|
+
parts.push(`**Epic:** ${epicId}${epicTitle ? ` - ${epicTitle}` : ""}`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (scanned.agentName) {
|
|
465
|
+
parts.push(`**Coordinator:** ${scanned.agentName}`);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
parts.push(`**Project:** ${projectPath}`);
|
|
469
|
+
|
|
470
|
+
// Show detailed subtask info from scanned state
|
|
471
|
+
if (scanned.subtasks.size > 0) {
|
|
472
|
+
parts.push(`\n**Subtasks:**`);
|
|
473
|
+
for (const [id, subtask] of scanned.subtasks) {
|
|
474
|
+
const status = subtask.status === "completed" ? "✓" : `[${subtask.status}]`;
|
|
475
|
+
const worker = subtask.worker ? ` → ${subtask.worker}` : "";
|
|
476
|
+
const files = subtask.files?.length ? ` (${subtask.files.join(", ")})` : "";
|
|
477
|
+
parts.push(` - ${id}: ${subtask.title} ${status}${worker}${files}`);
|
|
478
|
+
}
|
|
479
|
+
} else if (detected.subtasks) {
|
|
480
|
+
// Fall back to counts from hive detection
|
|
481
|
+
const total =
|
|
482
|
+
detected.subtasks.closed +
|
|
483
|
+
detected.subtasks.in_progress +
|
|
484
|
+
detected.subtasks.open +
|
|
485
|
+
detected.subtasks.blocked;
|
|
486
|
+
|
|
487
|
+
if (total > 0) {
|
|
488
|
+
parts.push(`**Subtasks:**`);
|
|
489
|
+
if (detected.subtasks.closed > 0)
|
|
490
|
+
parts.push(` - ${detected.subtasks.closed} closed`);
|
|
491
|
+
if (detected.subtasks.in_progress > 0)
|
|
492
|
+
parts.push(` - ${detected.subtasks.in_progress} in_progress`);
|
|
493
|
+
if (detected.subtasks.open > 0)
|
|
494
|
+
parts.push(` - ${detected.subtasks.open} open`);
|
|
495
|
+
if (detected.subtasks.blocked > 0)
|
|
496
|
+
parts.push(` - ${detected.subtasks.blocked} blocked`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Show last action if available
|
|
501
|
+
if (scanned.lastAction) {
|
|
502
|
+
parts.push(`\n**Last Action:** \`${scanned.lastAction.tool}\``);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (epicId) {
|
|
506
|
+
parts.push(`\n## 🎯 YOU ARE THE COORDINATOR`);
|
|
507
|
+
parts.push(``);
|
|
508
|
+
parts.push(
|
|
509
|
+
`**Primary role:** Orchestrate workers, review their output, unblock dependencies.`,
|
|
510
|
+
);
|
|
511
|
+
parts.push(`**Spawn workers** for implementation tasks - don't do them yourself.`);
|
|
512
|
+
parts.push(``);
|
|
513
|
+
parts.push(`**RESUME STEPS:**`);
|
|
514
|
+
parts.push(
|
|
515
|
+
`1. Check swarm status: \`swarm_status(epic_id="${epicId}", project_key="${projectPath}")\``,
|
|
516
|
+
);
|
|
517
|
+
parts.push(`2. Check inbox for worker messages: \`swarmmail_inbox(limit=5)\``);
|
|
518
|
+
parts.push(
|
|
519
|
+
`3. For in_progress subtasks: Review worker results with \`swarm_review\``,
|
|
520
|
+
);
|
|
521
|
+
parts.push(`4. For open subtasks: Spawn workers with \`swarm_spawn_subtask\``);
|
|
522
|
+
parts.push(`5. For blocked subtasks: Investigate and unblock`);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return parts.join("\n");
|
|
526
|
+
}
|
|
527
|
+
|
|
416
528
|
// ============================================================================
|
|
417
529
|
// Swarm Detection
|
|
418
530
|
// ============================================================================
|
|
@@ -678,17 +790,21 @@ async function detectSwarm(): Promise<SwarmDetection> {
|
|
|
678
790
|
* Philosophy: Err on the side of continuation. A false positive costs
|
|
679
791
|
* a bit of context space. A false negative loses the swarm.
|
|
680
792
|
*
|
|
793
|
+
* @param client - Optional OpenCode SDK client for scanning session messages.
|
|
794
|
+
* When provided, extracts PRECISE swarm state from actual tool calls.
|
|
795
|
+
* When undefined, falls back to hive/swarm-mail heuristic detection.
|
|
796
|
+
*
|
|
681
797
|
* @example
|
|
682
798
|
* ```typescript
|
|
683
799
|
* import { createCompactionHook } from "opencode-swarm-plugin";
|
|
684
800
|
*
|
|
685
|
-
* export const SwarmPlugin: Plugin = async () => ({
|
|
801
|
+
* export const SwarmPlugin: Plugin = async (input) => ({
|
|
686
802
|
* tool: { ... },
|
|
687
|
-
* "experimental.session.compacting": createCompactionHook(),
|
|
803
|
+
* "experimental.session.compacting": createCompactionHook(input.client),
|
|
688
804
|
* });
|
|
689
805
|
* ```
|
|
690
806
|
*/
|
|
691
|
-
export function createCompactionHook() {
|
|
807
|
+
export function createCompactionHook(client?: OpencodeClient) {
|
|
692
808
|
return async (
|
|
693
809
|
input: { sessionID: string },
|
|
694
810
|
output: { context: string[] },
|
|
@@ -699,41 +815,73 @@ export function createCompactionHook() {
|
|
|
699
815
|
{
|
|
700
816
|
session_id: input.sessionID,
|
|
701
817
|
trigger: "session_compaction",
|
|
818
|
+
has_sdk_client: !!client,
|
|
702
819
|
},
|
|
703
820
|
"compaction started",
|
|
704
821
|
);
|
|
705
822
|
|
|
706
823
|
try {
|
|
824
|
+
// Scan session messages for precise swarm state (if client available)
|
|
825
|
+
const scannedState = await scanSessionMessages(client, input.sessionID);
|
|
826
|
+
|
|
827
|
+
// Also run heuristic detection from hive/swarm-mail
|
|
707
828
|
const detection = await detectSwarm();
|
|
708
829
|
|
|
830
|
+
// Boost confidence if we found swarm evidence in session messages
|
|
831
|
+
let effectiveConfidence = detection.confidence;
|
|
832
|
+
if (scannedState.epicId || scannedState.subtasks.size > 0) {
|
|
833
|
+
// Session messages show swarm activity - this is HIGH confidence
|
|
834
|
+
if (effectiveConfidence === "none" || effectiveConfidence === "low") {
|
|
835
|
+
effectiveConfidence = "medium";
|
|
836
|
+
detection.reasons.push("swarm tool calls found in session");
|
|
837
|
+
}
|
|
838
|
+
if (scannedState.subtasks.size > 0) {
|
|
839
|
+
effectiveConfidence = "high";
|
|
840
|
+
detection.reasons.push(`${scannedState.subtasks.size} subtasks spawned`);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
709
844
|
if (
|
|
710
|
-
|
|
711
|
-
|
|
845
|
+
effectiveConfidence === "high" ||
|
|
846
|
+
effectiveConfidence === "medium"
|
|
712
847
|
) {
|
|
713
848
|
// Definite or probable swarm - inject full context
|
|
714
849
|
const header = `[Swarm detected: ${detection.reasons.join(", ")}]\n\n`;
|
|
715
|
-
|
|
716
|
-
// Build dynamic state section
|
|
850
|
+
|
|
851
|
+
// Build dynamic state section - prefer scanned state (ground truth) over detected
|
|
717
852
|
let dynamicState = "";
|
|
718
|
-
if (
|
|
853
|
+
if (scannedState.epicId || scannedState.subtasks.size > 0) {
|
|
854
|
+
// Use scanned state (more precise)
|
|
855
|
+
dynamicState =
|
|
856
|
+
buildDynamicSwarmStateFromScanned(
|
|
857
|
+
scannedState,
|
|
858
|
+
detection.state || {
|
|
859
|
+
projectPath: scannedState.projectPath || process.cwd(),
|
|
860
|
+
subtasks: { closed: 0, in_progress: 0, open: 0, blocked: 0 },
|
|
861
|
+
},
|
|
862
|
+
) + "\n\n";
|
|
863
|
+
} else if (detection.state && detection.state.epicId) {
|
|
864
|
+
// Fall back to hive-detected state
|
|
719
865
|
dynamicState = buildDynamicSwarmState(detection.state) + "\n\n";
|
|
720
866
|
}
|
|
721
|
-
|
|
867
|
+
|
|
722
868
|
const contextContent = header + dynamicState + SWARM_COMPACTION_CONTEXT;
|
|
723
869
|
output.context.push(contextContent);
|
|
724
870
|
|
|
725
871
|
getLog().info(
|
|
726
872
|
{
|
|
727
|
-
confidence:
|
|
873
|
+
confidence: effectiveConfidence,
|
|
728
874
|
context_length: contextContent.length,
|
|
729
875
|
context_type: "full",
|
|
730
876
|
reasons: detection.reasons,
|
|
731
877
|
has_dynamic_state: !!dynamicState,
|
|
732
|
-
epic_id: detection.state?.epicId,
|
|
878
|
+
epic_id: scannedState.epicId || detection.state?.epicId,
|
|
879
|
+
scanned_subtasks: scannedState.subtasks.size,
|
|
880
|
+
scanned_agent: scannedState.agentName,
|
|
733
881
|
},
|
|
734
882
|
"injected swarm context",
|
|
735
883
|
);
|
|
736
|
-
} else if (
|
|
884
|
+
} else if (effectiveConfidence === "low") {
|
|
737
885
|
// Possible swarm - inject fallback detection prompt
|
|
738
886
|
const header = `[Possible swarm: ${detection.reasons.join(", ")}]\n\n`;
|
|
739
887
|
const contextContent = header + SWARM_DETECTION_FALLBACK;
|
|
@@ -741,7 +889,7 @@ export function createCompactionHook() {
|
|
|
741
889
|
|
|
742
890
|
getLog().info(
|
|
743
891
|
{
|
|
744
|
-
confidence:
|
|
892
|
+
confidence: effectiveConfidence,
|
|
745
893
|
context_length: contextContent.length,
|
|
746
894
|
context_type: "fallback",
|
|
747
895
|
reasons: detection.reasons,
|
|
@@ -751,7 +899,7 @@ export function createCompactionHook() {
|
|
|
751
899
|
} else {
|
|
752
900
|
getLog().debug(
|
|
753
901
|
{
|
|
754
|
-
confidence:
|
|
902
|
+
confidence: effectiveConfidence,
|
|
755
903
|
context_type: "none",
|
|
756
904
|
},
|
|
757
905
|
"no swarm detected, skipping injection",
|
|
@@ -764,8 +912,8 @@ export function createCompactionHook() {
|
|
|
764
912
|
{
|
|
765
913
|
duration_ms: duration,
|
|
766
914
|
success: true,
|
|
767
|
-
detected: detection.detected,
|
|
768
|
-
confidence:
|
|
915
|
+
detected: detection.detected || scannedState.epicId !== undefined,
|
|
916
|
+
confidence: effectiveConfidence,
|
|
769
917
|
context_injected: output.context.length > 0,
|
|
770
918
|
},
|
|
771
919
|
"compaction complete",
|
package/src/eval-capture.ts
CHANGED
|
@@ -63,8 +63,8 @@ export const EvalRecordSchema = z.object({
|
|
|
63
63
|
context: z.string().optional(),
|
|
64
64
|
/** Strategy used for decomposition */
|
|
65
65
|
strategy: z.enum(["file-based", "feature-based", "risk-based", "auto"]),
|
|
66
|
-
/**
|
|
67
|
-
|
|
66
|
+
/** Number of subtasks generated */
|
|
67
|
+
subtask_count: z.number().int().min(1),
|
|
68
68
|
|
|
69
69
|
// OUTPUT (the decomposition)
|
|
70
70
|
/** Epic title */
|
|
@@ -238,7 +238,6 @@ export function captureDecomposition(params: {
|
|
|
238
238
|
task: string;
|
|
239
239
|
context?: string;
|
|
240
240
|
strategy: "file-based" | "feature-based" | "risk-based" | "auto";
|
|
241
|
-
maxSubtasks: number;
|
|
242
241
|
epicTitle: string;
|
|
243
242
|
epicDescription?: string;
|
|
244
243
|
subtasks: Array<{
|
|
@@ -256,7 +255,7 @@ export function captureDecomposition(params: {
|
|
|
256
255
|
task: params.task,
|
|
257
256
|
context: params.context,
|
|
258
257
|
strategy: params.strategy,
|
|
259
|
-
|
|
258
|
+
subtask_count: params.subtasks.length,
|
|
260
259
|
epic_title: params.epicTitle,
|
|
261
260
|
epic_description: params.epicDescription,
|
|
262
261
|
subtasks: params.subtasks,
|
|
@@ -409,7 +408,7 @@ export function exportForEvalite(projectPath: string): Array<{
|
|
|
409
408
|
input: { task: string; context?: string };
|
|
410
409
|
expected: {
|
|
411
410
|
minSubtasks: number;
|
|
412
|
-
|
|
411
|
+
subtaskCount: number;
|
|
413
412
|
requiredFiles?: string[];
|
|
414
413
|
overallSuccess?: boolean;
|
|
415
414
|
};
|
|
@@ -426,7 +425,7 @@ export function exportForEvalite(projectPath: string): Array<{
|
|
|
426
425
|
},
|
|
427
426
|
expected: {
|
|
428
427
|
minSubtasks: 2,
|
|
429
|
-
|
|
428
|
+
subtaskCount: record.subtask_count,
|
|
430
429
|
requiredFiles: record.subtasks.flatMap((s) => s.files),
|
|
431
430
|
overallSuccess: record.overall_success,
|
|
432
431
|
},
|
package/src/index.ts
CHANGED
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
analyzeTodoWrite,
|
|
59
59
|
shouldAnalyzeTool,
|
|
60
60
|
} from "./planning-guardrails";
|
|
61
|
+
import { createCompactionHook } from "./compaction-hook";
|
|
61
62
|
|
|
62
63
|
/**
|
|
63
64
|
* OpenCode Swarm Plugin
|
|
@@ -80,7 +81,7 @@ import {
|
|
|
80
81
|
export const SwarmPlugin: Plugin = async (
|
|
81
82
|
input: PluginInput,
|
|
82
83
|
): Promise<Hooks> => {
|
|
83
|
-
const { $, directory } = input;
|
|
84
|
+
const { $, directory, client } = input;
|
|
84
85
|
|
|
85
86
|
// Set the working directory for hive commands
|
|
86
87
|
// This ensures hive operations run in the project directory, not ~/.config/opencode
|
|
@@ -261,6 +262,25 @@ export const SwarmPlugin: Plugin = async (
|
|
|
261
262
|
// Auto-sync was removed because bd CLI is deprecated
|
|
262
263
|
// The hive_sync tool handles flushing to JSONL and git commit/push
|
|
263
264
|
},
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Compaction hook for swarm context preservation
|
|
268
|
+
*
|
|
269
|
+
* When OpenCode compacts session context, this hook injects swarm state
|
|
270
|
+
* to ensure coordinators can resume orchestration seamlessly.
|
|
271
|
+
*
|
|
272
|
+
* Uses SDK client to scan actual session messages for precise swarm state
|
|
273
|
+
* (epic IDs, subtask status, agent names) rather than relying solely on
|
|
274
|
+
* heuristic detection from hive/swarm-mail.
|
|
275
|
+
*
|
|
276
|
+
* Note: This hook is experimental and may not be in the published Hooks type yet.
|
|
277
|
+
*/
|
|
278
|
+
"experimental.session.compacting": createCompactionHook(client),
|
|
279
|
+
} as Hooks & {
|
|
280
|
+
"experimental.session.compacting"?: (
|
|
281
|
+
input: { sessionID: string },
|
|
282
|
+
output: { context: string[] },
|
|
283
|
+
) => Promise<void>;
|
|
264
284
|
};
|
|
265
285
|
};
|
|
266
286
|
|
|
@@ -976,7 +976,6 @@ describe("Swarm Tool Integrations", () => {
|
|
|
976
976
|
const result = await swarm_decompose.execute(
|
|
977
977
|
{
|
|
978
978
|
task: "Add user authentication",
|
|
979
|
-
max_subtasks: 3,
|
|
980
979
|
query_cass: true,
|
|
981
980
|
},
|
|
982
981
|
mockContext,
|
|
@@ -992,7 +991,6 @@ describe("Swarm Tool Integrations", () => {
|
|
|
992
991
|
const result = await swarm_decompose.execute(
|
|
993
992
|
{
|
|
994
993
|
task: "Add user authentication",
|
|
995
|
-
max_subtasks: 3,
|
|
996
994
|
query_cass: false,
|
|
997
995
|
},
|
|
998
996
|
mockContext,
|
package/src/schemas/task.ts
CHANGED
|
@@ -87,7 +87,6 @@ export type TaskDecomposition = z.infer<typeof TaskDecompositionSchema>;
|
|
|
87
87
|
*/
|
|
88
88
|
export const DecomposeArgsSchema = z.object({
|
|
89
89
|
task: z.string().min(1),
|
|
90
|
-
max_subtasks: z.number().int().min(1).default(5),
|
|
91
90
|
context: z.string().optional(),
|
|
92
91
|
});
|
|
93
92
|
export type DecomposeArgs = z.infer<typeof DecomposeArgsSchema>;
|
package/src/swarm-decompose.ts
CHANGED
|
@@ -690,12 +690,6 @@ export const swarm_delegate_planning = tool({
|
|
|
690
690
|
.string()
|
|
691
691
|
.optional()
|
|
692
692
|
.describe("Additional context to include"),
|
|
693
|
-
max_subtasks: tool.schema
|
|
694
|
-
.number()
|
|
695
|
-
.int()
|
|
696
|
-
.min(1)
|
|
697
|
-
.optional()
|
|
698
|
-
.describe("Suggested max subtasks (optional - LLM decides if not specified)"),
|
|
699
693
|
strategy: tool.schema
|
|
700
694
|
.enum(["auto", "file-based", "feature-based", "risk-based"])
|
|
701
695
|
.optional()
|
|
@@ -797,8 +791,7 @@ export const swarm_delegate_planning = tool({
|
|
|
797
791
|
.replace("{strategy_guidelines}", strategyGuidelines)
|
|
798
792
|
.replace("{context_section}", contextSection)
|
|
799
793
|
.replace("{cass_history}", cassContext || "")
|
|
800
|
-
.replace("{skills_context}", skillsContext || "")
|
|
801
|
-
.replace("{max_subtasks}", (args.max_subtasks ?? 5).toString());
|
|
794
|
+
.replace("{skills_context}", skillsContext || "");
|
|
802
795
|
|
|
803
796
|
// Add strict JSON-only instructions for the subagent
|
|
804
797
|
const subagentInstructions = `
|
|
@@ -79,7 +79,6 @@ describe("swarm_decompose", () => {
|
|
|
79
79
|
const result = await swarm_decompose.execute(
|
|
80
80
|
{
|
|
81
81
|
task: "Add user authentication with OAuth",
|
|
82
|
-
max_subtasks: 3,
|
|
83
82
|
},
|
|
84
83
|
mockContext,
|
|
85
84
|
);
|
|
@@ -97,7 +96,6 @@ describe("swarm_decompose", () => {
|
|
|
97
96
|
const result = await swarm_decompose.execute(
|
|
98
97
|
{
|
|
99
98
|
task: "Refactor the API routes",
|
|
100
|
-
max_subtasks: 5,
|
|
101
99
|
context: "Using Next.js App Router with RSC",
|
|
102
100
|
},
|
|
103
101
|
mockContext,
|
|
@@ -109,20 +107,6 @@ describe("swarm_decompose", () => {
|
|
|
109
107
|
expect(parsed.prompt).toContain("Additional Context");
|
|
110
108
|
});
|
|
111
109
|
|
|
112
|
-
it("uses default max_subtasks when not provided", async () => {
|
|
113
|
-
const result = await swarm_decompose.execute(
|
|
114
|
-
{
|
|
115
|
-
task: "Simple task",
|
|
116
|
-
max_subtasks: 5, // Explicit default since schema requires it
|
|
117
|
-
},
|
|
118
|
-
mockContext,
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
const parsed = JSON.parse(result);
|
|
122
|
-
|
|
123
|
-
// Prompt should say "as many as needed" (max_subtasks no longer in template)
|
|
124
|
-
expect(parsed.prompt).toContain("as many as needed");
|
|
125
|
-
});
|
|
126
110
|
});
|
|
127
111
|
|
|
128
112
|
// ============================================================================
|
|
@@ -262,7 +246,6 @@ describe("swarm_plan_prompt", () => {
|
|
|
262
246
|
const result = await swarm_plan_prompt.execute(
|
|
263
247
|
{
|
|
264
248
|
task: "Add user settings page",
|
|
265
|
-
max_subtasks: 3,
|
|
266
249
|
query_cass: false, // Disable CASS to isolate test
|
|
267
250
|
},
|
|
268
251
|
mockContext,
|
|
@@ -281,7 +264,6 @@ describe("swarm_plan_prompt", () => {
|
|
|
281
264
|
{
|
|
282
265
|
task: "Do something",
|
|
283
266
|
strategy: "risk-based",
|
|
284
|
-
max_subtasks: 3,
|
|
285
267
|
query_cass: false,
|
|
286
268
|
},
|
|
287
269
|
mockContext,
|
|
@@ -296,7 +278,6 @@ describe("swarm_plan_prompt", () => {
|
|
|
296
278
|
const result = await swarm_plan_prompt.execute(
|
|
297
279
|
{
|
|
298
280
|
task: "Refactor the codebase",
|
|
299
|
-
max_subtasks: 4,
|
|
300
281
|
query_cass: false,
|
|
301
282
|
},
|
|
302
283
|
mockContext,
|
|
@@ -314,7 +295,6 @@ describe("swarm_plan_prompt", () => {
|
|
|
314
295
|
const result = await swarm_plan_prompt.execute(
|
|
315
296
|
{
|
|
316
297
|
task: "Build new feature",
|
|
317
|
-
max_subtasks: 3,
|
|
318
298
|
query_cass: false,
|
|
319
299
|
},
|
|
320
300
|
mockContext,
|
|
@@ -330,7 +310,6 @@ describe("swarm_plan_prompt", () => {
|
|
|
330
310
|
const result = await swarm_plan_prompt.execute(
|
|
331
311
|
{
|
|
332
312
|
task: "Some task",
|
|
333
|
-
max_subtasks: 5,
|
|
334
313
|
query_cass: false,
|
|
335
314
|
},
|
|
336
315
|
mockContext,
|
|
@@ -350,7 +329,6 @@ describe("swarm_plan_prompt", () => {
|
|
|
350
329
|
const result = await swarm_plan_prompt.execute(
|
|
351
330
|
{
|
|
352
331
|
task: "Add feature",
|
|
353
|
-
max_subtasks: 3,
|
|
354
332
|
},
|
|
355
333
|
mockContext,
|
|
356
334
|
);
|
|
@@ -375,7 +353,6 @@ describe("swarm_plan_prompt", () => {
|
|
|
375
353
|
const result = await swarm_plan_prompt.execute(
|
|
376
354
|
{
|
|
377
355
|
task: "Add user profile",
|
|
378
|
-
max_subtasks: 3,
|
|
379
356
|
context: "We use Next.js App Router with server components",
|
|
380
357
|
query_cass: false,
|
|
381
358
|
},
|
|
@@ -387,19 +364,6 @@ describe("swarm_plan_prompt", () => {
|
|
|
387
364
|
expect(parsed.prompt).toContain("server components");
|
|
388
365
|
});
|
|
389
366
|
|
|
390
|
-
it("includes max_subtasks in prompt", async () => {
|
|
391
|
-
const result = await swarm_plan_prompt.execute(
|
|
392
|
-
{
|
|
393
|
-
task: "Build something",
|
|
394
|
-
max_subtasks: 7,
|
|
395
|
-
query_cass: false,
|
|
396
|
-
},
|
|
397
|
-
mockContext,
|
|
398
|
-
);
|
|
399
|
-
const parsed = JSON.parse(result);
|
|
400
|
-
|
|
401
|
-
expect(parsed.prompt).toContain("as many as needed");
|
|
402
|
-
});
|
|
403
367
|
});
|
|
404
368
|
|
|
405
369
|
describe("swarm_validate_decomposition", () => {
|
|
@@ -920,7 +884,6 @@ describe("full swarm flow (integration)", () => {
|
|
|
920
884
|
const decomposeResult = await swarm_decompose.execute(
|
|
921
885
|
{
|
|
922
886
|
task: "Add unit tests for auth module",
|
|
923
|
-
max_subtasks: 2,
|
|
924
887
|
},
|
|
925
888
|
ctx,
|
|
926
889
|
);
|
|
@@ -1228,7 +1191,6 @@ describe("swarm_init", () => {
|
|
|
1228
1191
|
const result = await swarm_decompose.execute(
|
|
1229
1192
|
{
|
|
1230
1193
|
task: "Add user authentication",
|
|
1231
|
-
max_subtasks: 3,
|
|
1232
1194
|
query_cass: true, // Request CASS but it may not be available
|
|
1233
1195
|
},
|
|
1234
1196
|
mockContext,
|
|
@@ -1249,7 +1211,6 @@ describe("swarm_init", () => {
|
|
|
1249
1211
|
const result = await swarm_decompose.execute(
|
|
1250
1212
|
{
|
|
1251
1213
|
task: "Add user authentication",
|
|
1252
|
-
max_subtasks: 3,
|
|
1253
1214
|
query_cass: false, // Explicitly skip CASS
|
|
1254
1215
|
},
|
|
1255
1216
|
mockContext,
|
|
@@ -1264,7 +1225,6 @@ describe("swarm_init", () => {
|
|
|
1264
1225
|
const result = await swarm_decompose.execute(
|
|
1265
1226
|
{
|
|
1266
1227
|
task: "Build feature X",
|
|
1267
|
-
max_subtasks: 3,
|
|
1268
1228
|
},
|
|
1269
1229
|
mockContext,
|
|
1270
1230
|
);
|