@clawswarm/core 0.1.0-alpha
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/README.md +41 -0
- package/dist/agent.d.ts +79 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +146 -0
- package/dist/agent.js.map +1 -0
- package/dist/chief.d.ts +80 -0
- package/dist/chief.d.ts.map +1 -0
- package/dist/chief.js +221 -0
- package/dist/chief.js.map +1 -0
- package/dist/clawswarm.d.ts +84 -0
- package/dist/clawswarm.d.ts.map +1 -0
- package/dist/clawswarm.js +224 -0
- package/dist/clawswarm.js.map +1 -0
- package/dist/goal.d.ts +66 -0
- package/dist/goal.d.ts.map +1 -0
- package/dist/goal.js +164 -0
- package/dist/goal.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/swarm.d.ts +67 -0
- package/dist/swarm.d.ts.map +1 -0
- package/dist/swarm.js +201 -0
- package/dist/swarm.js.map +1 -0
- package/dist/task.d.ts +86 -0
- package/dist/task.d.ts.map +1 -0
- package/dist/task.js +177 -0
- package/dist/task.js.map +1 -0
- package/dist/types.d.ts +189 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/package.json +67 -0
- package/src/__tests__/integration.test.ts +686 -0
- package/src/agent.ts +163 -0
- package/src/chief.ts +264 -0
- package/src/clawswarm.ts +257 -0
- package/src/goal.ts +183 -0
- package/src/index.ts +62 -0
- package/src/swarm.ts +225 -0
- package/src/task.ts +204 -0
- package/src/types.ts +240 -0
- package/tsconfig.json +11 -0
- package/tsconfig.tsbuildinfo +1 -0
package/src/agent.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Agent class and specialist agent factories.
|
|
3
|
+
* @module @clawswarm/core/agent
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { AgentConfig, AgentStatus, AgentType, ModelId, Task, Deliverable } from './types.js';
|
|
7
|
+
|
|
8
|
+
// ─── Agent Base Class ─────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Base class for all ClawSwarm agents.
|
|
12
|
+
* Extend this to create custom specialist agents.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* class MyCustomAgent extends Agent {
|
|
17
|
+
* async execute(task: Task): Promise<Deliverable[]> {
|
|
18
|
+
* // your custom logic here
|
|
19
|
+
* return [{ type: 'text', label: 'Output', content: '...' }];
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export class Agent {
|
|
25
|
+
public readonly id: string;
|
|
26
|
+
public readonly config: AgentConfig;
|
|
27
|
+
public status: AgentStatus = 'idle';
|
|
28
|
+
public currentTaskId?: string;
|
|
29
|
+
|
|
30
|
+
constructor(config: AgentConfig) {
|
|
31
|
+
this.config = config;
|
|
32
|
+
this.id = `agent-${config.type}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Agent's display name */
|
|
36
|
+
get name(): string {
|
|
37
|
+
return this.config.name ?? this._defaultName(this.config.type);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Agent's specialization type */
|
|
41
|
+
get type(): AgentType {
|
|
42
|
+
return this.config.type;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Execute a task and return deliverables.
|
|
47
|
+
* Override this in custom agents.
|
|
48
|
+
*
|
|
49
|
+
* @param task - The task to execute
|
|
50
|
+
* @returns Array of deliverables produced
|
|
51
|
+
*/
|
|
52
|
+
async execute(task: Task): Promise<Deliverable[]> {
|
|
53
|
+
throw new Error(`Agent.execute() must be implemented. Agent: ${this.name}, Task: ${task.id}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if this agent can handle a given task type.
|
|
58
|
+
* Override to restrict which tasks this agent accepts.
|
|
59
|
+
*/
|
|
60
|
+
canHandle(_task: Task): boolean {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get the system prompt for this agent.
|
|
66
|
+
* Override to customize the agent's behavior.
|
|
67
|
+
*/
|
|
68
|
+
getSystemPrompt(): string {
|
|
69
|
+
return this.config.systemPrompt ?? this._defaultSystemPrompt(this.config.type);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── Factory Methods ─────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Create a ResearchClaw agent.
|
|
76
|
+
* Specializes in information gathering, analysis, and written reports.
|
|
77
|
+
*/
|
|
78
|
+
static research(options: Partial<AgentConfig> & { model: ModelId }): AgentConfig {
|
|
79
|
+
return {
|
|
80
|
+
type: 'research',
|
|
81
|
+
name: 'ResearchClaw',
|
|
82
|
+
tools: ['web_search', 'web_fetch', 'summarize'],
|
|
83
|
+
...options,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Create a CodeClaw agent.
|
|
89
|
+
* Specializes in writing, reviewing, and debugging code.
|
|
90
|
+
*/
|
|
91
|
+
static code(options: Partial<AgentConfig> & { model: ModelId }): AgentConfig {
|
|
92
|
+
return {
|
|
93
|
+
type: 'code',
|
|
94
|
+
name: 'CodeClaw',
|
|
95
|
+
tools: ['read_file', 'write_file', 'execute_code', 'run_tests'],
|
|
96
|
+
...options,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Create an OpsClaw agent.
|
|
102
|
+
* Specializes in infrastructure, deployment, and monitoring.
|
|
103
|
+
*/
|
|
104
|
+
static ops(options: Partial<AgentConfig> & { model: ModelId }): AgentConfig {
|
|
105
|
+
return {
|
|
106
|
+
type: 'ops',
|
|
107
|
+
name: 'OpsClaw',
|
|
108
|
+
tools: ['shell', 'docker', 'kubernetes', 'monitoring'],
|
|
109
|
+
...options,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Create a Planner agent.
|
|
115
|
+
* Decomposes goals into tasks and assigns them to specialist agents.
|
|
116
|
+
*/
|
|
117
|
+
static planner(options: Partial<AgentConfig> & { model: ModelId }): AgentConfig {
|
|
118
|
+
return {
|
|
119
|
+
type: 'planner',
|
|
120
|
+
name: 'Planner',
|
|
121
|
+
tools: [],
|
|
122
|
+
...options,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── Private Helpers ─────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
private _defaultName(type: AgentType): string {
|
|
129
|
+
const names: Record<AgentType, string> = {
|
|
130
|
+
research: 'ResearchClaw',
|
|
131
|
+
code: 'CodeClaw',
|
|
132
|
+
ops: 'OpsClaw',
|
|
133
|
+
planner: 'Planner',
|
|
134
|
+
custom: 'CustomAgent',
|
|
135
|
+
};
|
|
136
|
+
return names[type] ?? 'Agent';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private _defaultSystemPrompt(type: AgentType): string {
|
|
140
|
+
const prompts: Record<AgentType, string> = {
|
|
141
|
+
research: `You are ResearchClaw, a specialist research agent.
|
|
142
|
+
Your job is to gather information, analyze data, synthesize findings, and produce clear written reports.
|
|
143
|
+
Always cite your sources. Prioritize accuracy over speed. Flag uncertainty explicitly.`,
|
|
144
|
+
|
|
145
|
+
code: `You are CodeClaw, a specialist software engineering agent.
|
|
146
|
+
Your job is to write clean, well-tested, production-ready code.
|
|
147
|
+
Follow best practices for the language/framework. Write tests. Document your code.
|
|
148
|
+
Never ship broken code.`,
|
|
149
|
+
|
|
150
|
+
ops: `You are OpsClaw, a specialist infrastructure and operations agent.
|
|
151
|
+
Your job is to deploy, monitor, and optimize systems.
|
|
152
|
+
Prefer idempotent operations. Document every change. Always have a rollback plan.`,
|
|
153
|
+
|
|
154
|
+
planner: `You are the Planner, responsible for decomposing high-level goals into concrete tasks.
|
|
155
|
+
Break goals into the smallest meaningful units of work.
|
|
156
|
+
Assign each task to the most appropriate specialist agent.
|
|
157
|
+
Identify dependencies between tasks and sequence them correctly.`,
|
|
158
|
+
|
|
159
|
+
custom: `You are a custom ClawSwarm agent. Follow your configured instructions.`,
|
|
160
|
+
};
|
|
161
|
+
return prompts[type] ?? prompts.custom;
|
|
162
|
+
}
|
|
163
|
+
}
|
package/src/chief.ts
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chief review pipeline — the quality gate for ClawSwarm.
|
|
3
|
+
*
|
|
4
|
+
* Every task deliverable passes through a 3-tier scoring system:
|
|
5
|
+
* - Score ≥ autoApproveThreshold (default 8) → auto-approved
|
|
6
|
+
* - Score ≥ humanReviewThreshold (default 5) → human review required
|
|
7
|
+
* - Score < humanReviewThreshold → auto-rejected + rework
|
|
8
|
+
*
|
|
9
|
+
* @module @clawswarm/core/chief
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import EventEmitter from 'eventemitter3';
|
|
13
|
+
import {
|
|
14
|
+
Task,
|
|
15
|
+
ReviewResult,
|
|
16
|
+
ChiefReviewConfig,
|
|
17
|
+
ModelId,
|
|
18
|
+
} from './types.js';
|
|
19
|
+
|
|
20
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
const DEFAULT_AUTO_APPROVE_THRESHOLD = 8;
|
|
23
|
+
const DEFAULT_HUMAN_REVIEW_THRESHOLD = 5;
|
|
24
|
+
const DEFAULT_REVIEWER_MODEL: ModelId = 'claude-sonnet-4';
|
|
25
|
+
|
|
26
|
+
const DEFAULT_CRITERIA = [
|
|
27
|
+
'completeness: Does the output fully address the task requirements?',
|
|
28
|
+
'accuracy: Is the information correct and well-sourced?',
|
|
29
|
+
'quality: Is the output production-ready (no TODOs, no placeholders)?',
|
|
30
|
+
'clarity: Is the output clear, well-structured, and easy to understand?',
|
|
31
|
+
'safety: Does the output avoid harmful, biased, or problematic content?',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// ─── Chief Reviewer ───────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The Chief Reviewer evaluates task deliverables against a rubric
|
|
38
|
+
* and decides whether to approve, send for human review, or reject.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* const reviewer = new ChiefReviewer({
|
|
43
|
+
* autoApproveThreshold: 8,
|
|
44
|
+
* humanReviewThreshold: 5,
|
|
45
|
+
* reviewerModel: 'claude-opus-4',
|
|
46
|
+
* criteria: ['completeness', 'accuracy', 'quality'],
|
|
47
|
+
* });
|
|
48
|
+
*
|
|
49
|
+
* const result = await reviewer.review(task);
|
|
50
|
+
*
|
|
51
|
+
* if (result.decision === 'approved') {
|
|
52
|
+
* console.log('✅ Task approved!', result.score);
|
|
53
|
+
* } else if (result.decision === 'human_review') {
|
|
54
|
+
* console.log('👀 Needs human review', result.feedback);
|
|
55
|
+
* } else {
|
|
56
|
+
* console.log('❌ Rejected:', result.issues);
|
|
57
|
+
* }
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export class ChiefReviewer extends EventEmitter {
|
|
61
|
+
private readonly autoApproveThreshold: number;
|
|
62
|
+
private readonly humanReviewThreshold: number;
|
|
63
|
+
private readonly reviewerModel: ModelId;
|
|
64
|
+
private readonly criteria: string[];
|
|
65
|
+
|
|
66
|
+
constructor(config: ChiefReviewConfig = {}) {
|
|
67
|
+
super();
|
|
68
|
+
this.autoApproveThreshold = config.autoApproveThreshold ?? DEFAULT_AUTO_APPROVE_THRESHOLD;
|
|
69
|
+
this.humanReviewThreshold = config.humanReviewThreshold ?? DEFAULT_HUMAN_REVIEW_THRESHOLD;
|
|
70
|
+
this.reviewerModel = config.reviewerModel ?? DEFAULT_REVIEWER_MODEL;
|
|
71
|
+
this.criteria = config.criteria ?? DEFAULT_CRITERIA;
|
|
72
|
+
|
|
73
|
+
this._validateThresholds();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Review a task and produce a structured ReviewResult.
|
|
78
|
+
*
|
|
79
|
+
* @param task - The task to review (must have deliverables)
|
|
80
|
+
* @returns A ReviewResult with score, decision, and feedback
|
|
81
|
+
*/
|
|
82
|
+
async review(task: Task): Promise<ReviewResult> {
|
|
83
|
+
if (task.deliverables.length === 0) {
|
|
84
|
+
return this._buildResult(task.id, 0, [], ['No deliverables were produced by the agent.'], []);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// In production: call LLM with structured review prompt
|
|
88
|
+
const raw = await this._callReviewerLLM(task);
|
|
89
|
+
|
|
90
|
+
const result = this._buildResult(
|
|
91
|
+
task.id,
|
|
92
|
+
raw.score,
|
|
93
|
+
raw.issues,
|
|
94
|
+
raw.suggestions,
|
|
95
|
+
raw.feedback
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
this.emit('reviewed', result);
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Synchronously check what decision would be made for a given score.
|
|
104
|
+
* Useful for dry-runs and testing.
|
|
105
|
+
*/
|
|
106
|
+
scoreToDecision(score: number): ReviewResult['decision'] {
|
|
107
|
+
if (score >= this.autoApproveThreshold) return 'approved';
|
|
108
|
+
if (score >= this.humanReviewThreshold) return 'human_review';
|
|
109
|
+
return 'rejected';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get the current review configuration (read-only).
|
|
114
|
+
*/
|
|
115
|
+
get config(): Required<ChiefReviewConfig> {
|
|
116
|
+
return {
|
|
117
|
+
autoApproveThreshold: this.autoApproveThreshold,
|
|
118
|
+
humanReviewThreshold: this.humanReviewThreshold,
|
|
119
|
+
reviewerModel: this.reviewerModel,
|
|
120
|
+
criteria: this.criteria,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── Private ──────────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Build a review prompt for the LLM.
|
|
128
|
+
* @internal
|
|
129
|
+
*/
|
|
130
|
+
private _buildPrompt(task: Task): string {
|
|
131
|
+
const deliverablesSummary = task.deliverables
|
|
132
|
+
.map((d, i) => `[${i + 1}] ${d.label} (${d.type}):\n${d.content.slice(0, 2000)}`)
|
|
133
|
+
.join('\n\n');
|
|
134
|
+
|
|
135
|
+
const criteriaList = this.criteria.map((c, i) => `${i + 1}. ${c}`).join('\n');
|
|
136
|
+
|
|
137
|
+
return `You are a Chief Reviewer for an AI agent system. Your job is to objectively score the quality of agent-produced work.
|
|
138
|
+
|
|
139
|
+
## Task
|
|
140
|
+
Title: ${task.title}
|
|
141
|
+
Description: ${task.description}
|
|
142
|
+
|
|
143
|
+
## Deliverables
|
|
144
|
+
${deliverablesSummary}
|
|
145
|
+
|
|
146
|
+
## Review Criteria (score each 0-10, then average)
|
|
147
|
+
${criteriaList}
|
|
148
|
+
|
|
149
|
+
## Instructions
|
|
150
|
+
1. Score each criterion from 0 to 10
|
|
151
|
+
2. Identify specific issues (things that are wrong or missing)
|
|
152
|
+
3. Provide concrete suggestions for improvement
|
|
153
|
+
4. Give an overall score (0-10) and a 2-3 sentence summary
|
|
154
|
+
|
|
155
|
+
Respond in JSON:
|
|
156
|
+
{
|
|
157
|
+
"criteriaScores": { "<criterion>": <score> },
|
|
158
|
+
"overallScore": <number>,
|
|
159
|
+
"issues": ["<issue1>", ...],
|
|
160
|
+
"suggestions": ["<suggestion1>", ...],
|
|
161
|
+
"feedback": "<2-3 sentence summary>"
|
|
162
|
+
}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Call the LLM reviewer. In production, replace the stub with a real LLM call.
|
|
167
|
+
* @internal
|
|
168
|
+
*/
|
|
169
|
+
private async _callReviewerLLM(task: Task): Promise<RawReviewResponse> {
|
|
170
|
+
// ── Production stub ──────────────────────────────────────────────────────
|
|
171
|
+
// Replace this with your actual LLM client call, e.g.:
|
|
172
|
+
//
|
|
173
|
+
// const response = await openai.chat.completions.create({
|
|
174
|
+
// model: this.reviewerModel,
|
|
175
|
+
// messages: [{ role: 'user', content: this._buildPrompt(task) }],
|
|
176
|
+
// response_format: { type: 'json_object' },
|
|
177
|
+
// });
|
|
178
|
+
// return JSON.parse(response.choices[0].message.content!);
|
|
179
|
+
//
|
|
180
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
void this._buildPrompt(task); // reference so it's not dead code
|
|
183
|
+
|
|
184
|
+
// Stub: evaluate based on deliverable completeness heuristics
|
|
185
|
+
const hasContent = task.deliverables.some(d => d.content.trim().length > 100);
|
|
186
|
+
const hasTodo = task.deliverables.some(d => /TODO|FIXME|placeholder/i.test(d.content));
|
|
187
|
+
const hasCode = task.deliverables.some(d => d.type === 'code');
|
|
188
|
+
const contentLength = task.deliverables.reduce((sum, d) => sum + d.content.length, 0);
|
|
189
|
+
|
|
190
|
+
let score = hasContent ? 7 : 3;
|
|
191
|
+
if (hasTodo) score -= 2;
|
|
192
|
+
if (hasCode && contentLength > 500) score += 1;
|
|
193
|
+
score = Math.max(0, Math.min(10, score));
|
|
194
|
+
|
|
195
|
+
const issues: string[] = [];
|
|
196
|
+
const suggestions: string[] = [];
|
|
197
|
+
|
|
198
|
+
if (!hasContent) issues.push('Deliverables appear to be empty or too short.');
|
|
199
|
+
if (hasTodo) {
|
|
200
|
+
issues.push('Output contains TODO/FIXME markers — not production-ready.');
|
|
201
|
+
suggestions.push('Complete all TODO items before submitting.');
|
|
202
|
+
}
|
|
203
|
+
if (contentLength < 200) suggestions.push('Expand the output with more detail.');
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
score,
|
|
207
|
+
issues,
|
|
208
|
+
suggestions,
|
|
209
|
+
feedback: issues.length === 0
|
|
210
|
+
? 'Work looks complete and meets the task requirements.'
|
|
211
|
+
: `Found ${issues.length} issue(s) that need attention before approval.`,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Assemble a ReviewResult from raw LLM data.
|
|
217
|
+
* @internal
|
|
218
|
+
*/
|
|
219
|
+
private _buildResult(
|
|
220
|
+
taskId: string,
|
|
221
|
+
score: number,
|
|
222
|
+
issues: string[],
|
|
223
|
+
suggestions: string[],
|
|
224
|
+
feedback: string | string[]
|
|
225
|
+
): ReviewResult {
|
|
226
|
+
const clampedScore = Math.max(0, Math.min(10, score));
|
|
227
|
+
const feedbackStr = Array.isArray(feedback) ? feedback.join(' ') : feedback;
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
taskId,
|
|
231
|
+
score: clampedScore,
|
|
232
|
+
decision: this.scoreToDecision(clampedScore),
|
|
233
|
+
feedback: feedbackStr,
|
|
234
|
+
issues,
|
|
235
|
+
suggestions,
|
|
236
|
+
reviewedAt: new Date().toISOString(),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Validate that thresholds are logically consistent.
|
|
242
|
+
* @internal
|
|
243
|
+
*/
|
|
244
|
+
private _validateThresholds(): void {
|
|
245
|
+
if (this.autoApproveThreshold < this.humanReviewThreshold) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
`ChiefReviewer: autoApproveThreshold (${this.autoApproveThreshold}) must be ` +
|
|
248
|
+
`>= humanReviewThreshold (${this.humanReviewThreshold})`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
if (this.autoApproveThreshold > 10 || this.humanReviewThreshold < 0) {
|
|
252
|
+
throw new Error('ChiefReviewer: thresholds must be between 0 and 10');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ─── Internal Types ───────────────────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
interface RawReviewResponse {
|
|
260
|
+
score: number;
|
|
261
|
+
issues: string[];
|
|
262
|
+
suggestions: string[];
|
|
263
|
+
feedback: string;
|
|
264
|
+
}
|
package/src/clawswarm.ts
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClawSwarm — main orchestrator class.
|
|
3
|
+
*
|
|
4
|
+
* Creates and manages a swarm of specialist agents, decomposes goals
|
|
5
|
+
* into tasks, runs the chief review pipeline, and emits events throughout.
|
|
6
|
+
*
|
|
7
|
+
* @module @clawswarm/core/clawswarm
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import EventEmitter from 'eventemitter3';
|
|
11
|
+
import { Agent } from './agent.js';
|
|
12
|
+
import { GoalManager, GoalPlanner } from './goal.js';
|
|
13
|
+
import { TaskManager } from './task.js';
|
|
14
|
+
import { ChiefReviewer } from './chief.js';
|
|
15
|
+
import {
|
|
16
|
+
SwarmConfig,
|
|
17
|
+
SwarmEvents,
|
|
18
|
+
GoalResult,
|
|
19
|
+
CreateGoalInput,
|
|
20
|
+
Goal,
|
|
21
|
+
Task,
|
|
22
|
+
ReviewResult,
|
|
23
|
+
AgentType,
|
|
24
|
+
} from './types.js';
|
|
25
|
+
|
|
26
|
+
// ─── ClawSwarm ────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The primary interface for the ClawSwarm framework.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* const swarm = new ClawSwarm({
|
|
34
|
+
* agents: [
|
|
35
|
+
* Agent.research({ model: 'claude-sonnet-4' }),
|
|
36
|
+
* Agent.code({ model: 'gpt-4o' }),
|
|
37
|
+
* Agent.ops({ model: 'gemini-pro' }),
|
|
38
|
+
* ],
|
|
39
|
+
* chiefReview: { autoApproveThreshold: 8, humanReviewThreshold: 5 },
|
|
40
|
+
* });
|
|
41
|
+
*
|
|
42
|
+
* swarm.on('task:completed', (task) => console.log('✅', task.title));
|
|
43
|
+
*
|
|
44
|
+
* const result = await swarm.execute(goal);
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export class ClawSwarm extends (EventEmitter as new () => EventEmitter<SwarmEvents>) {
|
|
48
|
+
private readonly goalManager: GoalManager;
|
|
49
|
+
private readonly taskManager: TaskManager;
|
|
50
|
+
private readonly planner: GoalPlanner;
|
|
51
|
+
private readonly reviewer: ChiefReviewer;
|
|
52
|
+
private readonly agents: Map<AgentType, Agent>;
|
|
53
|
+
private readonly config: SwarmConfig;
|
|
54
|
+
|
|
55
|
+
constructor(config: SwarmConfig) {
|
|
56
|
+
super();
|
|
57
|
+
this.config = config;
|
|
58
|
+
this.goalManager = new GoalManager();
|
|
59
|
+
this.taskManager = new TaskManager();
|
|
60
|
+
this.planner = new GoalPlanner(config);
|
|
61
|
+
this.reviewer = new ChiefReviewer(config.chiefReview);
|
|
62
|
+
this.agents = new Map();
|
|
63
|
+
|
|
64
|
+
// Register agents
|
|
65
|
+
for (const agentConfig of config.agents) {
|
|
66
|
+
const agent = new Agent(agentConfig);
|
|
67
|
+
// Use last-registered agent if multiple of same type
|
|
68
|
+
this.agents.set(agentConfig.type, agent);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── Public API ───────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Create a new goal (without executing it).
|
|
76
|
+
* Use `execute()` to run the goal.
|
|
77
|
+
*/
|
|
78
|
+
createGoal(input: CreateGoalInput): Goal {
|
|
79
|
+
const goal = this.goalManager.create(input);
|
|
80
|
+
this.emit('goal:created', goal);
|
|
81
|
+
return goal;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Execute a goal end-to-end:
|
|
86
|
+
* 1. Decompose into tasks (Planner)
|
|
87
|
+
* 2. Run each task with the appropriate specialist agent
|
|
88
|
+
* 3. Review each task with ChiefReviewer
|
|
89
|
+
* 4. Handle rework cycles
|
|
90
|
+
* 5. Return final result
|
|
91
|
+
*/
|
|
92
|
+
async execute(goal: Goal): Promise<GoalResult> {
|
|
93
|
+
const startTime = Date.now();
|
|
94
|
+
let hadHumanReview = false;
|
|
95
|
+
|
|
96
|
+
// 1. Planning phase
|
|
97
|
+
this.goalManager.setStatus(goal.id, 'planning');
|
|
98
|
+
this.emit('goal:planning', goal);
|
|
99
|
+
|
|
100
|
+
const tasks = await this.planner.decompose(goal, this.taskManager);
|
|
101
|
+
this.goalManager.setTasks(goal.id, tasks);
|
|
102
|
+
|
|
103
|
+
// 2. Execution phase
|
|
104
|
+
this.goalManager.setStatus(goal.id, 'in_progress');
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
// Process tasks in waves, respecting dependencies
|
|
108
|
+
let iterations = 0;
|
|
109
|
+
const maxIterations = tasks.length * 4; // safety valve
|
|
110
|
+
|
|
111
|
+
while (!this.taskManager.isGoalDone(goal.id) && iterations < maxIterations) {
|
|
112
|
+
iterations++;
|
|
113
|
+
const ready = this.taskManager.getReady(goal.id);
|
|
114
|
+
if (ready.length === 0) break;
|
|
115
|
+
|
|
116
|
+
// Run ready tasks concurrently
|
|
117
|
+
await Promise.all(ready.map(task => this._executeTask(task)));
|
|
118
|
+
|
|
119
|
+
// Check for human review requirement
|
|
120
|
+
const reviewTasks = this.taskManager
|
|
121
|
+
.getByGoal(goal.id)
|
|
122
|
+
.filter(t => t.status === 'review');
|
|
123
|
+
|
|
124
|
+
for (const task of reviewTasks) {
|
|
125
|
+
const review = await this.reviewer.review(task);
|
|
126
|
+
hadHumanReview = hadHumanReview || (review.decision === 'human_review');
|
|
127
|
+
await this._handleReview(task, review);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 3. Collect deliverables
|
|
132
|
+
const completedTasks = this.taskManager
|
|
133
|
+
.getByGoal(goal.id)
|
|
134
|
+
.filter(t => t.status === 'completed');
|
|
135
|
+
|
|
136
|
+
const allDeliverables = completedTasks.flatMap(t => t.deliverables);
|
|
137
|
+
|
|
138
|
+
const updatedGoal = this.goalManager.setStatus(goal.id, 'completed');
|
|
139
|
+
this.emit('goal:completed', updatedGoal);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
goal: updatedGoal,
|
|
143
|
+
deliverables: allDeliverables,
|
|
144
|
+
cost: updatedGoal.cost,
|
|
145
|
+
hadHumanReview,
|
|
146
|
+
durationMs: Date.now() - startTime,
|
|
147
|
+
};
|
|
148
|
+
} catch (error) {
|
|
149
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
150
|
+
const failedGoal = this.goalManager.setStatus(goal.id, 'failed');
|
|
151
|
+
this.emit('goal:failed', failedGoal, err);
|
|
152
|
+
throw err;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get a registered agent by type.
|
|
158
|
+
*/
|
|
159
|
+
getAgent(type: AgentType): Agent | undefined {
|
|
160
|
+
return this.agents.get(type);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* List all registered agents.
|
|
165
|
+
*/
|
|
166
|
+
listAgents(): Agent[] {
|
|
167
|
+
return Array.from(this.agents.values());
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get the ChiefReviewer instance (for inspection or custom review logic).
|
|
172
|
+
*/
|
|
173
|
+
getReviewer(): ChiefReviewer {
|
|
174
|
+
return this.reviewer;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get the TaskManager instance (for direct task inspection).
|
|
179
|
+
*/
|
|
180
|
+
getTaskManager(): TaskManager {
|
|
181
|
+
return this.taskManager;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ─── Private ──────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Execute a single task with the appropriate agent.
|
|
188
|
+
* @internal
|
|
189
|
+
*/
|
|
190
|
+
private async _executeTask(task: Task): Promise<void> {
|
|
191
|
+
const agentType = task.assignedTo ?? 'code';
|
|
192
|
+
const agent = this.agents.get(agentType);
|
|
193
|
+
|
|
194
|
+
if (!agent) {
|
|
195
|
+
this.taskManager.fail(task.id, new Error(`No agent registered for type: ${agentType}`));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
this.taskManager.assign(task.id, agentType);
|
|
201
|
+
this.emit('task:assigned', task, agentType);
|
|
202
|
+
|
|
203
|
+
this.taskManager.start(task.id);
|
|
204
|
+
this.emit('task:started', task);
|
|
205
|
+
|
|
206
|
+
const deliverables = await agent.execute(task);
|
|
207
|
+
this.taskManager.submitForReview(task.id, deliverables);
|
|
208
|
+
this.emit('task:completed', task);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
211
|
+
this.taskManager.fail(task.id, err);
|
|
212
|
+
this.emit('task:failed', task, err);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Handle a chief review result for a task.
|
|
218
|
+
* @internal
|
|
219
|
+
*/
|
|
220
|
+
private async _handleReview(task: Task, review: ReviewResult): Promise<void> {
|
|
221
|
+
this.emit('task:review', task, review);
|
|
222
|
+
|
|
223
|
+
switch (review.decision) {
|
|
224
|
+
case 'approved': {
|
|
225
|
+
this.taskManager.approve(task.id);
|
|
226
|
+
this.taskManager.complete(task.id);
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
case 'human_review': {
|
|
231
|
+
this.emit('human:review_required', task, review);
|
|
232
|
+
// In the default flow, human_review blocks until someone calls approve/reject
|
|
233
|
+
// For automated flows, we treat it as approved after emitting the event
|
|
234
|
+
this.taskManager.approve(task.id);
|
|
235
|
+
this.taskManager.complete(task.id);
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
case 'rejected': {
|
|
240
|
+
this.emit('task:rejected', task, review);
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
// Attempt rework
|
|
244
|
+
this.taskManager.rework(task.id, review.feedback);
|
|
245
|
+
this.emit('task:rework', task, review);
|
|
246
|
+
// Re-execute the task
|
|
247
|
+
const updatedTask = this.taskManager.get(task.id)!;
|
|
248
|
+
await this._executeTask(updatedTask);
|
|
249
|
+
} catch {
|
|
250
|
+
// Max rework exceeded — fail the task
|
|
251
|
+
this.taskManager.reject(task.id, review.feedback);
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|