@haoyiyin/workflow 0.2.2 → 0.2.3
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/package.json +9 -8
- package/src/agents/contracts.ts +559 -0
- package/src/agents/dispatcher-enhanced.ts +350 -0
- package/src/agents/dispatcher.ts +680 -0
- package/src/agents/index.ts +48 -0
- package/src/agents/resilience.ts +255 -0
- package/src/agents/token-budget.ts +83 -0
- package/src/agents/types.ts +73 -0
- package/src/guard/main-agent.ts +245 -0
- package/src/hooks/builtin/index.ts +8 -0
- package/src/hooks/builtin/on-error.ts +23 -0
- package/src/hooks/builtin/post-execute.ts +40 -0
- package/src/hooks/builtin/post-plan.ts +23 -0
- package/src/hooks/builtin/pre-execute.ts +30 -0
- package/src/hooks/builtin/pre-plan.ts +26 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/loader.ts +98 -0
- package/src/hooks/manager.ts +99 -0
- package/src/hooks/types-enhanced.ts +38 -0
- package/src/hooks/types.ts +35 -0
- package/src/index.ts +127 -0
- package/src/persistence/index.ts +17 -0
- package/src/persistence/plan-md.ts +141 -0
- package/src/persistence/state-md.ts +167 -0
- package/src/persistence/types.ts +89 -0
- package/src/router/classifier.ts +610 -0
- package/src/router/guard.ts +483 -0
- package/src/router/index.ts +22 -0
- package/src/router/router.ts +108 -0
- package/src/router/types.ts +127 -0
- package/src/skills/agents-md/SKILL.md +45 -0
- package/src/skills/agents-md/index.ts +33 -0
- package/src/skills/execute-plan/SKILL.md +60 -0
- package/src/skills/execute-plan/index.ts +970 -0
- package/src/skills/index.ts +13 -0
- package/src/skills/quick-task/SKILL.md +54 -0
- package/src/skills/quick-task/index.ts +346 -0
- package/src/skills/registry.ts +59 -0
- package/src/skills/review-diff/SKILL.md +53 -0
- package/src/skills/review-diff/index.ts +394 -0
- package/src/skills/skill.ts +59 -0
- package/src/skills/systematic-debugging/SKILL.md +56 -0
- package/src/skills/systematic-debugging/index.ts +404 -0
- package/src/skills/tdd/SKILL.md +52 -0
- package/src/skills/tdd/index.ts +409 -0
- package/src/skills/to-plan/SKILL.md +56 -0
- package/src/skills/to-plan/index-enhanced.ts +551 -0
- package/src/skills/to-plan/index.ts +586 -0
- package/src/skills/types.ts +47 -0
- package/src/state/cleanup.ts +118 -0
- package/src/state/index.ts +8 -0
- package/src/state/manager.ts +96 -0
- package/src/state/persistence.ts +77 -0
- package/src/state/types.ts +30 -0
- package/src/state/validator.ts +78 -0
- package/src/types.ts +102 -0
- package/src/utils/compress.ts +347 -0
- package/src/utils/git.ts +82 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/logger.ts +23 -0
- package/src/utils/paths.ts +55 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced subagent dispatcher with resilience and token budget
|
|
3
|
+
*/
|
|
4
|
+
import { spawn } from 'child_process'
|
|
5
|
+
import { TokenBudget, BudgetExceededError } from './token-budget.js'
|
|
6
|
+
import type { SubagentConfig, SubagentContract, SubagentResult, PermissionSet } from './types.js'
|
|
7
|
+
|
|
8
|
+
// Circuit breaker states
|
|
9
|
+
enum CircuitState {
|
|
10
|
+
CLOSED = 'closed',
|
|
11
|
+
OPEN = 'open',
|
|
12
|
+
HALF_OPEN = 'half-open'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Circuit breaker configuration
|
|
16
|
+
interface CircuitBreakerConfig {
|
|
17
|
+
failureThreshold: number
|
|
18
|
+
successThreshold: number
|
|
19
|
+
timeout: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Retry policy configuration
|
|
23
|
+
interface RetryPolicyConfig {
|
|
24
|
+
maxRetries: number
|
|
25
|
+
baseDelay: number
|
|
26
|
+
maxDelay: number
|
|
27
|
+
backoffMultiplier: number
|
|
28
|
+
retryableErrors: string[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Circuit breaker implementation
|
|
32
|
+
class CircuitBreaker {
|
|
33
|
+
private state: CircuitState = CircuitState.CLOSED
|
|
34
|
+
private failureCount: number = 0
|
|
35
|
+
private successCount: number = 0
|
|
36
|
+
private nextAttempt: number = 0
|
|
37
|
+
|
|
38
|
+
constructor(private config: CircuitBreakerConfig) {}
|
|
39
|
+
|
|
40
|
+
async execute<T>(operation: () => Promise<T>, fallback?: () => T | undefined): Promise<T> {
|
|
41
|
+
if (this.state === CircuitState.OPEN) {
|
|
42
|
+
if (Date.now() < this.nextAttempt) {
|
|
43
|
+
if (fallback) {
|
|
44
|
+
console.log('[CircuitBreaker] Using fallback')
|
|
45
|
+
const result = fallback()
|
|
46
|
+
if (result !== undefined) return result as T
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`Circuit OPEN. Retry after ${new Date(this.nextAttempt).toISOString()}`)
|
|
49
|
+
}
|
|
50
|
+
this.state = CircuitState.HALF_OPEN
|
|
51
|
+
console.log('[CircuitBreaker] Entering HALF_OPEN state')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const result = await operation()
|
|
56
|
+
this.onSuccess()
|
|
57
|
+
return result
|
|
58
|
+
} catch (error) {
|
|
59
|
+
this.onFailure()
|
|
60
|
+
throw error
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private onSuccess(): void {
|
|
65
|
+
this.failureCount = 0
|
|
66
|
+
if (this.state === CircuitState.HALF_OPEN) {
|
|
67
|
+
this.successCount++
|
|
68
|
+
if (this.successCount >= this.config.successThreshold) {
|
|
69
|
+
console.log('[CircuitBreaker] CLOSED - service recovered')
|
|
70
|
+
this.state = CircuitState.CLOSED
|
|
71
|
+
this.successCount = 0
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private onFailure(): void {
|
|
77
|
+
this.failureCount++
|
|
78
|
+
if (this.state === CircuitState.HALF_OPEN || this.failureCount >= this.config.failureThreshold) {
|
|
79
|
+
console.log(`[CircuitBreaker] OPEN - failures: ${this.failureCount}`)
|
|
80
|
+
this.state = CircuitState.OPEN
|
|
81
|
+
this.nextAttempt = Date.now() + this.config.timeout
|
|
82
|
+
this.successCount = 0
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getState(): CircuitState {
|
|
87
|
+
return this.state
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Retry policy implementation
|
|
92
|
+
class RetryPolicy {
|
|
93
|
+
constructor(private config: RetryPolicyConfig) {}
|
|
94
|
+
|
|
95
|
+
async execute<T>(operation: () => Promise<T>): Promise<T> {
|
|
96
|
+
let lastError: Error | undefined
|
|
97
|
+
|
|
98
|
+
for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
|
|
99
|
+
try {
|
|
100
|
+
return await operation()
|
|
101
|
+
} catch (error) {
|
|
102
|
+
lastError = error as Error
|
|
103
|
+
if (!this.isRetryable(error)) throw error
|
|
104
|
+
if (attempt < this.config.maxRetries) {
|
|
105
|
+
const delay = this.calculateDelay(attempt)
|
|
106
|
+
console.log(`[RetryPolicy] Attempt ${attempt + 1} failed, retrying in ${Math.round(delay)}ms`)
|
|
107
|
+
await this.sleep(delay)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
throw new Error(`Failed after ${this.config.maxRetries} retries: ${lastError?.message}`)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private isRetryable(error: unknown): boolean {
|
|
116
|
+
if (!(error instanceof Error)) return false
|
|
117
|
+
const errorName = error.constructor.name
|
|
118
|
+
return this.config.retryableErrors.some(re =>
|
|
119
|
+
errorName.includes(re) || error.message.includes(re)
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private calculateDelay(attempt: number): number {
|
|
124
|
+
const exponentialDelay = this.config.baseDelay * Math.pow(this.config.backoffMultiplier, attempt)
|
|
125
|
+
const jitter = exponentialDelay * 0.25 * (Math.random() * 2 - 1)
|
|
126
|
+
return Math.min(exponentialDelay + jitter, this.config.maxDelay)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private sleep(ms: number): Promise<void> {
|
|
130
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Resilient dispatcher combining circuit breaker and retry
|
|
135
|
+
export class ResilientDispatcher {
|
|
136
|
+
private circuitBreaker: CircuitBreaker
|
|
137
|
+
private retryPolicy: RetryPolicy
|
|
138
|
+
private tokenBudget: TokenBudget
|
|
139
|
+
|
|
140
|
+
constructor(
|
|
141
|
+
circuitConfig?: Partial<CircuitBreakerConfig>,
|
|
142
|
+
retryConfig?: Partial<RetryPolicyConfig>
|
|
143
|
+
) {
|
|
144
|
+
this.circuitBreaker = new CircuitBreaker({
|
|
145
|
+
failureThreshold: 5,
|
|
146
|
+
successThreshold: 3,
|
|
147
|
+
timeout: 60000,
|
|
148
|
+
...circuitConfig
|
|
149
|
+
})
|
|
150
|
+
this.retryPolicy = new RetryPolicy({
|
|
151
|
+
maxRetries: 3,
|
|
152
|
+
baseDelay: 1000,
|
|
153
|
+
maxDelay: 30000,
|
|
154
|
+
backoffMultiplier: 2,
|
|
155
|
+
retryableErrors: ['RateLimitError', 'TimeoutError', 'NetworkError', 'ECONNREFUSED'],
|
|
156
|
+
...retryConfig
|
|
157
|
+
})
|
|
158
|
+
this.tokenBudget = new TokenBudget()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Dispatch a subagent with resilience
|
|
163
|
+
*/
|
|
164
|
+
async dispatch<T>(
|
|
165
|
+
config: SubagentConfig,
|
|
166
|
+
contract: SubagentContract,
|
|
167
|
+
options?: {
|
|
168
|
+
fallback?: () => T
|
|
169
|
+
onRetry?: (attempt: number, error: Error) => void
|
|
170
|
+
}
|
|
171
|
+
): Promise<T> {
|
|
172
|
+
// Check token budget
|
|
173
|
+
const estimatedTokens = TokenBudget.estimateTokens(contract.prompt)
|
|
174
|
+
if (!this.tokenBudget.canAfford(estimatedTokens)) {
|
|
175
|
+
throw new BudgetExceededError()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return this.circuitBreaker.execute(
|
|
179
|
+
async () => {
|
|
180
|
+
return await this.retryPolicy.execute(async () => {
|
|
181
|
+
const result = await this.spawnSubagent(config, contract)
|
|
182
|
+
this.tokenBudget.recordUsage(result.tokensUsed || estimatedTokens)
|
|
183
|
+
return this.parseResult<T>(result, contract)
|
|
184
|
+
})
|
|
185
|
+
},
|
|
186
|
+
options?.fallback
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Dispatch multiple subagents in parallel with resilience
|
|
192
|
+
*/
|
|
193
|
+
async dispatchParallel<T>(
|
|
194
|
+
configs: SubagentConfig[],
|
|
195
|
+
contract: SubagentContract
|
|
196
|
+
): Promise<(T | { success: false; error: string })[]> {
|
|
197
|
+
const promises = configs.map(async config => {
|
|
198
|
+
try {
|
|
199
|
+
return await this.dispatch<T>(config, contract)
|
|
200
|
+
} catch (error) {
|
|
201
|
+
console.error(`[Dispatcher] Subagent failed:`, error)
|
|
202
|
+
return { success: false, error: (error as Error).message } as T
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// Use allSettled to ensure one failure doesn't affect others
|
|
207
|
+
const results = await Promise.allSettled(promises)
|
|
208
|
+
|
|
209
|
+
return results.map(result => {
|
|
210
|
+
if (result.status === 'fulfilled') {
|
|
211
|
+
return result.value
|
|
212
|
+
} else {
|
|
213
|
+
return { success: false, error: result.reason?.message || 'Unknown error' } as T
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Build fresh prompt with minimal context
|
|
220
|
+
*/
|
|
221
|
+
private buildFreshPrompt(contract: SubagentContract): string {
|
|
222
|
+
// Only include: task description + permissions + output format
|
|
223
|
+
// Exclude: previous subagent outputs, main agent thinking
|
|
224
|
+
return `${contract.prompt}\n\nPermissions: ${JSON.stringify(contract.permissions)}`
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Spawn subagent process
|
|
229
|
+
*/
|
|
230
|
+
private async spawnSubagent(
|
|
231
|
+
config: SubagentConfig,
|
|
232
|
+
contract: SubagentContract
|
|
233
|
+
): Promise<SubagentResult> {
|
|
234
|
+
const startTime = Date.now()
|
|
235
|
+
const freshPrompt = this.buildFreshPrompt(contract)
|
|
236
|
+
|
|
237
|
+
return new Promise((resolve, reject) => {
|
|
238
|
+
// Spawn subagent process
|
|
239
|
+
const subagent = spawn('node', ['-e', `
|
|
240
|
+
// Subagent runner - receives prompt via stdin, outputs result via stdout
|
|
241
|
+
const readline = require('readline');
|
|
242
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
243
|
+
|
|
244
|
+
let prompt = '';
|
|
245
|
+
rl.on('line', (line) => {
|
|
246
|
+
prompt += line + '\n';
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
rl.on('close', async () => {
|
|
250
|
+
// Simulate subagent execution (replace with actual AI call)
|
|
251
|
+
const result = {
|
|
252
|
+
id: '${this.generateId()}',
|
|
253
|
+
role: '${config.role}',
|
|
254
|
+
status: 'success',
|
|
255
|
+
output: JSON.stringify({ result: 'Task completed', prompt }),
|
|
256
|
+
artifacts: [],
|
|
257
|
+
tokensUsed: ${TokenBudget.estimateTokens(freshPrompt)},
|
|
258
|
+
duration: 0,
|
|
259
|
+
errors: []
|
|
260
|
+
};
|
|
261
|
+
console.log(JSON.stringify(result));
|
|
262
|
+
});
|
|
263
|
+
`], {
|
|
264
|
+
env: { ...process.env, CLAUDE_SUBAGENT: '1' },
|
|
265
|
+
timeout: config.timeout || 120000
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
let output = ''
|
|
269
|
+
let stderr = ''
|
|
270
|
+
|
|
271
|
+
subagent.stdout.on('data', (data: Buffer) => {
|
|
272
|
+
output += data.toString()
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
subagent.stderr.on('data', (data: Buffer) => {
|
|
276
|
+
stderr += data.toString()
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
subagent.on('error', (error) => {
|
|
280
|
+
reject(error)
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
subagent.on('close', (code) => {
|
|
284
|
+
const duration = Date.now() - startTime
|
|
285
|
+
|
|
286
|
+
if (code === 0) {
|
|
287
|
+
try {
|
|
288
|
+
const result = JSON.parse(output)
|
|
289
|
+
resolve({
|
|
290
|
+
...result,
|
|
291
|
+
duration
|
|
292
|
+
} as SubagentResult)
|
|
293
|
+
} catch {
|
|
294
|
+
resolve({
|
|
295
|
+
id: this.generateId(),
|
|
296
|
+
role: config.role,
|
|
297
|
+
status: 'success',
|
|
298
|
+
output,
|
|
299
|
+
artifacts: [],
|
|
300
|
+
tokensUsed: TokenBudget.estimateTokens(freshPrompt),
|
|
301
|
+
duration,
|
|
302
|
+
errors: []
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
reject(new Error(`Subagent exited with code ${code}: ${stderr}`))
|
|
307
|
+
}
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
// Send prompt
|
|
311
|
+
subagent.stdin.write(freshPrompt)
|
|
312
|
+
subagent.stdin.end()
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Parse subagent result
|
|
318
|
+
*/
|
|
319
|
+
private parseResult<T>(result: SubagentResult, contract: SubagentContract): T {
|
|
320
|
+
try {
|
|
321
|
+
const parsed = JSON.parse(result.output)
|
|
322
|
+
return parsed as T
|
|
323
|
+
} catch {
|
|
324
|
+
return result.output as unknown as T
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Generate unique ID
|
|
330
|
+
*/
|
|
331
|
+
private generateId(): string {
|
|
332
|
+
return `sub_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Get health status
|
|
337
|
+
*/
|
|
338
|
+
getHealth(): {
|
|
339
|
+
circuitState: CircuitState
|
|
340
|
+
tokenUsage: { used: number; limit: number; percentage: number }
|
|
341
|
+
} {
|
|
342
|
+
return {
|
|
343
|
+
circuitState: this.circuitBreaker.getState(),
|
|
344
|
+
tokenUsage: this.tokenBudget.getUsage()
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export { CircuitState }
|
|
350
|
+
export type { CircuitBreakerConfig, RetryPolicyConfig }
|