agentic-orchestrator 0.1.13 → 0.1.15
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/AGENTS.md +139 -0
- package/CLAUDE.md +12 -0
- package/agentic/orchestrator/agents.yaml +3 -0
- package/agentic/orchestrator/defaults/policy.defaults.yaml +3 -0
- package/agentic/orchestrator/policy.yaml +3 -0
- package/agentic/orchestrator/schemas/agents.schema.json +15 -0
- package/agentic/orchestrator/schemas/policy.schema.json +14 -0
- package/apps/control-plane/src/cli/cli-argument-parser.ts +7 -0
- package/apps/control-plane/src/cli/help-command-handler.ts +8 -0
- package/apps/control-plane/src/cli/init-command-handler.ts +6 -0
- package/apps/control-plane/src/cli/resume-command-handler.ts +31 -2
- package/apps/control-plane/src/cli/run-command-handler.ts +31 -3
- package/apps/control-plane/src/cli/types.ts +1 -0
- package/apps/control-plane/src/core/error-codes.ts +4 -0
- package/apps/control-plane/src/core/kernel.ts +3 -0
- package/apps/control-plane/src/index.ts +14 -0
- package/apps/control-plane/src/interfaces/cli/bootstrap.ts +25 -3
- package/apps/control-plane/src/providers/api-worker-provider.ts +115 -0
- package/apps/control-plane/src/providers/cli-worker-provider.ts +385 -0
- package/apps/control-plane/src/providers/output-parsers/generic-output-parser.ts +100 -0
- package/apps/control-plane/src/providers/output-parsers/index.ts +11 -0
- package/apps/control-plane/src/providers/output-parsers/types.ts +23 -0
- package/apps/control-plane/src/providers/providers.ts +19 -0
- package/apps/control-plane/src/providers/worker-provider-factory.ts +198 -0
- package/apps/control-plane/src/supervisor/build-wave-executor.ts +140 -3
- package/apps/control-plane/src/supervisor/planning-wave-executor.ts +125 -5
- package/apps/control-plane/src/supervisor/qa-wave-executor.ts +144 -2
- package/apps/control-plane/src/supervisor/runtime.ts +24 -0
- package/apps/control-plane/src/supervisor/worker-decision-loop.ts +134 -12
- package/apps/control-plane/test/cli.unit.spec.ts +36 -0
- package/apps/control-plane/test/dashboard-api.integration.spec.ts +2 -2
- package/apps/control-plane/test/resume-command.spec.ts +31 -1
- package/apps/control-plane/test/worker-decision-loop.spec.ts +3 -0
- package/apps/control-plane/test/worker-execution-policy.spec.ts +284 -0
- package/apps/control-plane/test/worker-provider-adapters.spec.ts +440 -0
- package/apps/control-plane/test/worker-provider-factory.spec.ts +151 -0
- package/config/agentic/orchestrator/agents.yaml +3 -0
- package/dist/apps/control-plane/cli/cli-argument-parser.js +7 -0
- package/dist/apps/control-plane/cli/cli-argument-parser.js.map +1 -1
- package/dist/apps/control-plane/cli/help-command-handler.js +8 -0
- package/dist/apps/control-plane/cli/help-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/init-command-handler.js +6 -0
- package/dist/apps/control-plane/cli/init-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/resume-command-handler.d.ts +3 -0
- package/dist/apps/control-plane/cli/resume-command-handler.js +18 -2
- package/dist/apps/control-plane/cli/resume-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/run-command-handler.d.ts +3 -1
- package/dist/apps/control-plane/cli/run-command-handler.js +17 -3
- package/dist/apps/control-plane/cli/run-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/types.d.ts +1 -0
- package/dist/apps/control-plane/core/error-codes.d.ts +4 -0
- package/dist/apps/control-plane/core/error-codes.js +4 -0
- package/dist/apps/control-plane/core/error-codes.js.map +1 -1
- package/dist/apps/control-plane/core/kernel.d.ts +3 -0
- package/dist/apps/control-plane/core/kernel.js.map +1 -1
- package/dist/apps/control-plane/index.d.ts +2 -0
- package/dist/apps/control-plane/index.js +1 -0
- package/dist/apps/control-plane/index.js.map +1 -1
- package/dist/apps/control-plane/interfaces/cli/bootstrap.js +14 -2
- package/dist/apps/control-plane/interfaces/cli/bootstrap.js.map +1 -1
- package/dist/apps/control-plane/providers/api-worker-provider.d.ts +31 -0
- package/dist/apps/control-plane/providers/api-worker-provider.js +73 -0
- package/dist/apps/control-plane/providers/api-worker-provider.js.map +1 -0
- package/dist/apps/control-plane/providers/cli-worker-provider.d.ts +46 -0
- package/dist/apps/control-plane/providers/cli-worker-provider.js +274 -0
- package/dist/apps/control-plane/providers/cli-worker-provider.js.map +1 -0
- package/dist/apps/control-plane/providers/output-parsers/generic-output-parser.d.ts +10 -0
- package/dist/apps/control-plane/providers/output-parsers/generic-output-parser.js +79 -0
- package/dist/apps/control-plane/providers/output-parsers/generic-output-parser.js.map +1 -0
- package/dist/apps/control-plane/providers/output-parsers/index.d.ts +2 -0
- package/dist/apps/control-plane/providers/output-parsers/index.js +2 -0
- package/dist/apps/control-plane/providers/output-parsers/index.js.map +1 -0
- package/dist/apps/control-plane/providers/output-parsers/types.d.ts +21 -0
- package/dist/apps/control-plane/providers/output-parsers/types.js +2 -0
- package/dist/apps/control-plane/providers/output-parsers/types.js.map +1 -0
- package/dist/apps/control-plane/providers/providers.d.ts +4 -0
- package/dist/apps/control-plane/providers/providers.js +15 -0
- package/dist/apps/control-plane/providers/providers.js.map +1 -1
- package/dist/apps/control-plane/providers/worker-provider-factory.d.ts +41 -0
- package/dist/apps/control-plane/providers/worker-provider-factory.js +104 -0
- package/dist/apps/control-plane/providers/worker-provider-factory.js.map +1 -0
- package/dist/apps/control-plane/supervisor/build-wave-executor.d.ts +13 -0
- package/dist/apps/control-plane/supervisor/build-wave-executor.js +92 -3
- package/dist/apps/control-plane/supervisor/build-wave-executor.js.map +1 -1
- package/dist/apps/control-plane/supervisor/planning-wave-executor.d.ts +12 -0
- package/dist/apps/control-plane/supervisor/planning-wave-executor.js +83 -5
- package/dist/apps/control-plane/supervisor/planning-wave-executor.js.map +1 -1
- package/dist/apps/control-plane/supervisor/qa-wave-executor.d.ts +13 -0
- package/dist/apps/control-plane/supervisor/qa-wave-executor.js +91 -2
- package/dist/apps/control-plane/supervisor/qa-wave-executor.js.map +1 -1
- package/dist/apps/control-plane/supervisor/runtime.js +19 -0
- package/dist/apps/control-plane/supervisor/runtime.js.map +1 -1
- package/dist/apps/control-plane/supervisor/worker-decision-loop.d.ts +10 -0
- package/dist/apps/control-plane/supervisor/worker-decision-loop.js +113 -12
- package/dist/apps/control-plane/supervisor/worker-decision-loop.js.map +1 -1
- package/package.json +2 -2
- package/packages/web-dashboard/next-env.d.ts +2 -1
- package/packages/web-dashboard/src/app/api/features/[id]/checkout/route.ts +4 -3
- package/packages/web-dashboard/src/app/api/features/[id]/diff/route.ts +6 -2
- package/packages/web-dashboard/src/app/api/features/[id]/evidence/[artifact]/route.ts +6 -5
- package/packages/web-dashboard/src/app/api/features/[id]/review/route.ts +5 -4
- package/packages/web-dashboard/src/app/api/features/[id]/route.ts +7 -3
- package/packages/web-dashboard/src/lib/aop-client.ts +2 -2
- package/packages/web-dashboard/src/lib/orchestrator-tools.ts +1 -1
- package/packages/web-dashboard/tsconfig.json +1 -0
- package/spec-files/outstanding/agentic_orchestrator_human_input_interaction_protocol_spec.md +590 -0
- package/spec-files/outstanding/agentic_orchestrator_real_worker_provider_execution_spec.md +616 -0
- package/spec-files/progress.md +91 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GATE_RESULT, STATUS, TOOLS } from '../core/constants.js';
|
|
2
|
+
import { ERROR_CODES } from '../core/error-codes.js';
|
|
2
3
|
import type { WorkerProvider } from '../providers/providers.js';
|
|
3
4
|
import type {
|
|
4
5
|
AgentPromptProvider,
|
|
@@ -14,6 +15,11 @@ import type {
|
|
|
14
15
|
GateRepairContext,
|
|
15
16
|
ReactionsService,
|
|
16
17
|
} from '../application/services/reactions-service.js';
|
|
18
|
+
import type {
|
|
19
|
+
WorkerMalformedOutputAction,
|
|
20
|
+
WorkerNoProgressAction,
|
|
21
|
+
WorkerProviderMode,
|
|
22
|
+
} from '../providers/worker-provider-factory.js';
|
|
17
23
|
|
|
18
24
|
interface GatesRunData {
|
|
19
25
|
overall?: string;
|
|
@@ -29,6 +35,10 @@ interface QaWaveExecutorDependencies {
|
|
|
29
35
|
state: SupervisorRuntimeState;
|
|
30
36
|
workerDecisionRunner?: WorkerDecisionRunner;
|
|
31
37
|
reactionsService?: ReactionsService;
|
|
38
|
+
providerMode?: WorkerProviderMode;
|
|
39
|
+
noProgressLimit?: number;
|
|
40
|
+
noProgressAction?: WorkerNoProgressAction;
|
|
41
|
+
malformedOutputAction?: WorkerMalformedOutputAction;
|
|
32
42
|
}
|
|
33
43
|
|
|
34
44
|
function isRetryableToolError(error: unknown): boolean {
|
|
@@ -42,6 +52,13 @@ function isRetryableToolError(error: unknown): boolean {
|
|
|
42
52
|
return (details as { retryable?: unknown }).retryable === true;
|
|
43
53
|
}
|
|
44
54
|
|
|
55
|
+
function asVersion(value: unknown): number | null {
|
|
56
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return Math.floor(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
45
62
|
export class QaWaveExecutor {
|
|
46
63
|
private readonly kernel: FeatureOrchestrationPort;
|
|
47
64
|
private readonly provider: WorkerProvider;
|
|
@@ -51,6 +68,12 @@ export class QaWaveExecutor {
|
|
|
51
68
|
private readonly state: SupervisorRuntimeState;
|
|
52
69
|
private readonly workerDecisionRunner: WorkerDecisionRunner;
|
|
53
70
|
private readonly reactionsService: ReactionsService | undefined;
|
|
71
|
+
private readonly providerMode: WorkerProviderMode;
|
|
72
|
+
private readonly noProgressLimit: number;
|
|
73
|
+
private readonly noProgressAction: WorkerNoProgressAction;
|
|
74
|
+
private readonly malformedOutputAction: WorkerMalformedOutputAction;
|
|
75
|
+
private readonly noProgressByFeature = new Map<string, number>();
|
|
76
|
+
private readonly blockedForCurrentCycle = new Set<string>();
|
|
54
77
|
|
|
55
78
|
constructor(dependencies: QaWaveExecutorDependencies) {
|
|
56
79
|
this.kernel = dependencies.kernel;
|
|
@@ -61,9 +84,17 @@ export class QaWaveExecutor {
|
|
|
61
84
|
this.state = dependencies.state;
|
|
62
85
|
this.workerDecisionRunner = dependencies.workerDecisionRunner ?? NOOP_WORKER_DECISION_RUNNER;
|
|
63
86
|
this.reactionsService = dependencies.reactionsService;
|
|
87
|
+
this.providerMode = dependencies.providerMode ?? 'stub';
|
|
88
|
+
this.noProgressLimit =
|
|
89
|
+
typeof dependencies.noProgressLimit === 'number' && dependencies.noProgressLimit > 0
|
|
90
|
+
? Math.floor(dependencies.noProgressLimit)
|
|
91
|
+
: 2;
|
|
92
|
+
this.noProgressAction = dependencies.noProgressAction ?? 'block_feature';
|
|
93
|
+
this.malformedOutputAction = dependencies.malformedOutputAction ?? 'block_feature';
|
|
64
94
|
}
|
|
65
95
|
|
|
66
96
|
async run(featureIds: string[], maxParallelGateRuns: number): Promise<void> {
|
|
97
|
+
this.blockedForCurrentCycle.clear();
|
|
67
98
|
const batch: string[] = [];
|
|
68
99
|
for (const featureId of featureIds) {
|
|
69
100
|
const state = await this.toolCaller.callTool<FeatureStatePayload>(
|
|
@@ -83,13 +114,18 @@ export class QaWaveExecutor {
|
|
|
83
114
|
const context = await this.toolCaller.callTool('qa', TOOLS.FEATURE_GET_CONTEXT, {
|
|
84
115
|
feature_id: featureId,
|
|
85
116
|
});
|
|
86
|
-
await this.workerDecisionRunner.execute({
|
|
117
|
+
const decision = await this.workerDecisionRunner.execute({
|
|
87
118
|
role: 'qa',
|
|
88
119
|
featureId,
|
|
89
120
|
contextBundle: context.data,
|
|
90
121
|
instructions:
|
|
91
122
|
'Emit PATCH outputs for deterministic QA remediations, NOTE outputs for findings, and REQUEST outputs for lock/context needs.',
|
|
92
123
|
});
|
|
124
|
+
const shouldSkip = await this.enforceWorkerExecutionPolicy(featureId, decision, 'qa');
|
|
125
|
+
if (shouldSkip) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
this.noProgressByFeature.delete(featureId);
|
|
93
129
|
|
|
94
130
|
const stateForRetry = await this.toolCaller.callTool<FeatureStatePayload>(
|
|
95
131
|
'qa',
|
|
@@ -154,12 +190,21 @@ export class QaWaveExecutor {
|
|
|
154
190
|
failureHistory,
|
|
155
191
|
};
|
|
156
192
|
const repairPrompt = this.reactionsService.buildRepairPrompt(ctx);
|
|
157
|
-
await this.workerDecisionRunner.execute({
|
|
193
|
+
const repairDecision = await this.workerDecisionRunner.execute({
|
|
158
194
|
role: 'qa',
|
|
159
195
|
featureId,
|
|
160
196
|
contextBundle: context.data,
|
|
161
197
|
instructions: repairPrompt,
|
|
162
198
|
});
|
|
199
|
+
const shouldSkipRepair = await this.enforceWorkerExecutionPolicy(
|
|
200
|
+
featureId,
|
|
201
|
+
repairDecision,
|
|
202
|
+
'qa',
|
|
203
|
+
);
|
|
204
|
+
if (shouldSkipRepair) {
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
this.noProgressByFeature.delete(featureId);
|
|
163
208
|
|
|
164
209
|
try {
|
|
165
210
|
const retryResult = await this.toolCaller.callTool<GatesRunData>(
|
|
@@ -246,4 +291,101 @@ export class QaWaveExecutor {
|
|
|
246
291
|
});
|
|
247
292
|
}
|
|
248
293
|
}
|
|
294
|
+
|
|
295
|
+
private async enforceWorkerExecutionPolicy(
|
|
296
|
+
featureId: string,
|
|
297
|
+
decision: {
|
|
298
|
+
invalidOutput: boolean;
|
|
299
|
+
noProgress: boolean;
|
|
300
|
+
},
|
|
301
|
+
phase: 'qa',
|
|
302
|
+
): Promise<boolean> {
|
|
303
|
+
if (this.providerMode !== 'live') {
|
|
304
|
+
this.noProgressByFeature.delete(featureId);
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (decision.invalidOutput) {
|
|
309
|
+
await this.applyWorkerPolicyAction(
|
|
310
|
+
featureId,
|
|
311
|
+
this.malformedOutputAction,
|
|
312
|
+
ERROR_CODES.PROVIDER_OUTPUT_INVALID,
|
|
313
|
+
'QA emitted malformed worker outputs',
|
|
314
|
+
phase,
|
|
315
|
+
);
|
|
316
|
+
this.blockedForCurrentCycle.add(featureId);
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!decision.noProgress) {
|
|
321
|
+
this.noProgressByFeature.delete(featureId);
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const currentCount = (this.noProgressByFeature.get(featureId) ?? 0) + 1;
|
|
326
|
+
this.noProgressByFeature.set(featureId, currentCount);
|
|
327
|
+
this.blockedForCurrentCycle.add(featureId);
|
|
328
|
+
if (currentCount < this.noProgressLimit) {
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
await this.applyWorkerPolicyAction(
|
|
333
|
+
featureId,
|
|
334
|
+
this.noProgressAction,
|
|
335
|
+
ERROR_CODES.PROVIDER_NO_PROGRESS,
|
|
336
|
+
`QA made no actionable progress in ${currentCount} consecutive iterations`,
|
|
337
|
+
phase,
|
|
338
|
+
);
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private async applyWorkerPolicyAction(
|
|
343
|
+
featureId: string,
|
|
344
|
+
action: WorkerNoProgressAction | WorkerMalformedOutputAction,
|
|
345
|
+
errorCode: string,
|
|
346
|
+
message: string,
|
|
347
|
+
phase: 'qa',
|
|
348
|
+
): Promise<void> {
|
|
349
|
+
if (action === 'fail_run') {
|
|
350
|
+
const error = new Error(message) as Error & {
|
|
351
|
+
code?: string;
|
|
352
|
+
details?: Record<string, unknown>;
|
|
353
|
+
};
|
|
354
|
+
error.code = errorCode;
|
|
355
|
+
error.details = {
|
|
356
|
+
feature_id: featureId,
|
|
357
|
+
retryable: false,
|
|
358
|
+
requires_human: true,
|
|
359
|
+
};
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const state = await this.toolCaller.callTool<FeatureStatePayload>(
|
|
364
|
+
'orchestrator',
|
|
365
|
+
TOOLS.FEATURE_STATE_GET,
|
|
366
|
+
{
|
|
367
|
+
feature_id: featureId,
|
|
368
|
+
},
|
|
369
|
+
);
|
|
370
|
+
const expectedVersion = asVersion(state.data.front_matter.version);
|
|
371
|
+
await this.toolCaller.callTool('orchestrator', TOOLS.FEATURE_STATE_PATCH, {
|
|
372
|
+
feature_id: featureId,
|
|
373
|
+
expected_version: expectedVersion,
|
|
374
|
+
patch: {
|
|
375
|
+
front_matter: {
|
|
376
|
+
status: STATUS.BLOCKED,
|
|
377
|
+
status_reason: `${errorCode}: ${message}`,
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
await this.toolCaller.callTool('orchestrator', TOOLS.FEATURE_LOG_APPEND, {
|
|
382
|
+
feature_id: featureId,
|
|
383
|
+
note: JSON.stringify({
|
|
384
|
+
phase,
|
|
385
|
+
decision: 'blocked',
|
|
386
|
+
error_code: errorCode,
|
|
387
|
+
reason: message,
|
|
388
|
+
}),
|
|
389
|
+
});
|
|
390
|
+
}
|
|
249
391
|
}
|
|
@@ -16,6 +16,11 @@ import { ReactionsService } from '../application/services/reactions-service.js';
|
|
|
16
16
|
import { ActivityMonitorService } from '../application/services/activity-monitor-service.js';
|
|
17
17
|
import { PrMonitorService, createGhRunner } from '../application/services/pr-monitor-service.js';
|
|
18
18
|
import { createIssueTracker } from '../application/services/issue-tracker-service.js';
|
|
19
|
+
import {
|
|
20
|
+
resolveMalformedWorkerOutputAction,
|
|
21
|
+
resolveNoProgressAction,
|
|
22
|
+
resolveWorkerProviderRuntime,
|
|
23
|
+
} from '../providers/worker-provider-factory.js';
|
|
19
24
|
import {
|
|
20
25
|
globalAdapterRegistry,
|
|
21
26
|
NOTIFICATION_CHANNEL_SLOT,
|
|
@@ -190,23 +195,38 @@ export class SupervisorRuntime
|
|
|
190
195
|
? (rawIssueTrackerConfig as { type: string; config?: Record<string, string> })
|
|
191
196
|
: undefined;
|
|
192
197
|
const issueTracker = createIssueTracker(issueTrackerConfig);
|
|
198
|
+
const workerRuntimeConfig = resolveWorkerProviderRuntime(this.kernel.getAgentsConfig().runtime);
|
|
199
|
+
const executionPolicy =
|
|
200
|
+
policy.execution && typeof policy.execution === 'object'
|
|
201
|
+
? (policy.execution as Record<string, unknown>)
|
|
202
|
+
: null;
|
|
193
203
|
|
|
194
204
|
this.workerDecisionLoop = new WorkerDecisionLoop({
|
|
195
205
|
provider: this.provider,
|
|
196
206
|
toolCaller: this,
|
|
197
207
|
activityMonitor,
|
|
208
|
+
repoRoot: this.kernel.getRepoRoot(),
|
|
209
|
+
runId: () => this.state.runId,
|
|
198
210
|
});
|
|
199
211
|
|
|
200
212
|
this.planningWaveExecutor = new PlanningWaveExecutor({
|
|
201
213
|
toolCaller: this,
|
|
202
214
|
planGenerator: this,
|
|
203
215
|
workerDecisionRunner: this.workerDecisionLoop,
|
|
216
|
+
providerMode: this.provider.mode,
|
|
217
|
+
noProgressLimit: workerRuntimeConfig.max_consecutive_no_progress_iterations,
|
|
218
|
+
noProgressAction: resolveNoProgressAction(executionPolicy),
|
|
219
|
+
malformedOutputAction: resolveMalformedWorkerOutputAction(executionPolicy),
|
|
204
220
|
});
|
|
205
221
|
|
|
206
222
|
this.buildWaveExecutor = new BuildWaveExecutor({
|
|
207
223
|
toolCaller: this,
|
|
208
224
|
workerDecisionRunner: this.workerDecisionLoop,
|
|
209
225
|
reactionsService,
|
|
226
|
+
providerMode: this.provider.mode,
|
|
227
|
+
noProgressLimit: workerRuntimeConfig.max_consecutive_no_progress_iterations,
|
|
228
|
+
noProgressAction: resolveNoProgressAction(executionPolicy),
|
|
229
|
+
malformedOutputAction: resolveMalformedWorkerOutputAction(executionPolicy),
|
|
210
230
|
});
|
|
211
231
|
|
|
212
232
|
this.qaWaveExecutor = new QaWaveExecutor({
|
|
@@ -218,6 +238,10 @@ export class SupervisorRuntime
|
|
|
218
238
|
state: this.state,
|
|
219
239
|
workerDecisionRunner: this.workerDecisionLoop,
|
|
220
240
|
reactionsService,
|
|
241
|
+
providerMode: this.provider.mode,
|
|
242
|
+
noProgressLimit: workerRuntimeConfig.max_consecutive_no_progress_iterations,
|
|
243
|
+
noProgressAction: resolveNoProgressAction(executionPolicy),
|
|
244
|
+
malformedOutputAction: resolveMalformedWorkerOutputAction(executionPolicy),
|
|
221
245
|
});
|
|
222
246
|
|
|
223
247
|
this.leaseHeartbeatService = new LeaseHeartbeatService({
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import { TOOLS } from '../core/constants.js';
|
|
4
|
+
import { ERROR_CODES } from '../core/error-codes.js';
|
|
2
5
|
import type { WorkerProvider } from '../providers/providers.js';
|
|
3
6
|
import type { RuntimeRole, SupervisorToolCaller } from './types.js';
|
|
4
7
|
import type { ActivityMonitorService } from '../application/services/activity-monitor-service.js';
|
|
@@ -18,6 +21,9 @@ export interface WorkerDecisionResult {
|
|
|
18
21
|
patchApplied: boolean;
|
|
19
22
|
noteLogged: boolean;
|
|
20
23
|
requestHandled: boolean;
|
|
24
|
+
invalidOutput: boolean;
|
|
25
|
+
noProgress: boolean;
|
|
26
|
+
outputTypes: string[];
|
|
21
27
|
priorityOrder: string[];
|
|
22
28
|
toolResults: AnyRecord[];
|
|
23
29
|
}
|
|
@@ -73,6 +79,9 @@ function makeEmptyResult(): WorkerDecisionResult {
|
|
|
73
79
|
patchApplied: false,
|
|
74
80
|
noteLogged: false,
|
|
75
81
|
requestHandled: false,
|
|
82
|
+
invalidOutput: false,
|
|
83
|
+
noProgress: false,
|
|
84
|
+
outputTypes: [],
|
|
76
85
|
priorityOrder: [],
|
|
77
86
|
toolResults: [],
|
|
78
87
|
};
|
|
@@ -86,17 +95,23 @@ interface WorkerDecisionLoopDependencies {
|
|
|
86
95
|
provider: WorkerProvider;
|
|
87
96
|
toolCaller: SupervisorToolCaller;
|
|
88
97
|
activityMonitor?: ActivityMonitorService;
|
|
98
|
+
repoRoot?: string;
|
|
99
|
+
runId?: string | (() => string);
|
|
89
100
|
}
|
|
90
101
|
|
|
91
102
|
export class WorkerDecisionLoop implements WorkerDecisionRunner {
|
|
92
103
|
private readonly provider: WorkerProvider;
|
|
93
104
|
private readonly toolCaller: SupervisorToolCaller;
|
|
94
105
|
private readonly activityMonitor: ActivityMonitorService | undefined;
|
|
106
|
+
private readonly repoRoot: string | null;
|
|
107
|
+
private readonly runId: string | (() => string) | null;
|
|
95
108
|
|
|
96
109
|
constructor(dependencies: WorkerDecisionLoopDependencies) {
|
|
97
110
|
this.provider = dependencies.provider;
|
|
98
111
|
this.toolCaller = dependencies.toolCaller;
|
|
99
112
|
this.activityMonitor = dependencies.activityMonitor;
|
|
113
|
+
this.repoRoot = dependencies.repoRoot ?? null;
|
|
114
|
+
this.runId = dependencies.runId ?? null;
|
|
100
115
|
}
|
|
101
116
|
|
|
102
117
|
async execute(input: WorkerDecisionInput): Promise<WorkerDecisionResult> {
|
|
@@ -105,43 +120,93 @@ export class WorkerDecisionLoop implements WorkerDecisionRunner {
|
|
|
105
120
|
return result;
|
|
106
121
|
}
|
|
107
122
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
123
|
+
let workerOutput: Record<string, unknown>;
|
|
124
|
+
try {
|
|
125
|
+
workerOutput = await this.provider.runWorker({
|
|
126
|
+
role: input.role,
|
|
127
|
+
feature_id: input.featureId,
|
|
128
|
+
context_bundle: input.contextBundle,
|
|
129
|
+
instructions: input.instructions,
|
|
130
|
+
last_tool_results: input.lastToolResults ?? [],
|
|
131
|
+
runtime_selection: {
|
|
132
|
+
provider: this.provider.selection.provider,
|
|
133
|
+
model: this.provider.selection.model,
|
|
134
|
+
provider_config_ref: this.provider.selection.provider_config_ref,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
} catch (error) {
|
|
138
|
+
const typed = error as { code?: string };
|
|
139
|
+
await this.appendWorkerEvent({
|
|
140
|
+
featureId: input.featureId,
|
|
141
|
+
role: input.role,
|
|
142
|
+
outputTypes: [],
|
|
143
|
+
patchCount: 0,
|
|
144
|
+
planSubmissionCount: 0,
|
|
145
|
+
requestCount: 0,
|
|
146
|
+
noteCount: 0,
|
|
147
|
+
valid: false,
|
|
148
|
+
errorCode:
|
|
149
|
+
typeof typed.code === 'string' && typed.code.length > 0
|
|
150
|
+
? typed.code
|
|
151
|
+
: ERROR_CODES.PROVIDER_RUNTIME_UNAVAILABLE,
|
|
152
|
+
});
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
120
155
|
|
|
121
156
|
const outputs = asOutputs(workerOutput);
|
|
157
|
+
const allowedTypes = new Set(['PLAN_SUBMISSION', 'PATCH', 'NOTE', 'REQUEST']);
|
|
158
|
+
let planSubmissionCount = 0;
|
|
159
|
+
let patchCount = 0;
|
|
160
|
+
let requestCount = 0;
|
|
161
|
+
let noteCount = 0;
|
|
122
162
|
for (const output of outputs) {
|
|
123
163
|
const outputType = asNonEmptyString(output.type)?.toUpperCase();
|
|
124
164
|
if (!outputType) {
|
|
165
|
+
result.invalidOutput = true;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
result.outputTypes.push(outputType);
|
|
169
|
+
if (!allowedTypes.has(outputType)) {
|
|
170
|
+
result.invalidOutput = true;
|
|
125
171
|
continue;
|
|
126
172
|
}
|
|
127
173
|
|
|
128
174
|
if (outputType === 'PLAN_SUBMISSION') {
|
|
175
|
+
planSubmissionCount += 1;
|
|
129
176
|
await this.routePlanSubmission(input, output, result);
|
|
130
177
|
continue;
|
|
131
178
|
}
|
|
132
179
|
if (outputType === 'PATCH') {
|
|
180
|
+
patchCount += 1;
|
|
133
181
|
await this.routePatch(input, output, result);
|
|
134
182
|
continue;
|
|
135
183
|
}
|
|
136
184
|
if (outputType === 'NOTE') {
|
|
185
|
+
noteCount += 1;
|
|
137
186
|
await this.routeNote(input.featureId, output, result);
|
|
138
187
|
continue;
|
|
139
188
|
}
|
|
140
189
|
if (outputType === 'REQUEST') {
|
|
190
|
+
requestCount += 1;
|
|
141
191
|
await this.routeRequest(input, output, result);
|
|
142
192
|
}
|
|
143
193
|
}
|
|
144
194
|
|
|
195
|
+
const hasProgress = result.planSubmission || result.patchApplied || result.requestHandled;
|
|
196
|
+
result.noProgress = !hasProgress;
|
|
197
|
+
|
|
198
|
+
await this.appendWorkerEvent({
|
|
199
|
+
featureId: input.featureId,
|
|
200
|
+
role: input.role,
|
|
201
|
+
outputTypes: result.outputTypes,
|
|
202
|
+
patchCount,
|
|
203
|
+
planSubmissionCount,
|
|
204
|
+
requestCount,
|
|
205
|
+
noteCount,
|
|
206
|
+
valid: !result.invalidOutput,
|
|
207
|
+
errorCode: result.invalidOutput ? ERROR_CODES.PROVIDER_OUTPUT_INVALID : null,
|
|
208
|
+
});
|
|
209
|
+
|
|
145
210
|
if (this.activityMonitor) {
|
|
146
211
|
await this.activityMonitor.checkAndNotifyStuck(input.featureId);
|
|
147
212
|
}
|
|
@@ -314,4 +379,61 @@ export class WorkerDecisionLoop implements WorkerDecisionRunner {
|
|
|
314
379
|
result.planSubmission = true;
|
|
315
380
|
}
|
|
316
381
|
}
|
|
382
|
+
|
|
383
|
+
private resolveRunId(): string {
|
|
384
|
+
if (typeof this.runId === 'function') {
|
|
385
|
+
const value = this.runId();
|
|
386
|
+
return value && value.length > 0 ? value : `run:unknown:${Date.now()}`;
|
|
387
|
+
}
|
|
388
|
+
if (typeof this.runId === 'string' && this.runId.length > 0) {
|
|
389
|
+
return this.runId;
|
|
390
|
+
}
|
|
391
|
+
return `run:unknown:${Date.now()}`;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private sanitizeRunId(runId: string): string {
|
|
395
|
+
return runId.replace(/[^a-zA-Z0-9._:-]/g, '_');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private async appendWorkerEvent(event: {
|
|
399
|
+
featureId: string;
|
|
400
|
+
role: RuntimeRole;
|
|
401
|
+
outputTypes: string[];
|
|
402
|
+
patchCount: number;
|
|
403
|
+
planSubmissionCount: number;
|
|
404
|
+
requestCount: number;
|
|
405
|
+
noteCount: number;
|
|
406
|
+
valid: boolean;
|
|
407
|
+
errorCode: string | null;
|
|
408
|
+
}): Promise<void> {
|
|
409
|
+
if (!this.repoRoot) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const runId = this.resolveRunId();
|
|
413
|
+
const workerEventsDir = path.join(this.repoRoot, '.aop', 'runtime', 'worker-events');
|
|
414
|
+
const filePath = path.join(workerEventsDir, `${this.sanitizeRunId(runId)}.jsonl`);
|
|
415
|
+
|
|
416
|
+
const payload = {
|
|
417
|
+
ts: new Date().toISOString(),
|
|
418
|
+
run_id: runId,
|
|
419
|
+
feature_id: event.featureId,
|
|
420
|
+
role: event.role,
|
|
421
|
+
output_types: event.outputTypes,
|
|
422
|
+
patch_count: event.patchCount,
|
|
423
|
+
plan_submission_count: event.planSubmissionCount,
|
|
424
|
+
request_count: event.requestCount,
|
|
425
|
+
note_count: event.noteCount,
|
|
426
|
+
valid: event.valid,
|
|
427
|
+
error_code: event.errorCode,
|
|
428
|
+
provider: this.provider.selection.provider,
|
|
429
|
+
model: this.provider.selection.model,
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
try {
|
|
433
|
+
await fs.mkdir(workerEventsDir, { recursive: true });
|
|
434
|
+
await fs.appendFile(filePath, `${JSON.stringify(payload)}\n`, 'utf8');
|
|
435
|
+
} catch {
|
|
436
|
+
// Diagnostic logging is best-effort and must not alter wave behavior.
|
|
437
|
+
}
|
|
438
|
+
}
|
|
317
439
|
}
|
|
@@ -97,6 +97,10 @@ vi.mock('../src/core/kernel.js', () => {
|
|
|
97
97
|
return {};
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
getPolicySnapshot() {
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
|
|
100
104
|
async readIndex() {
|
|
101
105
|
return await readIndexMock();
|
|
102
106
|
}
|
|
@@ -124,6 +128,7 @@ vi.mock('../src/mcp/runtime-factory.js', () => ({
|
|
|
124
128
|
|
|
125
129
|
vi.mock('../src/providers/providers.js', () => ({
|
|
126
130
|
resolveProviderSelection: resolveProviderSelectionMock,
|
|
131
|
+
SUPPORTED_PROVIDERS: new Set(['codex', 'claude', 'gemini', 'custom', 'kiro-cli', 'copilot']),
|
|
127
132
|
NullWorkerProvider: class NullWorkerProvider {
|
|
128
133
|
selection: unknown;
|
|
129
134
|
|
|
@@ -133,6 +138,37 @@ vi.mock('../src/providers/providers.js', () => ({
|
|
|
133
138
|
},
|
|
134
139
|
}));
|
|
135
140
|
|
|
141
|
+
vi.mock('../src/providers/worker-provider-factory.js', () => ({
|
|
142
|
+
DefaultWorkerProviderFactory: class DefaultWorkerProviderFactory {
|
|
143
|
+
create(input: { selection: unknown }) {
|
|
144
|
+
return {
|
|
145
|
+
mode: 'live',
|
|
146
|
+
selection: input.selection,
|
|
147
|
+
async createSession() {
|
|
148
|
+
return { session_id: 'mock-session' };
|
|
149
|
+
},
|
|
150
|
+
async reattachSession() {
|
|
151
|
+
return null;
|
|
152
|
+
},
|
|
153
|
+
async closeSession() {
|
|
154
|
+
return { closed: true };
|
|
155
|
+
},
|
|
156
|
+
async runWorker() {
|
|
157
|
+
return { outputs: [{ type: 'NOTE', content: 'mock' }] };
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
resolveWorkerProviderMode: vi.fn((_cliMode, _configuredMode, fallback) => fallback ?? 'live'),
|
|
163
|
+
resolveWorkerProviderPolicy: vi.fn(() => ({ require_live_provider_for_run: true })),
|
|
164
|
+
resolveWorkerProviderRuntime: vi.fn(() => ({
|
|
165
|
+
worker_response_timeout_ms: 120000,
|
|
166
|
+
max_consecutive_no_progress_iterations: 2,
|
|
167
|
+
})),
|
|
168
|
+
resolveMalformedWorkerOutputAction: vi.fn(() => 'block_feature'),
|
|
169
|
+
resolveNoProgressAction: vi.fn(() => 'block_feature'),
|
|
170
|
+
}));
|
|
171
|
+
|
|
136
172
|
vi.mock('../src/supervisor/runtime.js', () => {
|
|
137
173
|
class SupervisorRuntime {
|
|
138
174
|
private readonly options: unknown;
|
|
@@ -190,7 +190,7 @@ describe('dashboard api integration', () => {
|
|
|
190
190
|
method: 'POST',
|
|
191
191
|
body: JSON.stringify({ action: 'checkout', stash_changes: false }),
|
|
192
192
|
}),
|
|
193
|
-
{ params: { id: 'feature_checkout' } },
|
|
193
|
+
{ params: Promise.resolve({ id: 'feature_checkout' }) },
|
|
194
194
|
);
|
|
195
195
|
const checkoutBody = (await checkoutResponse.json()) as {
|
|
196
196
|
ok: boolean;
|
|
@@ -208,7 +208,7 @@ describe('dashboard api integration', () => {
|
|
|
208
208
|
method: 'POST',
|
|
209
209
|
body: JSON.stringify({ action: 'restore' }),
|
|
210
210
|
}),
|
|
211
|
-
{ params: { id: 'feature_checkout' } },
|
|
211
|
+
{ params: Promise.resolve({ id: 'feature_checkout' }) },
|
|
212
212
|
);
|
|
213
213
|
const restoreBody = (await restoreResponse.json()) as {
|
|
214
214
|
ok: boolean;
|
|
@@ -2,10 +2,11 @@ import { describe, it, expect, vi } from 'vitest';
|
|
|
2
2
|
|
|
3
3
|
vi.mock('../src/providers/providers.js', () => ({
|
|
4
4
|
resolveProviderSelection: vi.fn(() => ({
|
|
5
|
-
provider: '
|
|
5
|
+
provider: 'custom',
|
|
6
6
|
model: 'none',
|
|
7
7
|
provider_config_ref: null,
|
|
8
8
|
})),
|
|
9
|
+
SUPPORTED_PROVIDERS: new Set(['codex', 'claude', 'gemini', 'custom', 'kiro-cli', 'copilot']),
|
|
9
10
|
NullWorkerProvider: class NullWorkerProvider {
|
|
10
11
|
selection: unknown;
|
|
11
12
|
|
|
@@ -27,6 +28,35 @@ vi.mock('../src/providers/providers.js', () => ({
|
|
|
27
28
|
},
|
|
28
29
|
}));
|
|
29
30
|
|
|
31
|
+
vi.mock('../src/providers/worker-provider-factory.js', () => ({
|
|
32
|
+
DefaultWorkerProviderFactory: class DefaultWorkerProviderFactory {
|
|
33
|
+
create(input: { selection: unknown }) {
|
|
34
|
+
return {
|
|
35
|
+
mode: 'live',
|
|
36
|
+
selection: input.selection,
|
|
37
|
+
async createSession() {
|
|
38
|
+
return { session_id: 'mock-session' };
|
|
39
|
+
},
|
|
40
|
+
async reattachSession() {
|
|
41
|
+
return null;
|
|
42
|
+
},
|
|
43
|
+
async closeSession() {
|
|
44
|
+
return { closed: true };
|
|
45
|
+
},
|
|
46
|
+
async runWorker() {
|
|
47
|
+
return { outputs: [{ type: 'NOTE', content: 'mock' }] };
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
resolveWorkerProviderMode: vi.fn(() => 'live'),
|
|
53
|
+
resolveWorkerProviderPolicy: vi.fn(() => ({ require_live_provider_for_run: true })),
|
|
54
|
+
resolveWorkerProviderRuntime: vi.fn(() => ({
|
|
55
|
+
worker_response_timeout_ms: 120000,
|
|
56
|
+
max_consecutive_no_progress_iterations: 2,
|
|
57
|
+
})),
|
|
58
|
+
}));
|
|
59
|
+
|
|
30
60
|
vi.mock('../src/supervisor/runtime.js', () => ({
|
|
31
61
|
SupervisorRuntime: class SupervisorRuntime {
|
|
32
62
|
async start(features: unknown[]) {
|