@haoyiyin/workflow 0.2.0 → 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.
Files changed (62) hide show
  1. package/package.json +15 -10
  2. package/scripts/postinstall.js +2 -2
  3. package/src/agents/contracts.ts +559 -0
  4. package/src/agents/dispatcher-enhanced.ts +350 -0
  5. package/src/agents/dispatcher.ts +680 -0
  6. package/src/agents/index.ts +48 -0
  7. package/src/agents/resilience.ts +255 -0
  8. package/src/agents/token-budget.ts +83 -0
  9. package/src/agents/types.ts +73 -0
  10. package/src/guard/main-agent.ts +245 -0
  11. package/src/hooks/builtin/index.ts +8 -0
  12. package/src/hooks/builtin/on-error.ts +23 -0
  13. package/src/hooks/builtin/post-execute.ts +40 -0
  14. package/src/hooks/builtin/post-plan.ts +23 -0
  15. package/src/hooks/builtin/pre-execute.ts +30 -0
  16. package/src/hooks/builtin/pre-plan.ts +26 -0
  17. package/src/hooks/index.ts +7 -0
  18. package/src/hooks/loader.ts +98 -0
  19. package/src/hooks/manager.ts +99 -0
  20. package/src/hooks/types-enhanced.ts +38 -0
  21. package/src/hooks/types.ts +35 -0
  22. package/src/index.ts +127 -0
  23. package/src/persistence/index.ts +17 -0
  24. package/src/persistence/plan-md.ts +141 -0
  25. package/src/persistence/state-md.ts +167 -0
  26. package/src/persistence/types.ts +89 -0
  27. package/src/router/classifier.ts +610 -0
  28. package/src/router/guard.ts +483 -0
  29. package/src/router/index.ts +22 -0
  30. package/src/router/router.ts +108 -0
  31. package/src/router/types.ts +127 -0
  32. package/src/skills/agents-md/SKILL.md +45 -0
  33. package/src/skills/agents-md/index.ts +33 -0
  34. package/src/skills/execute-plan/SKILL.md +60 -0
  35. package/src/skills/execute-plan/index.ts +970 -0
  36. package/src/skills/index.ts +13 -0
  37. package/src/skills/quick-task/SKILL.md +54 -0
  38. package/src/skills/quick-task/index.ts +346 -0
  39. package/src/skills/registry.ts +59 -0
  40. package/src/skills/review-diff/SKILL.md +53 -0
  41. package/src/skills/review-diff/index.ts +394 -0
  42. package/src/skills/skill.ts +59 -0
  43. package/src/skills/systematic-debugging/SKILL.md +56 -0
  44. package/src/skills/systematic-debugging/index.ts +404 -0
  45. package/src/skills/tdd/SKILL.md +52 -0
  46. package/src/skills/tdd/index.ts +409 -0
  47. package/src/skills/to-plan/SKILL.md +56 -0
  48. package/src/skills/to-plan/index-enhanced.ts +551 -0
  49. package/src/skills/to-plan/index.ts +586 -0
  50. package/src/skills/types.ts +47 -0
  51. package/src/state/cleanup.ts +118 -0
  52. package/src/state/index.ts +8 -0
  53. package/src/state/manager.ts +96 -0
  54. package/src/state/persistence.ts +77 -0
  55. package/src/state/types.ts +30 -0
  56. package/src/state/validator.ts +78 -0
  57. package/src/types.ts +102 -0
  58. package/src/utils/compress.ts +347 -0
  59. package/src/utils/git.ts +82 -0
  60. package/src/utils/index.ts +6 -0
  61. package/src/utils/logger.ts +23 -0
  62. 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 }