swarm-control 0.1.3 → 0.1.4

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/dist/installer.js CHANGED
@@ -9,7 +9,7 @@ const __dirname = path.dirname(__filename);
9
9
  const PACKAGE_ROOT = __dirname.includes('node_modules')
10
10
  ? path.join(__dirname, '..')
11
11
  : path.join(__dirname, '..');
12
- const PLUGIN_SOURCE = path.join(PACKAGE_ROOT, 'src/plugin');
12
+ const PLUGIN_DIST_PATH = path.join(PACKAGE_ROOT, 'dist/plugin');
13
13
  const PLUGIN_TEMPLATE = path.join(PACKAGE_ROOT, 'templates/swarm-control.template.ts');
14
14
  const PLUGIN_TARGET = path.join(process.env.HOME, '.opencode/plugin/swarm-control.ts');
15
15
  const OPENCODE_CONFIG_DIR = path.join(process.env.HOME, '.config/opencode');
@@ -22,7 +22,7 @@ export async function setupPlugin() {
22
22
  const template = await fs.readFile(PLUGIN_TEMPLATE, 'utf-8');
23
23
  // Replace placeholder with the actual package path
24
24
  // Use .js extension since this is an ESM module
25
- const packagePath = path.join(PACKAGE_ROOT, 'src/plugin/index.js');
25
+ const packagePath = path.join(PACKAGE_ROOT, 'dist/plugin/index.js');
26
26
  const pluginContent = template.replace('PLACEHOLDER_PACKAGE_PATH', packagePath);
27
27
  // Ensure plugin directory exists
28
28
  const pluginDir = path.dirname(PLUGIN_TARGET);
@@ -1 +1 @@
1
- {"version":3,"file":"installer.js","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"installer.js","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swarm-control",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for multi-agent swarm coordination",
6
6
  "main": "dist/cli.js",
@@ -9,7 +9,6 @@
9
9
  },
10
10
  "files": [
11
11
  "dist/",
12
- "src/plugin/",
13
12
  "templates/",
14
13
  "README.md",
15
14
  "LICENSE"
@@ -1,56 +0,0 @@
1
- import fs from 'fs-extra'
2
- import path from 'path'
3
- import { SwarmConfigSchema, type SwarmConfig } from './types.js'
4
-
5
- const OPENCODE_CONFIG_DIR = path.join(process.env.HOME!, '.config/opencode')
6
- const OPENCODE_CONFIG_FILE = path.join(OPENCODE_CONFIG_DIR, 'opencode.json')
7
- const SWARM_CONFIG_KEY = 'swarm'
8
-
9
- export async function getConfig(): Promise<SwarmConfig | null> {
10
- try {
11
- const configExists = await fs.pathExists(OPENCODE_CONFIG_FILE)
12
- if (!configExists) return null
13
-
14
- const data = await fs.readFile(OPENCODE_CONFIG_FILE, 'utf-8')
15
- const parsed = JSON.parse(data)
16
-
17
- if (!parsed[SWARM_CONFIG_KEY]) return null
18
-
19
- return SwarmConfigSchema.parse(parsed[SWARM_CONFIG_KEY])
20
- } catch {
21
- return null
22
- }
23
- }
24
-
25
- export async function setConfig(config: SwarmConfig): Promise<void> {
26
- await fs.ensureDir(OPENCODE_CONFIG_DIR)
27
-
28
- let existingConfig = {}
29
- try {
30
- const configExists = await fs.pathExists(OPENCODE_CONFIG_FILE)
31
- if (configExists) {
32
- existingConfig = await fs.readJson(OPENCODE_CONFIG_FILE)
33
- }
34
- } catch {
35
- // Config doesn't exist or is invalid, start fresh
36
- }
37
-
38
- const newConfig = {
39
- ...existingConfig,
40
- [SWARM_CONFIG_KEY]: config
41
- }
42
-
43
- await fs.writeJson(OPENCODE_CONFIG_FILE, newConfig, { spaces: 2 })
44
- }
45
-
46
- export async function clearConfig(): Promise<void> {
47
- const configExists = await fs.pathExists(OPENCODE_CONFIG_FILE)
48
- if (!configExists) return
49
-
50
- const config = await fs.readJson(OPENCODE_CONFIG_FILE)
51
-
52
- if (config[SWARM_CONFIG_KEY]) {
53
- delete config[SWARM_CONFIG_KEY]
54
- await fs.writeJson(OPENCODE_CONFIG_FILE, config, { spaces: 2 })
55
- }
56
- }
@@ -1,195 +0,0 @@
1
- import type { SwarmConfig, Subtask, SwarmTask, WorkerInfo } from './types.js'
2
- import { lockManager } from './file-lock.js'
3
- import { StateManager } from './state.js'
4
- import { executeSubtask } from './worker.js'
5
-
6
- export function createSwarmCoordinator(
7
- client: any,
8
- directory: string,
9
- config: SwarmConfig
10
- ) {
11
- const state = new StateManager()
12
-
13
- return {
14
- async execute(task: string, decomposition: any, maxWorkers: number) {
15
- const taskId = `task-${Date.now()}`
16
-
17
- const swarmTask: SwarmTask = {
18
- id: taskId,
19
- description: task,
20
- status: 'in_progress',
21
- subtasks: decomposition.subtasks,
22
- createdAt: Date.now()
23
- }
24
-
25
- await state.setCurrentTask(swarmTask)
26
-
27
- // Create worker sessions (child sessions)
28
- const workers: WorkerInfo[] = []
29
- for (let i = 0; i < decomposition.numWorkers; i++) {
30
- try {
31
- const session = await client.session.create({
32
- body: { title: `Swarm Worker ${i + 1}` }
33
- })
34
-
35
- workers.push({
36
- id: `worker-${i}`,
37
- sessionId: session.id,
38
- status: 'idle'
39
- })
40
-
41
- await state.updateWorker(workers[i])
42
- } catch (error) {
43
- await client.app.log({
44
- service: 'swarm-control',
45
- level: 'error',
46
- message: `Failed to create worker ${i}`,
47
- extra: { error: error instanceof Error ? error.message : 'Unknown error' }
48
- })
49
- throw new Error(`Failed to create worker session: ${error instanceof Error ? error.message : 'Unknown error'}`)
50
- }
51
- }
52
-
53
- // Execute subtasks respecting dependencies
54
- const results: string[] = []
55
- const completedSubtasks = new Set<string>()
56
-
57
- for (const subtask of decomposition.subtasks) {
58
- // Wait for dependencies
59
- if (subtask.dependsOn && subtask.dependsOn.length > 0) {
60
- await this.waitForDependencies(state, taskId, subtask.dependsOn, completedSubtasks)
61
- }
62
-
63
- // Acquire file locks
64
- for (const file of subtask.files) {
65
- const worker = this.findIdleWorker(workers)
66
- if (!worker) {
67
- throw new Error('No available workers')
68
- }
69
- await lockManager.acquireLock(file, worker.id)
70
- }
71
-
72
- // Assign to worker
73
- const worker = this.findIdleWorker(workers)
74
- if (!worker) {
75
- // Release locks and throw error
76
- for (const file of subtask.files) {
77
- lockManager.releaseLock(file)
78
- }
79
- throw new Error('No available workers')
80
- }
81
-
82
- worker.status = 'working'
83
- worker.currentSubtaskId = subtask.id
84
- await state.updateWorker(worker)
85
-
86
- await state.updateSubtaskStatus(taskId, subtask.id, 'in_progress')
87
-
88
- // Execute subtask
89
- try {
90
- const result = await executeSubtask(client, worker, subtask, config.model)
91
- results.push(result)
92
-
93
- await state.updateSubtaskStatus(taskId, subtask.id, 'completed', result)
94
- completedSubtasks.add(subtask.id)
95
- } catch (error) {
96
- const errorMsg = error instanceof Error ? error.message : 'Unknown error'
97
- await client.app.log({
98
- service: 'swarm-control',
99
- level: 'error',
100
- message: `Subtask ${subtask.id} failed`,
101
- extra: { error: errorMsg, subtaskId: subtask.id }
102
- })
103
- await state.updateSubtaskStatus(taskId, subtask.id, 'failed', undefined, errorMsg)
104
- }
105
-
106
- // Release file locks
107
- for (const file of subtask.files) {
108
- lockManager.releaseLock(file)
109
- }
110
-
111
- worker.status = 'idle'
112
- worker.currentSubtaskId = undefined
113
- await state.updateWorker(worker)
114
- }
115
-
116
- swarmTask.status = 'completed'
117
- swarmTask.completedAt = Date.now()
118
- await state.setCurrentTask(swarmTask)
119
-
120
- return this.formatSummary(swarmTask, decomposition)
121
- },
122
-
123
- findIdleWorker(workers: WorkerInfo[]): WorkerInfo | undefined {
124
- return workers.find(w => w.status === 'idle')
125
- },
126
-
127
- async waitForDependencies(
128
- state: StateManager,
129
- taskId: string,
130
- depIds: string[],
131
- completedSubtasks: Set<string>
132
- ) {
133
- let attempts = 0
134
- const maxAttempts = 300 // 5 minutes timeout
135
-
136
- while (attempts < maxAttempts) {
137
- const task = await state.getCurrentTask()
138
- if (!task) break
139
-
140
- const allComplete = depIds.every(id => {
141
- return completedSubtasks.has(id) || task.subtasks.find(s => s.id === id)?.status === 'completed'
142
- })
143
-
144
- if (allComplete) break
145
- await new Promise(resolve => setTimeout(resolve, 1000))
146
- attempts++
147
- }
148
- },
149
-
150
- formatSummary(task: SwarmTask, decomposition: any): string {
151
- const completed = task.subtasks.filter((s: any) => s.status === 'completed').length
152
- const failed = task.subtasks.filter((s: any) => s.status === 'failed').length
153
-
154
- let summary = `## Swarm Task Completed\n\n`
155
- summary += `**Task:** ${task.description}\n\n`
156
- summary += `**Subtasks:** ${completed}/${task.subtasks.length} completed`
157
-
158
- if (failed > 0) {
159
- summary += ` (${failed} failed)`
160
- }
161
-
162
- summary += `\n\n`
163
-
164
- if (decomposition.analysis) {
165
- summary += `**Approach:** ${decomposition.analysis}\n\n`
166
- }
167
-
168
- summary += `**Details:**\n\n`
169
-
170
- for (const subtask of task.subtasks) {
171
- const statusEmoji = subtask.status === 'completed' ? '✅' :
172
- subtask.status === 'failed' ? '❌' :
173
- subtask.status === 'in_progress' ? '⏳' : '⏸️'
174
-
175
- summary += `${statusEmoji} **${subtask.description}**\n`
176
-
177
- if (subtask.files && subtask.files.length > 0) {
178
- summary += ` Files: ${subtask.files.join(', ')}\n`
179
- }
180
-
181
- if (subtask.result) {
182
- summary += ` Result: ${subtask.result}\n`
183
- }
184
-
185
- if (subtask.error) {
186
- summary += ` Error: ${subtask.error}\n`
187
- }
188
-
189
- summary += '\n'
190
- }
191
-
192
- return summary.trim()
193
- }
194
- }
195
- }
@@ -1,87 +0,0 @@
1
- import { z } from 'zod'
2
-
3
- const DecompositionResponseSchema = z.object({
4
- analysis: z.string(),
5
- numWorkers: z.number().min(1).max(4),
6
- subtasks: z.array(z.object({
7
- id: z.string(),
8
- description: z.string(),
9
- files: z.array(z.string()),
10
- dependsOn: z.array(z.string()).optional()
11
- }))
12
- })
13
-
14
- export async function decomposeTask(
15
- client: any,
16
- model: string,
17
- task: string,
18
- context: any
19
- ) {
20
- // Get file list from project
21
- const filesResult = await client.find.files({
22
- query: { type: 'file', limit: 200 }
23
- })
24
-
25
- const files = filesResult.data || []
26
- const fileList = files.length > 0 ? files.join('\n') : 'No files found'
27
-
28
- const prompt = `You are a task decomposition system. Analyze this programming task:
29
-
30
- TASK: ${task}
31
-
32
- PROJECT FILES:
33
- ${fileList}
34
-
35
- Your goal:
36
- 1. Identify which files need to be modified
37
- 2. Group related files into logical subtasks (maximum 4 workers)
38
- 3. Determine if subtasks can be executed in parallel
39
- 4. Identify dependencies between subtasks
40
-
41
- RESPONSE FORMAT (JSON only):
42
- {
43
- "analysis": "Brief explanation of approach",
44
- "numWorkers": number (1-4),
45
- "subtasks": [
46
- {
47
- "id": "unique_id",
48
- "description": "Clear description",
49
- "files": ["path/to/file1.ts"],
50
- "dependsOn": ["id_of_dependency"]
51
- }
52
- ]
53
- }
54
-
55
- IMPORTANT:
56
- - Use the exact file paths from the PROJECT FILES list above
57
- - Maximum 4 workers total
58
- - Only include files that actually need to be modified
59
- - Provide unique IDs for each subtask`
60
-
61
- try {
62
- const result = await client.session.prompt({
63
- path: { id: context.sessionID },
64
- body: {
65
- model: {
66
- providerID: model.split('/')[0],
67
- modelID: model.split('/')[1]
68
- },
69
- parts: [{ type: 'text', text: prompt }]
70
- }
71
- })
72
-
73
- const responseText = result.parts?.find((p: any) => p.type === 'text')?.text || '{}'
74
- const jsonMatch = responseText.match(/\{[\s\S]*\}/)
75
- const jsonStr = jsonMatch ? jsonMatch[0] : responseText
76
-
77
- return DecompositionResponseSchema.parse(JSON.parse(jsonStr))
78
- } catch (error) {
79
- await client.app.log({
80
- service: 'swarm-control',
81
- level: 'error',
82
- message: 'Decomposition failed',
83
- extra: { error: error instanceof Error ? error.message : 'Unknown error', task }
84
- })
85
- throw new Error(`Failed to decompose task: ${error instanceof Error ? error.message : 'Unknown error'}`)
86
- }
87
- }
@@ -1,66 +0,0 @@
1
- import type { FileLock } from './types.js'
2
-
3
- class FileLockManager {
4
- private locks: Map<string, FileLock> = new Map()
5
- private waitQueue: Map<string, Array<(released: boolean) => void>> = new Map()
6
-
7
- async acquireLock(filePath: string, workerId: string): Promise<boolean> {
8
- if (!this.locks.has(filePath)) {
9
- this.locks.set(filePath, {
10
- filePath,
11
- lockedBy: workerId,
12
- lockedAt: Date.now()
13
- })
14
- return true
15
- }
16
-
17
- return new Promise((resolve) => {
18
- if (!this.waitQueue.has(filePath)) {
19
- this.waitQueue.set(filePath, [])
20
- }
21
- this.waitQueue.get(filePath)!.push(resolve)
22
- })
23
- }
24
-
25
- releaseLock(filePath: string): void {
26
- this.locks.delete(filePath)
27
-
28
- const queue = this.waitQueue.get(filePath)
29
- if (queue && queue.length > 0) {
30
- const next = queue.shift()!
31
- this.locks.set(filePath, {
32
- filePath,
33
- lockedBy: 'pending',
34
- lockedAt: Date.now()
35
- })
36
- next(true)
37
- }
38
-
39
- if (queue && queue.length === 0) {
40
- this.waitQueue.delete(filePath)
41
- }
42
- }
43
-
44
- isLocked(filePath: string): boolean {
45
- return this.locks.has(filePath)
46
- }
47
-
48
- getLock(filePath: string): FileLock | undefined {
49
- return this.locks.get(filePath)
50
- }
51
-
52
- releaseAllLocksForWorker(workerId: string): void {
53
- for (const [filePath, lock] of this.locks.entries()) {
54
- if (lock.lockedBy === workerId) {
55
- this.releaseLock(filePath)
56
- }
57
- }
58
- }
59
-
60
- clearAll(): void {
61
- this.locks.clear()
62
- this.waitQueue.clear()
63
- }
64
- }
65
-
66
- export const lockManager = new FileLockManager()
@@ -1,172 +0,0 @@
1
- import type { Plugin } from '@opencode-ai/plugin'
2
- import { tool } from '@opencode-ai/plugin/tool'
3
- import type { ToolContext } from '@opencode-ai/plugin/tool'
4
- import { getConfig, setConfig } from './config.js'
5
- import { decomposeTask } from './decomposer.js'
6
- import { createSwarmCoordinator } from './coordinator.js'
7
- import { StateManager } from './state.js'
8
- import { lockManager } from './file-lock.js'
9
-
10
- export const SwarmControlPlugin: Plugin = async ({ client, directory }: any) => {
11
- const state = new StateManager()
12
-
13
- return {
14
- tool: {
15
- swarm_model: tool({
16
- description: 'Configure model to use for swarm task decomposition (required before using swarm_spawn)',
17
- args: {
18
- model: tool.schema.string().describe('Model identifier (e.g., anthropic/claude-3-5-sonnet-20241022)')
19
- },
20
- async execute({ model }: any) {
21
- await setConfig({ model })
22
- return `Swarm model set to: ${model}`
23
- }
24
- }),
25
-
26
- swarm_spawn: tool({
27
- description: 'Spawn a swarm of subagents to tackle a complex programming task',
28
- args: {
29
- task: tool.schema.string().describe('The task description to decompose and execute'),
30
- maxWorkers: tool.schema.number().min(1).max(4).optional().describe('Maximum number of workers (default: 4)')
31
- },
32
- async execute({ task, maxWorkers = 4 }: any, context: ToolContext) {
33
- // Check if model is configured
34
- const config = await getConfig()
35
- if (!config) {
36
- throw new Error('Swarm model not configured. Please run /swarm_model <model> first to set the model for task decomposition.')
37
- }
38
-
39
- // Check if there's already a running task
40
- const currentTask = await state.getCurrentTask()
41
- if (currentTask && currentTask.status === 'in_progress') {
42
- throw new Error(`A swarm task is already in progress. Task ID: ${currentTask.id}. Use /swarm_status to check progress or wait for it to complete.`)
43
- }
44
-
45
- // Clear any stale locks
46
- lockManager.clearAll()
47
-
48
- let decompositionResult
49
-
50
- try {
51
- // Decompose task
52
- decompositionResult = await decomposeTask(client, config.model, task, context)
53
-
54
- // Validate numWorkers doesn't exceed maxWorkers
55
- const numWorkers = Math.min(decompositionResult.numWorkers, maxWorkers)
56
-
57
- // Create and run coordinator
58
- const coordinator = createSwarmCoordinator(client, directory, config)
59
- const result = await coordinator.execute(task, {
60
- ...decompositionResult,
61
- numWorkers
62
- }, numWorkers)
63
-
64
- return result
65
-
66
- } catch (error) {
67
- // Clean up on error
68
- lockManager.clearAll()
69
- await state.clearTask()
70
-
71
- const errorMsg = error instanceof Error ? error.message : 'Unknown error'
72
- throw new Error(`Swarm execution failed: ${errorMsg}`)
73
- }
74
- }
75
- }),
76
-
77
- swarm_status: tool({
78
- description: 'Show status of current or recent swarm tasks',
79
- args: {
80
- taskId: tool.schema.string().optional().describe('Specific task ID to check (if omitted, shows current task)')
81
- },
82
- async execute({ taskId }: any) {
83
- if (taskId) {
84
- // For specific task ID, we'd need to implement task history
85
- return `Task history not yet implemented. Use without arguments to check current task status.`
86
- }
87
-
88
- const currentTask = await state.getCurrentTask()
89
-
90
- if (!currentTask) {
91
- return 'No active swarm task. Use /swarm_spawn to start a new task.'
92
- }
93
-
94
- let status = `## Swarm Task Status\n\n`
95
- status += `**Task ID:** ${currentTask.id}\n`
96
- status += `**Description:** ${currentTask.description}\n`
97
- status += `**Status:** ${currentTask.status.toUpperCase()}\n`
98
-
99
- if (currentTask.completedAt) {
100
- const duration = Math.round((currentTask.completedAt - currentTask.createdAt) / 1000)
101
- status += `**Duration:** ${duration}s\n`
102
- }
103
-
104
- status += `\n**Subtasks:**\n\n`
105
-
106
- const completed = currentTask.subtasks.filter((s: any) => s.status === 'completed').length
107
- const failed = currentTask.subtasks.filter((s: any) => s.status === 'failed').length
108
- const inProgress = currentTask.subtasks.filter((s: any) => s.status === 'in_progress').length
109
-
110
- status += `- Total: ${currentTask.subtasks.length}\n`
111
- status += `- ✅ Completed: ${completed}\n`
112
- if (inProgress > 0) {
113
- status += `- ⏳ In Progress: ${inProgress}\n`
114
- }
115
- if (failed > 0) {
116
- status += `- ❌ Failed: ${failed}\n`
117
- }
118
-
119
- status += `\n**Details:**\n\n`
120
-
121
- for (const subtask of currentTask.subtasks) {
122
- const statusEmoji = subtask.status === 'completed' ? '✅' :
123
- subtask.status === 'failed' ? '❌' :
124
- subtask.status === 'in_progress' ? '⏳' : '⏸️'
125
-
126
- status += `${statusEmoji} **${subtask.description}**\n`
127
-
128
- if (subtask.files && subtask.files.length > 0) {
129
- status += ` Files: ${subtask.files.join(', ')}\n`
130
- }
131
-
132
- if (subtask.workerId) {
133
- status += ` Worker: ${subtask.workerId}\n`
134
- }
135
-
136
- if (subtask.result) {
137
- status += ` Result: ${subtask.result}\n`
138
- }
139
-
140
- if (subtask.error) {
141
- status += ` Error: ${subtask.error}\n`
142
- }
143
-
144
- status += '\n'
145
- }
146
-
147
- // Show worker status
148
- const workers = await state.getWorkers()
149
- if (workers.length > 0) {
150
- status += `**Workers:**\n\n`
151
-
152
- for (const worker of workers) {
153
- const statusEmoji = worker.status === 'idle' ? '💤' :
154
- worker.status === 'working' ? '🔨' :
155
- worker.status === 'waiting_for_lock' ? '⏳' : '❌'
156
-
157
- status += `${statusEmoji} **${worker.id}** (${worker.status})`
158
-
159
- if (worker.currentSubtaskId) {
160
- status += ` - Working on: ${worker.currentSubtaskId}`
161
- }
162
-
163
- status += '\n'
164
- }
165
- }
166
-
167
- return status.trim()
168
- }
169
- })
170
- }
171
- }
172
- }
@@ -1,81 +0,0 @@
1
- import fs from 'fs-extra'
2
- import path from 'path'
3
- import type { SwarmState, SwarmTask, WorkerInfo } from './types.js'
4
-
5
- const STATE_DIR = path.join(process.env.HOME!, '.config/swarm-control')
6
- const STATE_FILE = path.join(STATE_DIR, 'state.json')
7
-
8
- export class StateManager {
9
- async getState(): Promise<SwarmState> {
10
- try {
11
- const data = await fs.readFile(STATE_FILE, 'utf-8')
12
- const parsed = JSON.parse(data)
13
- return {
14
- currentTask: parsed.currentTask,
15
- workers: parsed.workers || [],
16
- fileLocks: parsed.fileLocks || []
17
- }
18
- } catch {
19
- return {
20
- currentTask: null,
21
- workers: [],
22
- fileLocks: []
23
- }
24
- }
25
- }
26
-
27
- async setState(state: SwarmState): Promise<void> {
28
- await fs.ensureDir(STATE_DIR)
29
- const tempFile = `${STATE_FILE}.tmp`
30
-
31
- await fs.writeJSON(tempFile, state, { spaces: 2 })
32
- await fs.move(tempFile, STATE_FILE, { overwrite: true })
33
- }
34
-
35
- async getCurrentTask(): Promise<SwarmTask | null> {
36
- const state = await this.getState()
37
- return state.currentTask
38
- }
39
-
40
- async setCurrentTask(task: SwarmTask | null): Promise<void> {
41
- const state = await this.getState()
42
- state.currentTask = task
43
- await this.setState(state)
44
- }
45
-
46
- async updateSubtaskStatus(taskId: string, subtaskId: string, status: 'pending' | 'in_progress' | 'completed' | 'failed', result?: string, error?: string): Promise<void> {
47
- const state = await this.getState()
48
- if (!state.currentTask || state.currentTask.id !== taskId) return
49
-
50
- const subtask = state.currentTask.subtasks.find(s => s.id === subtaskId)
51
- if (subtask) {
52
- subtask.status = status
53
- if (result) subtask.result = result
54
- if (error) subtask.error = error
55
- await this.setState(state)
56
- }
57
- }
58
-
59
- async getWorkers(): Promise<WorkerInfo[]> {
60
- const state = await this.getState()
61
- return state.workers
62
- }
63
-
64
- async updateWorker(worker: WorkerInfo): Promise<void> {
65
- const state = await this.getState()
66
- const index = state.workers.findIndex((w: WorkerInfo) => w.id === worker.id)
67
- if (index >= 0) {
68
- state.workers[index] = worker
69
- } else {
70
- state.workers.push(worker)
71
- }
72
- await this.setState(state)
73
- }
74
-
75
- async clearTask(): Promise<void> {
76
- const state = await this.getState()
77
- state.currentTask = null
78
- state.workers = []
79
- await this.setState(state)
80
- }
81
- }
@@ -1,47 +0,0 @@
1
- import { z } from 'zod'
2
-
3
- export const SwarmConfigSchema = z.object({
4
- model: z.string().describe('Model to use for decomposition'),
5
- })
6
-
7
- export type SwarmConfig = z.infer<typeof SwarmConfigSchema>
8
-
9
- export interface SwarmTask {
10
- id: string
11
- description: string
12
- status: 'pending' | 'in_progress' | 'completed' | 'failed'
13
- subtasks: Subtask[]
14
- createdAt: number
15
- completedAt?: number
16
- }
17
-
18
- export interface Subtask {
19
- id: string
20
- description: string
21
- files: string[]
22
- status: 'pending' | 'in_progress' | 'completed' | 'failed'
23
- workerId?: string
24
- sessionId?: string
25
- dependsOn?: string[]
26
- result?: string
27
- error?: string
28
- }
29
-
30
- export interface FileLock {
31
- filePath: string
32
- lockedBy: string
33
- lockedAt: number
34
- }
35
-
36
- export interface SwarmState {
37
- currentTask: SwarmTask | null
38
- workers: WorkerInfo[]
39
- fileLocks: FileLock[]
40
- }
41
-
42
- export interface WorkerInfo {
43
- id: string
44
- sessionId: string
45
- status: 'idle' | 'working' | 'waiting_for_lock' | 'error'
46
- currentSubtaskId?: string
47
- }
@@ -1,50 +0,0 @@
1
- import type { WorkerInfo, Subtask } from './types.js'
2
-
3
- export async function executeSubtask(
4
- client: any,
5
- worker: WorkerInfo,
6
- subtask: Subtask,
7
- model: string
8
- ): Promise<string> {
9
- const prompt = `You are a specialized worker agent. Execute this specific task:
10
-
11
- ${subtask.description}
12
-
13
- Files to modify:
14
- ${subtask.files.map((f: string) => `- ${f}`).join('\n')}
15
-
16
- Instructions:
17
- 1. Work on the specified files
18
- 2. Complete the task described above
19
- 3. Return a summary of changes made
20
- 4. If you encounter any errors, describe them clearly
21
-
22
- IMPORTANT:
23
- - Only modify the files listed above
24
- - Be specific about what changes you made
25
- - If a file doesn't exist, mention it in your response`
26
-
27
- try {
28
- const result = await client.session.prompt({
29
- path: { id: worker.sessionId },
30
- body: {
31
- model: {
32
- providerID: model.split('/')[0],
33
- modelID: model.split('/')[1]
34
- },
35
- parts: [{ type: 'text', text: prompt }]
36
- }
37
- })
38
-
39
- const responseText = result.parts?.find((p: any) => p.type === 'text')?.text || ''
40
-
41
- // Extract a concise summary
42
- const lines = responseText.split('\n').filter((l: string) => l.trim())
43
- const summary = lines.slice(0, 5).join(' ')
44
-
45
- return summary || 'Task completed'
46
-
47
- } catch (error) {
48
- throw new Error(`Worker execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
49
- }
50
- }