opencode-swarm-plugin 0.30.3 → 0.30.5
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-build.log +2 -2
- package/.turbo/turbo-test.log +473 -0
- package/.turbo/turbo-typecheck.log +1 -0
- package/CHANGELOG.md +106 -0
- package/bin/swarm.ts +36 -1
- package/dist/beads.d.ts +386 -0
- package/dist/beads.d.ts.map +1 -0
- package/dist/index.js +2 -2
- package/dist/plugin.js +2 -2
- package/dist/schemas/bead-events.d.ts +698 -0
- package/dist/schemas/bead-events.d.ts.map +1 -0
- package/dist/schemas/bead.d.ts +255 -0
- package/dist/schemas/bead.d.ts.map +1 -0
- package/package.json +5 -2
- package/src/model-selection.test.ts +188 -0
- package/src/model-selection.ts +68 -0
- package/src/schemas/task.ts +5 -0
- package/src/swarm-prompts.ts +28 -1
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bead schemas for type-safe beads operations
|
|
3
|
+
*
|
|
4
|
+
* These schemas validate all data from the `bd` CLI to ensure
|
|
5
|
+
* type safety and catch malformed responses early.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
/** Valid bead statuses */
|
|
9
|
+
export declare const BeadStatusSchema: z.ZodEnum<{
|
|
10
|
+
open: "open";
|
|
11
|
+
in_progress: "in_progress";
|
|
12
|
+
blocked: "blocked";
|
|
13
|
+
closed: "closed";
|
|
14
|
+
}>;
|
|
15
|
+
export type BeadStatus = z.infer<typeof BeadStatusSchema>;
|
|
16
|
+
/** Valid bead types */
|
|
17
|
+
export declare const BeadTypeSchema: z.ZodEnum<{
|
|
18
|
+
bug: "bug";
|
|
19
|
+
feature: "feature";
|
|
20
|
+
task: "task";
|
|
21
|
+
epic: "epic";
|
|
22
|
+
chore: "chore";
|
|
23
|
+
}>;
|
|
24
|
+
export type BeadType = z.infer<typeof BeadTypeSchema>;
|
|
25
|
+
/** Dependency relationship between beads */
|
|
26
|
+
export declare const BeadDependencySchema: z.ZodObject<{
|
|
27
|
+
id: z.ZodString;
|
|
28
|
+
type: z.ZodEnum<{
|
|
29
|
+
blocks: "blocks";
|
|
30
|
+
"blocked-by": "blocked-by";
|
|
31
|
+
related: "related";
|
|
32
|
+
"discovered-from": "discovered-from";
|
|
33
|
+
}>;
|
|
34
|
+
}, z.core.$strip>;
|
|
35
|
+
export type BeadDependency = z.infer<typeof BeadDependencySchema>;
|
|
36
|
+
/**
|
|
37
|
+
* Core bead schema - validates bd CLI JSON output
|
|
38
|
+
*
|
|
39
|
+
* ID format:
|
|
40
|
+
* - Standard: `{project}-{hash}` (e.g., `opencode-swarm-plugin-1i8`)
|
|
41
|
+
* - Subtask: `{project}-{hash}.{index}` (e.g., `opencode-swarm-plugin-1i8.1`)
|
|
42
|
+
* - Custom: `{project}-{custom-id}` (e.g., `migrate-egghead-phase-0`)
|
|
43
|
+
* - Custom subtask: `{project}-{custom-id}.{suffix}` (e.g., `migrate-egghead-phase-0.e2e-test`)
|
|
44
|
+
*/
|
|
45
|
+
export declare const BeadSchema: z.ZodObject<{
|
|
46
|
+
id: z.ZodString;
|
|
47
|
+
title: z.ZodString;
|
|
48
|
+
description: z.ZodDefault<z.ZodOptional<z.ZodString>>;
|
|
49
|
+
status: z.ZodDefault<z.ZodEnum<{
|
|
50
|
+
open: "open";
|
|
51
|
+
in_progress: "in_progress";
|
|
52
|
+
blocked: "blocked";
|
|
53
|
+
closed: "closed";
|
|
54
|
+
}>>;
|
|
55
|
+
priority: z.ZodDefault<z.ZodNumber>;
|
|
56
|
+
issue_type: z.ZodDefault<z.ZodEnum<{
|
|
57
|
+
bug: "bug";
|
|
58
|
+
feature: "feature";
|
|
59
|
+
task: "task";
|
|
60
|
+
epic: "epic";
|
|
61
|
+
chore: "chore";
|
|
62
|
+
}>>;
|
|
63
|
+
created_at: z.ZodString;
|
|
64
|
+
updated_at: z.ZodOptional<z.ZodString>;
|
|
65
|
+
closed_at: z.ZodOptional<z.ZodString>;
|
|
66
|
+
parent_id: z.ZodOptional<z.ZodString>;
|
|
67
|
+
dependencies: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
68
|
+
id: z.ZodString;
|
|
69
|
+
type: z.ZodEnum<{
|
|
70
|
+
blocks: "blocks";
|
|
71
|
+
"blocked-by": "blocked-by";
|
|
72
|
+
related: "related";
|
|
73
|
+
"discovered-from": "discovered-from";
|
|
74
|
+
}>;
|
|
75
|
+
}, z.core.$strip>>>;
|
|
76
|
+
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
77
|
+
}, z.core.$strip>;
|
|
78
|
+
export type Bead = z.infer<typeof BeadSchema>;
|
|
79
|
+
/** Arguments for creating a bead */
|
|
80
|
+
export declare const BeadCreateArgsSchema: z.ZodObject<{
|
|
81
|
+
title: z.ZodString;
|
|
82
|
+
type: z.ZodDefault<z.ZodEnum<{
|
|
83
|
+
bug: "bug";
|
|
84
|
+
feature: "feature";
|
|
85
|
+
task: "task";
|
|
86
|
+
epic: "epic";
|
|
87
|
+
chore: "chore";
|
|
88
|
+
}>>;
|
|
89
|
+
priority: z.ZodDefault<z.ZodNumber>;
|
|
90
|
+
description: z.ZodOptional<z.ZodString>;
|
|
91
|
+
parent_id: z.ZodOptional<z.ZodString>;
|
|
92
|
+
id: z.ZodOptional<z.ZodString>;
|
|
93
|
+
}, z.core.$strip>;
|
|
94
|
+
export type BeadCreateArgs = z.infer<typeof BeadCreateArgsSchema>;
|
|
95
|
+
/** Arguments for updating a bead */
|
|
96
|
+
export declare const BeadUpdateArgsSchema: z.ZodObject<{
|
|
97
|
+
id: z.ZodString;
|
|
98
|
+
status: z.ZodOptional<z.ZodEnum<{
|
|
99
|
+
open: "open";
|
|
100
|
+
in_progress: "in_progress";
|
|
101
|
+
blocked: "blocked";
|
|
102
|
+
closed: "closed";
|
|
103
|
+
}>>;
|
|
104
|
+
description: z.ZodOptional<z.ZodString>;
|
|
105
|
+
priority: z.ZodOptional<z.ZodNumber>;
|
|
106
|
+
}, z.core.$strip>;
|
|
107
|
+
export type BeadUpdateArgs = z.infer<typeof BeadUpdateArgsSchema>;
|
|
108
|
+
/** Arguments for closing a bead */
|
|
109
|
+
export declare const BeadCloseArgsSchema: z.ZodObject<{
|
|
110
|
+
id: z.ZodString;
|
|
111
|
+
reason: z.ZodString;
|
|
112
|
+
}, z.core.$strip>;
|
|
113
|
+
export type BeadCloseArgs = z.infer<typeof BeadCloseArgsSchema>;
|
|
114
|
+
/** Arguments for querying beads */
|
|
115
|
+
export declare const BeadQueryArgsSchema: z.ZodObject<{
|
|
116
|
+
status: z.ZodOptional<z.ZodEnum<{
|
|
117
|
+
open: "open";
|
|
118
|
+
in_progress: "in_progress";
|
|
119
|
+
blocked: "blocked";
|
|
120
|
+
closed: "closed";
|
|
121
|
+
}>>;
|
|
122
|
+
type: z.ZodOptional<z.ZodEnum<{
|
|
123
|
+
bug: "bug";
|
|
124
|
+
feature: "feature";
|
|
125
|
+
task: "task";
|
|
126
|
+
epic: "epic";
|
|
127
|
+
chore: "chore";
|
|
128
|
+
}>>;
|
|
129
|
+
ready: z.ZodOptional<z.ZodBoolean>;
|
|
130
|
+
limit: z.ZodDefault<z.ZodNumber>;
|
|
131
|
+
}, z.core.$strip>;
|
|
132
|
+
export type BeadQueryArgs = z.infer<typeof BeadQueryArgsSchema>;
|
|
133
|
+
/**
|
|
134
|
+
* Subtask specification for epic decomposition
|
|
135
|
+
*
|
|
136
|
+
* Used when creating an epic with subtasks in one operation.
|
|
137
|
+
* The `files` array is used for Agent Mail file reservations.
|
|
138
|
+
*/
|
|
139
|
+
export declare const SubtaskSpecSchema: z.ZodObject<{
|
|
140
|
+
title: z.ZodString;
|
|
141
|
+
description: z.ZodDefault<z.ZodOptional<z.ZodString>>;
|
|
142
|
+
files: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
143
|
+
dependencies: z.ZodDefault<z.ZodArray<z.ZodNumber>>;
|
|
144
|
+
estimated_complexity: z.ZodDefault<z.ZodNumber>;
|
|
145
|
+
}, z.core.$strip>;
|
|
146
|
+
export type SubtaskSpec = z.infer<typeof SubtaskSpecSchema>;
|
|
147
|
+
/**
|
|
148
|
+
* Bead tree for swarm decomposition
|
|
149
|
+
*
|
|
150
|
+
* Represents an epic with its subtasks, ready for spawning agents.
|
|
151
|
+
*/
|
|
152
|
+
export declare const BeadTreeSchema: z.ZodObject<{
|
|
153
|
+
epic: z.ZodObject<{
|
|
154
|
+
title: z.ZodString;
|
|
155
|
+
description: z.ZodDefault<z.ZodOptional<z.ZodString>>;
|
|
156
|
+
}, z.core.$strip>;
|
|
157
|
+
subtasks: z.ZodArray<z.ZodObject<{
|
|
158
|
+
title: z.ZodString;
|
|
159
|
+
description: z.ZodDefault<z.ZodOptional<z.ZodString>>;
|
|
160
|
+
files: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
161
|
+
dependencies: z.ZodDefault<z.ZodArray<z.ZodNumber>>;
|
|
162
|
+
estimated_complexity: z.ZodDefault<z.ZodNumber>;
|
|
163
|
+
}, z.core.$strip>>;
|
|
164
|
+
}, z.core.$strip>;
|
|
165
|
+
export type BeadTree = z.infer<typeof BeadTreeSchema>;
|
|
166
|
+
/** Arguments for creating an epic with subtasks */
|
|
167
|
+
export declare const EpicCreateArgsSchema: z.ZodObject<{
|
|
168
|
+
epic_title: z.ZodString;
|
|
169
|
+
epic_description: z.ZodOptional<z.ZodString>;
|
|
170
|
+
epic_id: z.ZodOptional<z.ZodString>;
|
|
171
|
+
subtasks: z.ZodArray<z.ZodObject<{
|
|
172
|
+
title: z.ZodString;
|
|
173
|
+
priority: z.ZodDefault<z.ZodNumber>;
|
|
174
|
+
files: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString>>>;
|
|
175
|
+
id_suffix: z.ZodOptional<z.ZodString>;
|
|
176
|
+
}, z.core.$strip>>;
|
|
177
|
+
}, z.core.$strip>;
|
|
178
|
+
export type EpicCreateArgs = z.infer<typeof EpicCreateArgsSchema>;
|
|
179
|
+
/**
|
|
180
|
+
* Result of epic creation
|
|
181
|
+
*
|
|
182
|
+
* Contains the created epic and all subtasks with their IDs.
|
|
183
|
+
*/
|
|
184
|
+
export declare const EpicCreateResultSchema: z.ZodObject<{
|
|
185
|
+
success: z.ZodBoolean;
|
|
186
|
+
epic: z.ZodObject<{
|
|
187
|
+
id: z.ZodString;
|
|
188
|
+
title: z.ZodString;
|
|
189
|
+
description: z.ZodDefault<z.ZodOptional<z.ZodString>>;
|
|
190
|
+
status: z.ZodDefault<z.ZodEnum<{
|
|
191
|
+
open: "open";
|
|
192
|
+
in_progress: "in_progress";
|
|
193
|
+
blocked: "blocked";
|
|
194
|
+
closed: "closed";
|
|
195
|
+
}>>;
|
|
196
|
+
priority: z.ZodDefault<z.ZodNumber>;
|
|
197
|
+
issue_type: z.ZodDefault<z.ZodEnum<{
|
|
198
|
+
bug: "bug";
|
|
199
|
+
feature: "feature";
|
|
200
|
+
task: "task";
|
|
201
|
+
epic: "epic";
|
|
202
|
+
chore: "chore";
|
|
203
|
+
}>>;
|
|
204
|
+
created_at: z.ZodString;
|
|
205
|
+
updated_at: z.ZodOptional<z.ZodString>;
|
|
206
|
+
closed_at: z.ZodOptional<z.ZodString>;
|
|
207
|
+
parent_id: z.ZodOptional<z.ZodString>;
|
|
208
|
+
dependencies: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
209
|
+
id: z.ZodString;
|
|
210
|
+
type: z.ZodEnum<{
|
|
211
|
+
blocks: "blocks";
|
|
212
|
+
"blocked-by": "blocked-by";
|
|
213
|
+
related: "related";
|
|
214
|
+
"discovered-from": "discovered-from";
|
|
215
|
+
}>;
|
|
216
|
+
}, z.core.$strip>>>;
|
|
217
|
+
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
218
|
+
}, z.core.$strip>;
|
|
219
|
+
subtasks: z.ZodArray<z.ZodObject<{
|
|
220
|
+
id: z.ZodString;
|
|
221
|
+
title: z.ZodString;
|
|
222
|
+
description: z.ZodDefault<z.ZodOptional<z.ZodString>>;
|
|
223
|
+
status: z.ZodDefault<z.ZodEnum<{
|
|
224
|
+
open: "open";
|
|
225
|
+
in_progress: "in_progress";
|
|
226
|
+
blocked: "blocked";
|
|
227
|
+
closed: "closed";
|
|
228
|
+
}>>;
|
|
229
|
+
priority: z.ZodDefault<z.ZodNumber>;
|
|
230
|
+
issue_type: z.ZodDefault<z.ZodEnum<{
|
|
231
|
+
bug: "bug";
|
|
232
|
+
feature: "feature";
|
|
233
|
+
task: "task";
|
|
234
|
+
epic: "epic";
|
|
235
|
+
chore: "chore";
|
|
236
|
+
}>>;
|
|
237
|
+
created_at: z.ZodString;
|
|
238
|
+
updated_at: z.ZodOptional<z.ZodString>;
|
|
239
|
+
closed_at: z.ZodOptional<z.ZodString>;
|
|
240
|
+
parent_id: z.ZodOptional<z.ZodString>;
|
|
241
|
+
dependencies: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
242
|
+
id: z.ZodString;
|
|
243
|
+
type: z.ZodEnum<{
|
|
244
|
+
blocks: "blocks";
|
|
245
|
+
"blocked-by": "blocked-by";
|
|
246
|
+
related: "related";
|
|
247
|
+
"discovered-from": "discovered-from";
|
|
248
|
+
}>;
|
|
249
|
+
}, z.core.$strip>>>;
|
|
250
|
+
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
251
|
+
}, z.core.$strip>>;
|
|
252
|
+
rollback_hint: z.ZodOptional<z.ZodString>;
|
|
253
|
+
}, z.core.$strip>;
|
|
254
|
+
export type EpicCreateResult = z.infer<typeof EpicCreateResultSchema>;
|
|
255
|
+
//# sourceMappingURL=bead.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bead.d.ts","sourceRoot":"","sources":["../../src/schemas/bead.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,0BAA0B;AAC1B,eAAO,MAAM,gBAAgB;;;;;EAK3B,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,uBAAuB;AACvB,eAAO,MAAM,cAAc;;;;;;EAMzB,CAAC;AACH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAEtD,4CAA4C;AAC5C,eAAO,MAAM,oBAAoB;;;;;;;;iBAG/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAsCrB,CAAC;AACH,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAE9C,oCAAoC;AACpC,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;iBAY/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE,oCAAoC;AACpC,eAAO,MAAM,oBAAoB;;;;;;;;;;iBAK/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE,mCAAmC;AACnC,eAAO,MAAM,mBAAmB;;;iBAG9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,mCAAmC;AACnC,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;iBAK9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB;;;;;;iBAc5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D;;;;GAIG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;iBAMzB,CAAC;AACH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAEtD,mDAAmD;AACnD,eAAO,MAAM,oBAAoB;;;;;;;;;;iBAwB/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE;;;;GAIG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAKjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-swarm-plugin",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.5",
|
|
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,6 +18,10 @@
|
|
|
18
18
|
"types": "./dist/plugin.d.ts"
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public",
|
|
23
|
+
"registry": "https://registry.npmjs.org/"
|
|
24
|
+
},
|
|
21
25
|
"scripts": {
|
|
22
26
|
"build": "bun build ./src/index.ts --outdir ./dist --target node --external @electric-sql/pglite --external swarm-mail && bun build ./src/plugin.ts --outfile ./dist/plugin.js --target node --external @electric-sql/pglite --external swarm-mail && tsc",
|
|
23
27
|
"dev": "bun --watch src/index.ts",
|
|
@@ -26,7 +30,6 @@
|
|
|
26
30
|
"test:all": "bun test --timeout 60000 src/",
|
|
27
31
|
"test:watch": "bun test --watch src/",
|
|
28
32
|
"typecheck": "tsc --noEmit",
|
|
29
|
-
"publish:pkg": "npm publish --access public --provenance",
|
|
30
33
|
"postinstall": "node -e \"console.log('\\n\\x1b[33m Run \\x1b[36mswarm setup\\x1b[33m to configure OpenCode integration\\x1b[0m\\n')\""
|
|
31
34
|
},
|
|
32
35
|
"dependencies": {
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model Selection Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for selectWorkerModel function that determines which model
|
|
5
|
+
* a worker should use based on subtask characteristics.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, test, expect } from "bun:test";
|
|
8
|
+
import { selectWorkerModel } from "./model-selection";
|
|
9
|
+
import type { DecomposedSubtask } from "./schemas/task";
|
|
10
|
+
|
|
11
|
+
// Mock config type matching expected SwarmConfig structure
|
|
12
|
+
interface TestConfig {
|
|
13
|
+
primaryModel: string;
|
|
14
|
+
liteModel?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("selectWorkerModel", () => {
|
|
18
|
+
const mockConfig: TestConfig = {
|
|
19
|
+
primaryModel: "anthropic/claude-sonnet-4-5",
|
|
20
|
+
liteModel: "anthropic/claude-haiku-4-5",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
test("uses explicit model field from subtask when provided", () => {
|
|
24
|
+
const subtask: DecomposedSubtask & { model?: string } = {
|
|
25
|
+
title: "Update docs",
|
|
26
|
+
description: "Update README",
|
|
27
|
+
files: ["README.md"],
|
|
28
|
+
estimated_effort: "trivial",
|
|
29
|
+
model: "anthropic/claude-opus-4-5", // Explicit override
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const result = selectWorkerModel(subtask, mockConfig);
|
|
33
|
+
expect(result).toBe("anthropic/claude-opus-4-5");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("uses liteModel for all markdown files", () => {
|
|
37
|
+
const subtask: DecomposedSubtask = {
|
|
38
|
+
title: "Update docs",
|
|
39
|
+
description: "Update all docs",
|
|
40
|
+
files: ["README.md", "CONTRIBUTING.md"],
|
|
41
|
+
estimated_effort: "small",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const result = selectWorkerModel(subtask, mockConfig);
|
|
45
|
+
expect(result).toBe("anthropic/claude-haiku-4-5");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("uses liteModel for all MDX files", () => {
|
|
49
|
+
const subtask: DecomposedSubtask = {
|
|
50
|
+
title: "Update docs",
|
|
51
|
+
description: "Update content",
|
|
52
|
+
files: ["docs/intro.mdx", "docs/guide.mdx"],
|
|
53
|
+
estimated_effort: "small",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const result = selectWorkerModel(subtask, mockConfig);
|
|
57
|
+
expect(result).toBe("anthropic/claude-haiku-4-5");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("uses liteModel for test files with .test. pattern", () => {
|
|
61
|
+
const subtask: DecomposedSubtask = {
|
|
62
|
+
title: "Write tests",
|
|
63
|
+
description: "Add unit tests",
|
|
64
|
+
files: ["src/auth.test.ts", "src/user.test.ts"],
|
|
65
|
+
estimated_effort: "small",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const result = selectWorkerModel(subtask, mockConfig);
|
|
69
|
+
expect(result).toBe("anthropic/claude-haiku-4-5");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("uses liteModel for test files with .spec. pattern", () => {
|
|
73
|
+
const subtask: DecomposedSubtask = {
|
|
74
|
+
title: "Write specs",
|
|
75
|
+
description: "Add spec tests",
|
|
76
|
+
files: ["src/auth.spec.ts", "src/user.spec.ts"],
|
|
77
|
+
estimated_effort: "small",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const result = selectWorkerModel(subtask, mockConfig);
|
|
81
|
+
expect(result).toBe("anthropic/claude-haiku-4-5");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("uses primaryModel when files are mixed (code + docs)", () => {
|
|
85
|
+
const subtask: DecomposedSubtask = {
|
|
86
|
+
title: "Implement feature with docs",
|
|
87
|
+
description: "Add feature and document it",
|
|
88
|
+
files: ["src/feature.ts", "README.md"],
|
|
89
|
+
estimated_effort: "medium",
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const result = selectWorkerModel(subtask, mockConfig);
|
|
93
|
+
expect(result).toBe("anthropic/claude-sonnet-4-5");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("uses primaryModel when files are mixed (code + tests)", () => {
|
|
97
|
+
const subtask: DecomposedSubtask = {
|
|
98
|
+
title: "Implement feature with tests",
|
|
99
|
+
description: "Add feature and tests",
|
|
100
|
+
files: ["src/feature.ts", "src/feature.test.ts"],
|
|
101
|
+
estimated_effort: "medium",
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const result = selectWorkerModel(subtask, mockConfig);
|
|
105
|
+
expect(result).toBe("anthropic/claude-sonnet-4-5");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("uses primaryModel for implementation files", () => {
|
|
109
|
+
const subtask: DecomposedSubtask = {
|
|
110
|
+
title: "Implement auth",
|
|
111
|
+
description: "Add authentication",
|
|
112
|
+
files: ["src/auth.ts", "src/middleware.ts"],
|
|
113
|
+
estimated_effort: "large",
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const result = selectWorkerModel(subtask, mockConfig);
|
|
117
|
+
expect(result).toBe("anthropic/claude-sonnet-4-5");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("defaults to primaryModel when liteModel not configured", () => {
|
|
121
|
+
const configWithoutLite: TestConfig = {
|
|
122
|
+
primaryModel: "anthropic/claude-sonnet-4-5",
|
|
123
|
+
// liteModel is undefined
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const subtask: DecomposedSubtask = {
|
|
127
|
+
title: "Update docs",
|
|
128
|
+
description: "Update README",
|
|
129
|
+
files: ["README.md"],
|
|
130
|
+
estimated_effort: "trivial",
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const result = selectWorkerModel(subtask, configWithoutLite);
|
|
134
|
+
expect(result).toBe("anthropic/claude-sonnet-4-5");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("falls back to claude-haiku when liteModel not configured but primaryModel missing", () => {
|
|
138
|
+
const emptyConfig: TestConfig = {
|
|
139
|
+
primaryModel: "",
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const subtask: DecomposedSubtask = {
|
|
143
|
+
title: "Update docs",
|
|
144
|
+
description: "Update README",
|
|
145
|
+
files: ["README.md"],
|
|
146
|
+
estimated_effort: "trivial",
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const result = selectWorkerModel(subtask, emptyConfig);
|
|
150
|
+
expect(result).toBe("anthropic/claude-haiku-4-5");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("handles empty files array by defaulting to primaryModel", () => {
|
|
154
|
+
const subtask: DecomposedSubtask = {
|
|
155
|
+
title: "Research task",
|
|
156
|
+
description: "Investigate options",
|
|
157
|
+
files: [],
|
|
158
|
+
estimated_effort: "small",
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const result = selectWorkerModel(subtask, mockConfig);
|
|
162
|
+
expect(result).toBe("anthropic/claude-sonnet-4-5");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("handles mixed markdown and mdx files", () => {
|
|
166
|
+
const subtask: DecomposedSubtask = {
|
|
167
|
+
title: "Update all docs",
|
|
168
|
+
description: "Update docs",
|
|
169
|
+
files: ["README.md", "docs/guide.mdx", "CHANGELOG.md"],
|
|
170
|
+
estimated_effort: "small",
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const result = selectWorkerModel(subtask, mockConfig);
|
|
174
|
+
expect(result).toBe("anthropic/claude-haiku-4-5");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("case insensitive file extension matching", () => {
|
|
178
|
+
const subtask: DecomposedSubtask = {
|
|
179
|
+
title: "Update docs",
|
|
180
|
+
description: "Update README",
|
|
181
|
+
files: ["README.MD", "CONTRIBUTING.MD"],
|
|
182
|
+
estimated_effort: "trivial",
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const result = selectWorkerModel(subtask, mockConfig);
|
|
186
|
+
expect(result).toBe("anthropic/claude-haiku-4-5");
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model Selection Module
|
|
3
|
+
*
|
|
4
|
+
* Determines which model a worker agent should use based on subtask
|
|
5
|
+
* characteristics like file types and complexity.
|
|
6
|
+
*
|
|
7
|
+
* Priority:
|
|
8
|
+
* 1. Explicit model field in subtask
|
|
9
|
+
* 2. File-type inference (docs/tests → lite model)
|
|
10
|
+
* 3. Default to primary model
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { DecomposedSubtask } from "./schemas/task";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Configuration interface for swarm models
|
|
17
|
+
*/
|
|
18
|
+
export interface SwarmConfig {
|
|
19
|
+
primaryModel: string;
|
|
20
|
+
liteModel?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Select the appropriate model for a worker agent based on subtask characteristics
|
|
25
|
+
*
|
|
26
|
+
* Priority order:
|
|
27
|
+
* 1. Explicit `model` field in subtask (if present)
|
|
28
|
+
* 2. File-type inference:
|
|
29
|
+
* - All .md/.mdx files → liteModel
|
|
30
|
+
* - All .test./.spec. files → liteModel
|
|
31
|
+
* 3. Mixed files or implementation → primaryModel
|
|
32
|
+
*
|
|
33
|
+
* @param subtask - The subtask to evaluate
|
|
34
|
+
* @param config - Swarm configuration with model preferences
|
|
35
|
+
* @returns Model identifier string
|
|
36
|
+
*/
|
|
37
|
+
export function selectWorkerModel(
|
|
38
|
+
subtask: DecomposedSubtask & { model?: string },
|
|
39
|
+
config: SwarmConfig,
|
|
40
|
+
): string {
|
|
41
|
+
// Priority 1: Explicit model in subtask
|
|
42
|
+
if (subtask.model) {
|
|
43
|
+
return subtask.model;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const files = subtask.files || [];
|
|
47
|
+
|
|
48
|
+
// Priority 2: File-type inference
|
|
49
|
+
if (files.length > 0) {
|
|
50
|
+
const allDocs = files.every((f) => {
|
|
51
|
+
const lower = f.toLowerCase();
|
|
52
|
+
return lower.endsWith(".md") || lower.endsWith(".mdx");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const allTests = files.every((f) => {
|
|
56
|
+
const lower = f.toLowerCase();
|
|
57
|
+
return lower.includes(".test.") || lower.includes(".spec.");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (allDocs || allTests) {
|
|
61
|
+
// Use lite model if configured, otherwise fall back to primary
|
|
62
|
+
return config.liteModel || config.primaryModel || "anthropic/claude-haiku-4-5";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Priority 3: Default to primary model
|
|
67
|
+
return config.primaryModel || "anthropic/claude-haiku-4-5";
|
|
68
|
+
}
|
package/src/schemas/task.ts
CHANGED
|
@@ -43,6 +43,11 @@ export const DecomposedSubtaskSchema = z.object({
|
|
|
43
43
|
estimated_effort: EffortLevelSchema,
|
|
44
44
|
/** Potential risks or complications (e.g., 'tight coupling', 'data migration required', 'breaking change') */
|
|
45
45
|
risks: z.array(z.string()).optional().default([]),
|
|
46
|
+
/**
|
|
47
|
+
* Optional explicit model override for this subtask.
|
|
48
|
+
* If not specified, model will be selected based on file types.
|
|
49
|
+
*/
|
|
50
|
+
model: z.string().optional(),
|
|
46
51
|
});
|
|
47
52
|
export type DecomposedSubtask = z.infer<typeof DecomposedSubtaskSchema>;
|
|
48
53
|
|
package/src/swarm-prompts.ts
CHANGED
|
@@ -739,7 +739,7 @@ export const swarm_subtask_prompt = tool({
|
|
|
739
739
|
*/
|
|
740
740
|
export const swarm_spawn_subtask = tool({
|
|
741
741
|
description:
|
|
742
|
-
"Prepare a subtask for spawning. Returns prompt with Agent Mail/hive tracking instructions. IMPORTANT: Pass project_path for swarmmail_init.",
|
|
742
|
+
"Prepare a subtask for spawning. Returns prompt with Agent Mail/hive tracking instructions. IMPORTANT: Pass project_path for swarmmail_init. Automatically selects appropriate model based on file types.",
|
|
743
743
|
args: {
|
|
744
744
|
bead_id: tool.schema.string().describe("Subtask bead ID"),
|
|
745
745
|
epic_id: tool.schema.string().describe("Parent epic bead ID"),
|
|
@@ -769,6 +769,10 @@ export const swarm_spawn_subtask = tool({
|
|
|
769
769
|
})
|
|
770
770
|
.optional()
|
|
771
771
|
.describe("Recovery context from checkpoint compaction"),
|
|
772
|
+
model: tool.schema
|
|
773
|
+
.string()
|
|
774
|
+
.optional()
|
|
775
|
+
.describe("Optional explicit model override (auto-selected if not provided)"),
|
|
772
776
|
},
|
|
773
777
|
async execute(args) {
|
|
774
778
|
const prompt = formatSubtaskPromptV2({
|
|
@@ -782,6 +786,28 @@ export const swarm_spawn_subtask = tool({
|
|
|
782
786
|
recovery_context: args.recovery_context,
|
|
783
787
|
});
|
|
784
788
|
|
|
789
|
+
// Import selectWorkerModel at function scope to avoid circular dependencies
|
|
790
|
+
const { selectWorkerModel } = await import("./model-selection.js");
|
|
791
|
+
|
|
792
|
+
// Create a mock subtask for model selection
|
|
793
|
+
const subtask = {
|
|
794
|
+
title: args.subtask_title,
|
|
795
|
+
description: args.subtask_description || "",
|
|
796
|
+
files: args.files,
|
|
797
|
+
estimated_effort: "medium" as const,
|
|
798
|
+
risks: [],
|
|
799
|
+
model: args.model,
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
// Use placeholder config - actual config should be passed from coordinator
|
|
803
|
+
// For now, we use reasonable defaults
|
|
804
|
+
const config = {
|
|
805
|
+
primaryModel: "anthropic/claude-sonnet-4-5",
|
|
806
|
+
liteModel: "anthropic/claude-haiku-4-5",
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
const selectedModel = selectWorkerModel(subtask, config);
|
|
810
|
+
|
|
785
811
|
return JSON.stringify(
|
|
786
812
|
{
|
|
787
813
|
prompt,
|
|
@@ -790,6 +816,7 @@ export const swarm_spawn_subtask = tool({
|
|
|
790
816
|
files: args.files,
|
|
791
817
|
project_path: args.project_path,
|
|
792
818
|
recovery_context: args.recovery_context,
|
|
819
|
+
recommended_model: selectedModel,
|
|
793
820
|
},
|
|
794
821
|
null,
|
|
795
822
|
2,
|