@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.
- package/package.json +15 -10
- package/scripts/postinstall.js +2 -2
- 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,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agents module index
|
|
3
|
+
*
|
|
4
|
+
* Public API surface for the subagent dispatch engine.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Types
|
|
8
|
+
export type {
|
|
9
|
+
SubagentConfig,
|
|
10
|
+
SubagentContract,
|
|
11
|
+
SubagentResult,
|
|
12
|
+
SubagentArtifact,
|
|
13
|
+
SubagentRole,
|
|
14
|
+
PermissionSet,
|
|
15
|
+
MainAgentGuard,
|
|
16
|
+
} from './types.js';
|
|
17
|
+
|
|
18
|
+
// Dispatcher
|
|
19
|
+
export {
|
|
20
|
+
SubagentDispatcher,
|
|
21
|
+
createDispatcher,
|
|
22
|
+
createProcessExecutor,
|
|
23
|
+
} from './dispatcher.js';
|
|
24
|
+
|
|
25
|
+
export type {
|
|
26
|
+
SubagentExecutor,
|
|
27
|
+
ExecutorOptions,
|
|
28
|
+
ExecutorResult,
|
|
29
|
+
ProcessExecutorConfig,
|
|
30
|
+
DispatcherOptions,
|
|
31
|
+
} from './dispatcher.js';
|
|
32
|
+
|
|
33
|
+
// Contracts
|
|
34
|
+
export {
|
|
35
|
+
explorerContract,
|
|
36
|
+
implementerContract,
|
|
37
|
+
reviewerContract,
|
|
38
|
+
debuggerContract,
|
|
39
|
+
verifierContract,
|
|
40
|
+
} from './contracts.js';
|
|
41
|
+
|
|
42
|
+
export type {
|
|
43
|
+
ExplorerParams,
|
|
44
|
+
ImplementerParams,
|
|
45
|
+
ReviewerParams,
|
|
46
|
+
DebuggerParams,
|
|
47
|
+
VerifierParams,
|
|
48
|
+
} from './contracts.js';
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Circuit breaker and retry mechanisms for resilient subagent dispatch
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Circuit breaker states
|
|
6
|
+
enum CircuitState {
|
|
7
|
+
CLOSED = 'closed', // Normal operation
|
|
8
|
+
OPEN = 'open', // Circuit broken, reject requests
|
|
9
|
+
HALF_OPEN = 'half-open' // Testing if service recovered
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class CircuitBreakerError extends Error {
|
|
13
|
+
constructor(message: string) {
|
|
14
|
+
super(message)
|
|
15
|
+
this.name = 'CircuitBreakerError'
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class RetryExhaustedError extends Error {
|
|
20
|
+
constructor(message: string) {
|
|
21
|
+
super(message)
|
|
22
|
+
this.name = 'RetryExhaustedError'
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CircuitBreakerConfig {
|
|
27
|
+
failureThreshold: number // Failures before opening circuit
|
|
28
|
+
successThreshold: number // Successes to close from half-open
|
|
29
|
+
timeout: number // How long to stay open (ms)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CircuitBreakerMetrics {
|
|
33
|
+
state: CircuitState
|
|
34
|
+
failureCount: number
|
|
35
|
+
successCount: number
|
|
36
|
+
timeUntilRetry: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class CircuitBreaker {
|
|
40
|
+
private state: CircuitState = CircuitState.CLOSED
|
|
41
|
+
private failureCount: number = 0
|
|
42
|
+
private successCount: number = 0
|
|
43
|
+
private nextAttempt: number = 0
|
|
44
|
+
private config: CircuitBreakerConfig
|
|
45
|
+
|
|
46
|
+
constructor(config: Partial<CircuitBreakerConfig> = {}) {
|
|
47
|
+
this.config = {
|
|
48
|
+
failureThreshold: 5,
|
|
49
|
+
successThreshold: 3,
|
|
50
|
+
timeout: 60000,
|
|
51
|
+
...config
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Execute operation with circuit breaker protection
|
|
57
|
+
*/
|
|
58
|
+
async execute<T>(
|
|
59
|
+
operation: () => Promise<T>,
|
|
60
|
+
fallback?: () => T
|
|
61
|
+
): Promise<T> {
|
|
62
|
+
// Check circuit state
|
|
63
|
+
if (this.state === CircuitState.OPEN) {
|
|
64
|
+
if (Date.now() < this.nextAttempt) {
|
|
65
|
+
// Still in timeout, use fallback or throw
|
|
66
|
+
if (fallback) {
|
|
67
|
+
console.log('[CircuitBreaker] Using fallback')
|
|
68
|
+
return fallback()
|
|
69
|
+
}
|
|
70
|
+
throw new CircuitBreakerError(
|
|
71
|
+
`Circuit open. Retry after ${new Date(this.nextAttempt).toISOString()}`
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
// Timeout elapsed, try half-open
|
|
75
|
+
this.state = CircuitState.HALF_OPEN
|
|
76
|
+
console.log('[CircuitBreaker] Entering HALF_OPEN state')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const result = await operation()
|
|
81
|
+
this.onSuccess()
|
|
82
|
+
return result
|
|
83
|
+
} catch (error) {
|
|
84
|
+
this.onFailure()
|
|
85
|
+
throw error
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private onSuccess(): void {
|
|
90
|
+
this.failureCount = 0
|
|
91
|
+
|
|
92
|
+
if (this.state === CircuitState.HALF_OPEN) {
|
|
93
|
+
this.successCount++
|
|
94
|
+
if (this.successCount >= this.config.successThreshold) {
|
|
95
|
+
console.log('[CircuitBreaker] CLOSED - service recovered')
|
|
96
|
+
this.state = CircuitState.CLOSED
|
|
97
|
+
this.successCount = 0
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private onFailure(): void {
|
|
103
|
+
this.failureCount++
|
|
104
|
+
|
|
105
|
+
if (this.state === CircuitState.HALF_OPEN) {
|
|
106
|
+
// Failed in half-open, go back to open
|
|
107
|
+
this.tripCircuit()
|
|
108
|
+
} else if (this.failureCount >= this.config.failureThreshold) {
|
|
109
|
+
// Reached threshold, open circuit
|
|
110
|
+
this.tripCircuit()
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private tripCircuit(): void {
|
|
115
|
+
console.log(`[CircuitBreaker] OPEN - failures: ${this.failureCount}`)
|
|
116
|
+
this.state = CircuitState.OPEN
|
|
117
|
+
this.nextAttempt = Date.now() + this.config.timeout
|
|
118
|
+
this.successCount = 0
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
getState(): CircuitState {
|
|
122
|
+
return this.state
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
getMetrics(): CircuitBreakerMetrics {
|
|
126
|
+
return {
|
|
127
|
+
state: this.state,
|
|
128
|
+
failureCount: this.failureCount,
|
|
129
|
+
successCount: this.successCount,
|
|
130
|
+
timeUntilRetry: Math.max(0, this.nextAttempt - Date.now())
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface RetryPolicyConfig {
|
|
136
|
+
maxRetries: number
|
|
137
|
+
baseDelay: number
|
|
138
|
+
maxDelay: number
|
|
139
|
+
backoffMultiplier: number
|
|
140
|
+
retryableErrors: string[]
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export class RetryPolicy {
|
|
144
|
+
private config: RetryPolicyConfig
|
|
145
|
+
|
|
146
|
+
constructor(config: Partial<RetryPolicyConfig> = {}) {
|
|
147
|
+
this.config = {
|
|
148
|
+
maxRetries: 3,
|
|
149
|
+
baseDelay: 1000,
|
|
150
|
+
maxDelay: 30000,
|
|
151
|
+
backoffMultiplier: 2,
|
|
152
|
+
retryableErrors: ['RateLimitError', 'TimeoutError', 'NetworkError', 'BudgetExceededError'],
|
|
153
|
+
...config
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Execute operation with retry logic
|
|
159
|
+
*/
|
|
160
|
+
async execute<T>(operation: () => Promise<T>): Promise<T> {
|
|
161
|
+
let lastError: Error | undefined
|
|
162
|
+
|
|
163
|
+
for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
|
|
164
|
+
try {
|
|
165
|
+
return await operation()
|
|
166
|
+
} catch (error) {
|
|
167
|
+
lastError = error as Error
|
|
168
|
+
|
|
169
|
+
if (!this.isRetryable(error)) {
|
|
170
|
+
throw error
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (attempt < this.config.maxRetries) {
|
|
174
|
+
const delay = this.calculateDelay(attempt)
|
|
175
|
+
console.log(`[RetryPolicy] Attempt ${attempt + 1} failed, retrying in ${Math.round(delay)}ms`)
|
|
176
|
+
await this.sleep(delay)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
throw new RetryExhaustedError(
|
|
182
|
+
`Failed after ${this.config.maxRetries} retries: ${lastError?.message}`
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private isRetryable(error: unknown): boolean {
|
|
187
|
+
if (!(error instanceof Error)) return false
|
|
188
|
+
|
|
189
|
+
const errorName = error.constructor.name
|
|
190
|
+
return this.config.retryableErrors.some(re =>
|
|
191
|
+
errorName.includes(re) || error.message.includes(re)
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private calculateDelay(attempt: number): number {
|
|
196
|
+
// Exponential backoff with jitter
|
|
197
|
+
const exponentialDelay = this.config.baseDelay *
|
|
198
|
+
Math.pow(this.config.backoffMultiplier, attempt)
|
|
199
|
+
const jitter = exponentialDelay * 0.25 * (Math.random() * 2 - 1)
|
|
200
|
+
return Math.min(exponentialDelay + jitter, this.config.maxDelay)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private sleep(ms: number): Promise<void> {
|
|
204
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface ResilientDispatchOptions<T> {
|
|
209
|
+
fallback?: () => T
|
|
210
|
+
onRetry?: (attempt: number, error: Error) => void
|
|
211
|
+
onCircuitOpen?: () => void
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export class ResilientDispatcher {
|
|
215
|
+
private circuitBreaker: CircuitBreaker
|
|
216
|
+
private retryPolicy: RetryPolicy
|
|
217
|
+
|
|
218
|
+
constructor(config?: {
|
|
219
|
+
circuitBreaker?: Partial<CircuitBreakerConfig>
|
|
220
|
+
retryPolicy?: Partial<RetryPolicyConfig>
|
|
221
|
+
}) {
|
|
222
|
+
this.circuitBreaker = new CircuitBreaker(config?.circuitBreaker)
|
|
223
|
+
this.retryPolicy = new RetryPolicy(config?.retryPolicy)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Dispatch with both circuit breaker and retry
|
|
228
|
+
*/
|
|
229
|
+
async dispatch<T>(
|
|
230
|
+
operation: () => Promise<T>,
|
|
231
|
+
options: ResilientDispatchOptions<T> = {}
|
|
232
|
+
): Promise<T> {
|
|
233
|
+
return this.circuitBreaker.execute(
|
|
234
|
+
async () => {
|
|
235
|
+
return await this.retryPolicy.execute(operation)
|
|
236
|
+
},
|
|
237
|
+
options.fallback
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Get health metrics
|
|
243
|
+
*/
|
|
244
|
+
getHealth(): {
|
|
245
|
+
circuitState: CircuitState
|
|
246
|
+
metrics: CircuitBreakerMetrics
|
|
247
|
+
} {
|
|
248
|
+
return {
|
|
249
|
+
circuitState: this.circuitBreaker.getState(),
|
|
250
|
+
metrics: this.circuitBreaker.getMetrics()
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export { CircuitState }
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token budget management for context window management
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface TokenUsage {
|
|
6
|
+
used: number
|
|
7
|
+
limit: number
|
|
8
|
+
percentage: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TokenBudgetConfig {
|
|
12
|
+
limit: number
|
|
13
|
+
warningThreshold: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class TokenBudget {
|
|
17
|
+
private used: number = 0
|
|
18
|
+
private readonly limit: number
|
|
19
|
+
private readonly warningThreshold: number
|
|
20
|
+
|
|
21
|
+
constructor(config: TokenBudgetConfig = { limit: 1_000_000, warningThreshold: 0.8 }) {
|
|
22
|
+
this.limit = config.limit
|
|
23
|
+
this.warningThreshold = config.warningThreshold
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if we can afford the estimated tokens
|
|
28
|
+
*/
|
|
29
|
+
canAfford(estimatedTokens: number): boolean {
|
|
30
|
+
return this.used + estimatedTokens <= this.limit
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Record token usage
|
|
35
|
+
*/
|
|
36
|
+
recordUsage(tokens: number): void {
|
|
37
|
+
this.used += tokens
|
|
38
|
+
|
|
39
|
+
if (this.used > this.limit * this.warningThreshold) {
|
|
40
|
+
console.warn(`[TokenBudget] Warning: ${this.used}/${this.limit} tokens used (${Math.round(this.getUsage().percentage)}%)`)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get remaining tokens
|
|
46
|
+
*/
|
|
47
|
+
getRemaining(): number {
|
|
48
|
+
return Math.max(0, this.limit - this.used)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get current usage stats
|
|
53
|
+
*/
|
|
54
|
+
getUsage(): TokenUsage {
|
|
55
|
+
return {
|
|
56
|
+
used: this.used,
|
|
57
|
+
limit: this.limit,
|
|
58
|
+
percentage: (this.used / this.limit) * 100
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Estimate tokens for a text string (rough approximation)
|
|
64
|
+
* ~4 chars per token for English text
|
|
65
|
+
*/
|
|
66
|
+
static estimateTokens(text: string): number {
|
|
67
|
+
return Math.ceil(text.length / 4)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Reset budget (for testing or new milestone)
|
|
72
|
+
*/
|
|
73
|
+
reset(): void {
|
|
74
|
+
this.used = 0
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class BudgetExceededError extends Error {
|
|
79
|
+
constructor(message: string = 'Token budget exceeded') {
|
|
80
|
+
super(message)
|
|
81
|
+
this.name = 'BudgetExceededError'
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent system types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type SubagentRole =
|
|
6
|
+
| 'explorer' // Read-only exploration
|
|
7
|
+
| 'implementer' // Code implementation
|
|
8
|
+
| 'reviewer' // Code review
|
|
9
|
+
| 'planner' // Plan creation
|
|
10
|
+
| 'debugger' // Root cause analysis
|
|
11
|
+
| 'verifier' // Goal verification
|
|
12
|
+
| 'researcher' // Internet research and information gathering
|
|
13
|
+
| 'general' // Catch-all
|
|
14
|
+
|
|
15
|
+
export interface SubagentConfig {
|
|
16
|
+
role: SubagentRole
|
|
17
|
+
model?: string
|
|
18
|
+
isolation?: 'worktree'
|
|
19
|
+
tokenBudget?: number
|
|
20
|
+
timeout?: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SubagentContract {
|
|
24
|
+
/** What this subagent is allowed to do */
|
|
25
|
+
permissions: PermissionSet
|
|
26
|
+
/** Exact prompt to send */
|
|
27
|
+
prompt: string
|
|
28
|
+
/** Required output format */
|
|
29
|
+
outputSchema?: Record<string, unknown>
|
|
30
|
+
/** Files this subagent owns */
|
|
31
|
+
owns: string[]
|
|
32
|
+
/** Files this subagent may read */
|
|
33
|
+
reads: string[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface PermissionSet {
|
|
37
|
+
readFiles: boolean
|
|
38
|
+
searchCode: boolean
|
|
39
|
+
runCommands: boolean
|
|
40
|
+
writeFiles: boolean
|
|
41
|
+
gitOperations: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SubagentResult {
|
|
45
|
+
id: string
|
|
46
|
+
role: SubagentRole
|
|
47
|
+
status: 'success' | 'failure' | 'timeout'
|
|
48
|
+
output: string
|
|
49
|
+
artifacts: SubagentArtifact[]
|
|
50
|
+
tokensUsed: number
|
|
51
|
+
duration: number
|
|
52
|
+
errors: string[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SubagentArtifact {
|
|
56
|
+
type: 'file' | 'diff' | 'test-result' | 'report'
|
|
57
|
+
path?: string
|
|
58
|
+
content: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Controls what the main agent is allowed to do */
|
|
62
|
+
export interface MainAgentGuard {
|
|
63
|
+
/** Is the main agent currently allowed to read source files? */
|
|
64
|
+
canReadSource: boolean
|
|
65
|
+
/** Is the main agent currently allowed to search the codebase? */
|
|
66
|
+
canSearchCodebase: boolean
|
|
67
|
+
/** Is the main agent currently allowed to write files? */
|
|
68
|
+
canWriteFiles: boolean
|
|
69
|
+
/** Is the main agent in "embargo" mode (must delegate everything)? */
|
|
70
|
+
embargoActive: boolean
|
|
71
|
+
/** Active subagent IDs that must complete before guard lifts */
|
|
72
|
+
pendingSubagents: string[]
|
|
73
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main Agent Guard - Enforces that Main Agent NEVER does actual work
|
|
3
|
+
*
|
|
4
|
+
* This is the CORE architectural constraint:
|
|
5
|
+
* - Main Agent = Orchestrator only
|
|
6
|
+
* - All real work = Subagents only
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Logger } from '../types.js'
|
|
10
|
+
|
|
11
|
+
export interface GuardConfig {
|
|
12
|
+
/** Token budget before forced checkpoint */
|
|
13
|
+
tokenBudget: number
|
|
14
|
+
/** Whether embargo is active */
|
|
15
|
+
embargoActive: boolean
|
|
16
|
+
/** List of prohibited operations */
|
|
17
|
+
prohibited: ProhibitedOperations
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ProhibitedOperations {
|
|
21
|
+
readSourceFiles: boolean
|
|
22
|
+
searchCodebase: boolean
|
|
23
|
+
writeFiles: boolean
|
|
24
|
+
runBuildCommands: boolean
|
|
25
|
+
runTests: boolean
|
|
26
|
+
editLargeFiles: boolean
|
|
27
|
+
longRunningTasks: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface OperationRequest {
|
|
31
|
+
type: 'read' | 'search' | 'write' | 'run' | 'edit'
|
|
32
|
+
target?: string
|
|
33
|
+
estimatedLines?: number
|
|
34
|
+
estimatedDuration?: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface GuardDecision {
|
|
38
|
+
allowed: boolean
|
|
39
|
+
/** If not allowed, which subagent to dispatch instead */
|
|
40
|
+
dispatchInstead?: string
|
|
41
|
+
/** Why this operation was blocked */
|
|
42
|
+
reason: string
|
|
43
|
+
/** Suggested action */
|
|
44
|
+
suggestion: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class MainAgentGuard {
|
|
48
|
+
private config: GuardConfig
|
|
49
|
+
private logger: Logger
|
|
50
|
+
private tokensUsed: number = 0
|
|
51
|
+
|
|
52
|
+
constructor(config: GuardConfig, logger: Logger) {
|
|
53
|
+
this.config = config
|
|
54
|
+
this.logger = logger
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if an operation is allowed for Main Agent
|
|
59
|
+
* ALWAYS returns false for actual work - forces subagent dispatch
|
|
60
|
+
*/
|
|
61
|
+
checkOperation(request: OperationRequest): GuardDecision {
|
|
62
|
+
// ALWAYS block file reads (except config files)
|
|
63
|
+
if (request.type === 'read' && this.isSourceFile(request.target)) {
|
|
64
|
+
return {
|
|
65
|
+
allowed: false,
|
|
66
|
+
dispatchInstead: 'explorer',
|
|
67
|
+
reason: 'Main Agent is prohibited from reading source files',
|
|
68
|
+
suggestion: 'Dispatch explorer subagent to read and summarize',
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ALWAYS block code search
|
|
73
|
+
if (request.type === 'search') {
|
|
74
|
+
return {
|
|
75
|
+
allowed: false,
|
|
76
|
+
dispatchInstead: 'explorer',
|
|
77
|
+
reason: 'Main Agent is prohibited from searching codebase',
|
|
78
|
+
suggestion: 'Dispatch explorer subagent with search query',
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ALWAYS block file writes
|
|
83
|
+
if (request.type === 'write') {
|
|
84
|
+
return {
|
|
85
|
+
allowed: false,
|
|
86
|
+
dispatchInstead: 'implementer',
|
|
87
|
+
reason: 'Main Agent is prohibited from writing files',
|
|
88
|
+
suggestion: 'Dispatch implementer subagent to write files',
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ALWAYS block edits (except tiny ones)
|
|
93
|
+
if (request.type === 'edit') {
|
|
94
|
+
if ((request.estimatedLines || 0) > 10) {
|
|
95
|
+
return {
|
|
96
|
+
allowed: false,
|
|
97
|
+
dispatchInstead: 'implementer',
|
|
98
|
+
reason: 'Main Agent is prohibited from editing large files',
|
|
99
|
+
suggestion: 'Dispatch implementer subagent for edits >10 lines',
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ALWAYS block long-running commands
|
|
105
|
+
if (request.type === 'run') {
|
|
106
|
+
if ((request.estimatedDuration || 0) > 30) {
|
|
107
|
+
return {
|
|
108
|
+
allowed: false,
|
|
109
|
+
dispatchInstead: 'runner',
|
|
110
|
+
reason: 'Main Agent is prohibited from long-running commands',
|
|
111
|
+
suggestion: 'Dispatch runner subagent for commands >30s',
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Block build/test commands
|
|
116
|
+
if (this.isBuildCommand(request.target)) {
|
|
117
|
+
return {
|
|
118
|
+
allowed: false,
|
|
119
|
+
dispatchInstead: 'runner',
|
|
120
|
+
reason: 'Main Agent is prohibited from build/test commands',
|
|
121
|
+
suggestion: 'Dispatch runner subagent for build/test',
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check token budget
|
|
127
|
+
if (this.tokensUsed > this.config.tokenBudget) {
|
|
128
|
+
return {
|
|
129
|
+
allowed: false,
|
|
130
|
+
dispatchInstead: 'checkpoint',
|
|
131
|
+
reason: `Token budget exceeded: ${this.tokensUsed}/${this.config.tokenBudget}`,
|
|
132
|
+
suggestion: 'Force checkpoint: summarize and ask user to continue',
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check embargo
|
|
137
|
+
if (this.config.embargoActive) {
|
|
138
|
+
return {
|
|
139
|
+
allowed: false,
|
|
140
|
+
dispatchInstead: 'coordinator',
|
|
141
|
+
reason: 'Embargo active: all work must be delegated to subagents',
|
|
142
|
+
suggestion: 'Wait for subagents to complete or dispatch coordinator',
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Only allow meta-operations (orchestration)
|
|
147
|
+
return {
|
|
148
|
+
allowed: true,
|
|
149
|
+
reason: 'Operation permitted for orchestration',
|
|
150
|
+
suggestion: 'Proceed with orchestration',
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Assert that Main Agent is NOT doing work - throws if violated
|
|
156
|
+
*/
|
|
157
|
+
assertNotDoingWork(operation: string): void {
|
|
158
|
+
const decision = this.checkOperation({
|
|
159
|
+
type: this.inferOperationType(operation),
|
|
160
|
+
target: operation,
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
if (!decision.allowed) {
|
|
164
|
+
const error = new Error(
|
|
165
|
+
`🚫 MAIN AGENT GUARD VIOLATION\n` +
|
|
166
|
+
`Operation: ${operation}\n` +
|
|
167
|
+
`Reason: ${decision.reason}\n` +
|
|
168
|
+
`Action: ${decision.suggestion}`
|
|
169
|
+
)
|
|
170
|
+
this.logger.error(error.message)
|
|
171
|
+
throw error
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Track token usage
|
|
177
|
+
*/
|
|
178
|
+
trackTokens(tokens: number): void {
|
|
179
|
+
this.tokensUsed += tokens
|
|
180
|
+
if (this.tokensUsed > this.config.tokenBudget * 0.8) {
|
|
181
|
+
this.logger.warn(
|
|
182
|
+
`⚠️ Token budget at ${Math.round((this.tokensUsed / this.config.tokenBudget) * 100)}%`
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Activate embargo mode (post subagent dispatch)
|
|
189
|
+
*/
|
|
190
|
+
activateEmbargo(): void {
|
|
191
|
+
this.config.embargoActive = true
|
|
192
|
+
this.logger.info('🔒 Embargo activated: Main Agent hands off to subagents')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Deactivate embargo (all subagents complete)
|
|
197
|
+
*/
|
|
198
|
+
deactivateEmbargo(): void {
|
|
199
|
+
this.config.embargoActive = false
|
|
200
|
+
this.logger.info('🔓 Embargo lifted: Main Agent can orchestrate')
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private isSourceFile(path?: string): boolean {
|
|
204
|
+
if (!path) return false
|
|
205
|
+
const sourceExts = ['.ts', '.js', '.tsx', '.jsx', '.py', '.java', '.go', '.rs', '.cpp', '.c', '.h']
|
|
206
|
+
return sourceExts.some((ext) => path.endsWith(ext))
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private isBuildCommand(cmd?: string): boolean {
|
|
210
|
+
if (!cmd) return false
|
|
211
|
+
const buildCmds = ['npm run build', 'tsc', 'vite build', 'webpack', 'rollup', 'esbuild', 'jest', 'vitest', 'pytest']
|
|
212
|
+
return buildCmds.some((bc) => cmd.includes(bc))
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private inferOperationType(operation: string): 'read' | 'search' | 'write' | 'run' | 'edit' {
|
|
216
|
+
if (operation.includes('read') || operation.includes('open')) return 'read'
|
|
217
|
+
if (operation.includes('search') || operation.includes('find') || operation.includes('grep')) return 'search'
|
|
218
|
+
if (operation.includes('write') || operation.includes('create')) return 'write'
|
|
219
|
+
if (operation.includes('edit') || operation.includes('modify')) return 'edit'
|
|
220
|
+
return 'run'
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function createMainAgentGuard(
|
|
225
|
+
config: Partial<GuardConfig> = {},
|
|
226
|
+
logger: Logger
|
|
227
|
+
): MainAgentGuard {
|
|
228
|
+
return new MainAgentGuard(
|
|
229
|
+
{
|
|
230
|
+
tokenBudget: config.tokenBudget || 60000,
|
|
231
|
+
embargoActive: config.embargoActive || false,
|
|
232
|
+
prohibited: {
|
|
233
|
+
readSourceFiles: true,
|
|
234
|
+
searchCodebase: true,
|
|
235
|
+
writeFiles: true,
|
|
236
|
+
runBuildCommands: true,
|
|
237
|
+
runTests: true,
|
|
238
|
+
editLargeFiles: true,
|
|
239
|
+
longRunningTasks: true,
|
|
240
|
+
...config.prohibited,
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
logger
|
|
244
|
+
)
|
|
245
|
+
}
|