parallel-codex-tui 0.2.10 → 0.3.1
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 +25 -8
- package/dist/bootstrap.js +8 -1
- package/dist/cli.js +10 -1
- package/dist/core/collaboration-timeline.js +3 -1
- package/dist/core/role-configuration.js +277 -0
- package/dist/domain/schemas.js +9 -0
- package/dist/orchestrator/collaboration-channel.js +11 -3
- package/dist/orchestrator/orchestrator.js +249 -46
- package/dist/tui/App.js +459 -12
- package/dist/tui/AppShell.js +7 -1
- package/dist/tui/FeatureBoardView.js +4 -1
- package/dist/tui/InputBar.js +37 -6
- package/dist/tui/RoleConfigurationView.js +96 -0
- package/dist/tui/StatusDetailView.js +34 -10
- package/dist/tui/keyboard.js +11 -0
- package/dist/tui/role-configuration-state.js +74 -0
- package/dist/version.js +1 -1
- package/dist/workers/native-attach.js +5 -1
- package/package.json +1 -1
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { cp, readdir } from "node:fs/promises";
|
|
2
3
|
import { join } from "node:path";
|
|
4
|
+
import { runRuntimePreflight } from "../doctor.js";
|
|
3
5
|
import { appendJsonLine, ensureDir, pathExists, readJson, readTextIfExists, removeIfExists, writeJson, writeText } from "../core/file-store.js";
|
|
4
6
|
import { runWithLeaseFinalization } from "../core/lease-finalization.js";
|
|
5
7
|
import { extractMainResponse } from "../core/main-response.js";
|
|
6
8
|
import { claimTaskRunLease, TaskRunLeaseConflictError } from "../core/process-ownership.js";
|
|
7
9
|
import { routerRuntimeDir } from "../core/paths.js";
|
|
10
|
+
import { RoleConfigurationManager, roleSelectionWithEngines } from "../core/role-configuration.js";
|
|
8
11
|
import { classifyRouterFailure, routerFallbackIsTransient } from "../core/router-audit.js";
|
|
9
12
|
import { sanitizeRouterText } from "../core/router-redaction.js";
|
|
10
13
|
import { routeRequestWithCodex, routerCommandLabel, routerProxyContext } from "../core/router.js";
|
|
@@ -59,6 +62,7 @@ export class Orchestrator {
|
|
|
59
62
|
routerConfigLoader;
|
|
60
63
|
dependencies;
|
|
61
64
|
activeFeatureRuns = new Map();
|
|
65
|
+
roleConfiguration;
|
|
62
66
|
constructor(config, sessions, workers, routeRunner, routerCwd = routerRuntimeDir(config.projectRoot, config.dataDir), routerConfigLoader, dependencies = {}) {
|
|
63
67
|
this.config = config;
|
|
64
68
|
this.sessions = sessions;
|
|
@@ -67,17 +71,21 @@ export class Orchestrator {
|
|
|
67
71
|
this.routerCwd = routerCwd;
|
|
68
72
|
this.routerConfigLoader = routerConfigLoader;
|
|
69
73
|
this.dependencies = dependencies;
|
|
74
|
+
this.roleConfiguration = dependencies.roleConfiguration ?? RoleConfigurationManager.transient(config);
|
|
70
75
|
}
|
|
71
76
|
async handleRequest(input) {
|
|
72
77
|
throwIfCancelled(input.signal);
|
|
73
|
-
const
|
|
78
|
+
const routed = await this.routeRequest(input.request, input.cwd, input.signal, "initial", input.onRouteStart, input.onRouteFallback, input.onRouteProgress);
|
|
79
|
+
const roleSelection = input.roleSelection ?? await this.roleConfiguration.selectionForRequest();
|
|
80
|
+
const route = routeWithRoleSelection(routed, roleSelection);
|
|
74
81
|
input.onRoute?.(route);
|
|
75
82
|
throwIfCancelled(input.signal);
|
|
76
83
|
const workers = [];
|
|
84
|
+
const executionInput = { ...input, roleSelection };
|
|
77
85
|
if (route.mode === "simple") {
|
|
78
86
|
try {
|
|
79
87
|
input.onStatus?.({ taskId: "main", main: "starting" });
|
|
80
|
-
const output = await this.runMain(
|
|
88
|
+
const output = await this.runMain(executionInput, workers);
|
|
81
89
|
input.onStatus?.({ taskId: "main", main: "done" });
|
|
82
90
|
return {
|
|
83
91
|
mode: "simple",
|
|
@@ -104,18 +112,20 @@ export class Orchestrator {
|
|
|
104
112
|
userPath: join(task.dir, "turns", "0001", "user.md"),
|
|
105
113
|
routePath: join(task.dir, "turns", "0001", "route.json")
|
|
106
114
|
};
|
|
107
|
-
|
|
115
|
+
await this.roleConfiguration.writeTurnSelection(turn.dir, roleSelectionWithEngines(route, roleSelection));
|
|
116
|
+
return this.withTaskRunLease(task, () => this.runInitialTask(executionInput, task, route, turn, workers));
|
|
108
117
|
}
|
|
109
118
|
async handleTaskTurn(input) {
|
|
110
119
|
throwIfCancelled(input.signal);
|
|
111
120
|
const task = this.sessions.taskFromId(input.taskId);
|
|
112
|
-
const
|
|
121
|
+
const roleSelection = input.roleSelection ?? await this.roleConfiguration.selectionForRequest(task.dir);
|
|
122
|
+
const route = routeWithRoleSelection(input.route ?? await this.routeRequest(input.request, input.cwd, input.signal, "follow-up", input.onRouteStart, input.onRouteFallback, input.onRouteProgress), roleSelection);
|
|
113
123
|
if (!input.route) {
|
|
114
124
|
input.onRoute?.(route);
|
|
115
125
|
}
|
|
116
126
|
throwIfCancelled(input.signal);
|
|
117
127
|
if (route.mode === "simple") {
|
|
118
|
-
return this.answerTaskQuestion({ ...input, route });
|
|
128
|
+
return this.answerTaskQuestion({ ...input, route, roleSelection });
|
|
119
129
|
}
|
|
120
130
|
return this.withTaskRunLease(task, async () => {
|
|
121
131
|
throwIfCancelled(input.signal);
|
|
@@ -124,8 +134,9 @@ export class Orchestrator {
|
|
|
124
134
|
request: input.request,
|
|
125
135
|
route
|
|
126
136
|
});
|
|
137
|
+
await this.roleConfiguration.writeTurnSelection(turn.dir, roleSelectionWithEngines(route, roleSelection));
|
|
127
138
|
const workers = [];
|
|
128
|
-
return this.runPairTask(input, task, route, turn, workers);
|
|
139
|
+
return this.runPairTask({ ...input, roleSelection }, task, route, turn, workers);
|
|
129
140
|
});
|
|
130
141
|
}
|
|
131
142
|
async retryTask(input) {
|
|
@@ -175,20 +186,126 @@ export class Orchestrator {
|
|
|
175
186
|
};
|
|
176
187
|
const assignmentPath = join(featureDir, "assignment.json");
|
|
177
188
|
const current = await readFeatureAssignment({ assignmentPath }, fallback);
|
|
178
|
-
const
|
|
189
|
+
const nextModel = input.model === undefined
|
|
190
|
+
? input.role === "actor"
|
|
191
|
+
? current.actor_engine === input.engine ? current.actor_model : ""
|
|
192
|
+
: current.critic_engine === input.engine ? current.critic_model : ""
|
|
193
|
+
: input.model;
|
|
194
|
+
const assignment = await writeFeatureAssignment({ assignmentPath }, input.role === "actor" ? input.engine : current.actor_engine, input.role === "critic" ? input.engine : current.critic_engine, input.role === "actor" ? nextModel : current.actor_model, input.role === "critic" ? nextModel : current.critic_model, {
|
|
195
|
+
actorOverride: input.role === "actor" ? true : current.actor_override,
|
|
196
|
+
criticOverride: input.role === "critic" ? true : current.critic_override
|
|
197
|
+
});
|
|
179
198
|
await appendJsonLine(join(task.dir, "dialogue", "actor-critic.jsonl"), {
|
|
180
199
|
time: new Date().toISOString(),
|
|
181
200
|
feature_id: input.featureId,
|
|
182
201
|
turn_id: status.turn_id,
|
|
183
202
|
type: "feature.assignment_changed",
|
|
184
203
|
role: input.role,
|
|
185
|
-
message: `${capitalize(input.role)} reassigned to ${input.engine}.`,
|
|
204
|
+
message: `${capitalize(input.role)} reassigned to ${input.engine}/${nextModel.trim() || "default"}.`,
|
|
186
205
|
paths: { assignment: assignmentPath }
|
|
187
206
|
});
|
|
188
|
-
await this.sessions.appendEvent(task, "feature.assignment_changed", `${input.featureId} ${input.role} reassigned to ${input.engine}`);
|
|
207
|
+
await this.sessions.appendEvent(task, "feature.assignment_changed", `${input.featureId} ${input.role} reassigned to ${input.engine}/${nextModel.trim() || "default"}`);
|
|
189
208
|
return { featureId: input.featureId, assignment };
|
|
190
209
|
});
|
|
191
210
|
}
|
|
211
|
+
async roleConfigurationSnapshot(taskId) {
|
|
212
|
+
const task = taskId ? this.sessions.taskFromId(taskId) : null;
|
|
213
|
+
if (task && !(await pathExists(task.dir))) {
|
|
214
|
+
throw new Error(`Task session not found: ${taskId}`);
|
|
215
|
+
}
|
|
216
|
+
return this.roleConfiguration.snapshot(task?.dir);
|
|
217
|
+
}
|
|
218
|
+
async validateRoleConfiguration(roles) {
|
|
219
|
+
const parsed = this.roleConfiguration.validate(roles);
|
|
220
|
+
const candidate = structuredClone(this.config);
|
|
221
|
+
candidate.router.defaultMode = "auto";
|
|
222
|
+
candidate.pairing.main = parsed.main.engine;
|
|
223
|
+
candidate.pairing.judge = parsed.judge.engine;
|
|
224
|
+
candidate.pairing.actor = parsed.actor.engine;
|
|
225
|
+
candidate.pairing.critic = parsed.critic.engine;
|
|
226
|
+
const preflight = this.dependencies.roleConfigurationPreflight
|
|
227
|
+
?? ((config, workspaceRoot) => runRuntimePreflight(config, workspaceRoot, process.env, { includeRouter: false }));
|
|
228
|
+
return preflight(candidate, this.roleConfiguration.workspaceRootPath());
|
|
229
|
+
}
|
|
230
|
+
async updateRoleConfiguration(input) {
|
|
231
|
+
if (input.scope !== "task") {
|
|
232
|
+
await this.roleConfiguration.apply(input.scope, input.roles);
|
|
233
|
+
return this.roleConfigurationSnapshot(input.taskId);
|
|
234
|
+
}
|
|
235
|
+
const taskId = input.taskId;
|
|
236
|
+
if (!taskId) {
|
|
237
|
+
throw new Error("No active Task is available for a current-task role configuration.");
|
|
238
|
+
}
|
|
239
|
+
const task = this.sessions.taskFromId(taskId);
|
|
240
|
+
if (!(await pathExists(task.dir))) {
|
|
241
|
+
throw new Error(`Task session not found: ${taskId}`);
|
|
242
|
+
}
|
|
243
|
+
return this.withTaskRunLease(task, async () => {
|
|
244
|
+
await this.roleConfiguration.apply("task", input.roles, task.dir);
|
|
245
|
+
await this.synchronizeRetryRoleConfiguration(task, input.roles);
|
|
246
|
+
await this.sessions.appendEvent(task, "task.role_configuration_changed", "Current Task role configuration updated");
|
|
247
|
+
return this.roleConfiguration.snapshot(task.dir);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
async clearRoleConfiguration(scope, taskId) {
|
|
251
|
+
if (scope !== "task") {
|
|
252
|
+
await this.roleConfiguration.clear(scope);
|
|
253
|
+
return this.roleConfigurationSnapshot(taskId);
|
|
254
|
+
}
|
|
255
|
+
if (!taskId) {
|
|
256
|
+
throw new Error("No active Task is available for a current-task role configuration.");
|
|
257
|
+
}
|
|
258
|
+
const task = this.sessions.taskFromId(taskId);
|
|
259
|
+
if (!(await pathExists(task.dir))) {
|
|
260
|
+
throw new Error(`Task session not found: ${taskId}`);
|
|
261
|
+
}
|
|
262
|
+
return this.withTaskRunLease(task, async () => {
|
|
263
|
+
await this.roleConfiguration.clear("task", task.dir);
|
|
264
|
+
const roles = await this.roleConfiguration.selectionForTask(task.dir);
|
|
265
|
+
await this.synchronizeRetryRoleConfiguration(task, roles);
|
|
266
|
+
await this.sessions.appendEvent(task, "task.role_configuration_cleared", "Current Task role configuration reset");
|
|
267
|
+
return this.roleConfiguration.snapshot(task.dir);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
async synchronizeRetryRoleConfiguration(task, roles) {
|
|
271
|
+
const meta = await readTaskMetaIfValid(task.metaPath);
|
|
272
|
+
if (!meta || !new Set(["failed", "cancelled", "paused"]).has(meta.status)) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const turn = await this.sessions.latestTurn(task);
|
|
276
|
+
if (!turn) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const route = routeWithRoleSelection(await readJson(turn.routePath, RouteDecisionSchema), roles);
|
|
280
|
+
const executionRoles = roleSelectionWithEngines(route, roles);
|
|
281
|
+
await writeJson(turn.routePath, route);
|
|
282
|
+
await this.sessions.recordLatestRoute(task, route);
|
|
283
|
+
await this.roleConfiguration.writeTurnSelection(turn.dir, executionRoles);
|
|
284
|
+
const featuresDir = join(task.dir, "features");
|
|
285
|
+
if (!(await pathExists(featuresDir))) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const entries = await readdir(featuresDir, { withFileTypes: true });
|
|
289
|
+
for (const entry of entries) {
|
|
290
|
+
if (!entry.isDirectory()) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
const featureDir = join(featuresDir, entry.name);
|
|
294
|
+
const status = await readFeatureStatusIfValid(join(featureDir, "status.json"));
|
|
295
|
+
if (!status || status.task_id !== task.id || status.state === "approved") {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const assignmentPath = join(featureDir, "assignment.json");
|
|
299
|
+
const current = await readFeatureAssignment({ assignmentPath }, {
|
|
300
|
+
actor: executionRoles.actor.engine,
|
|
301
|
+
critic: executionRoles.critic.engine
|
|
302
|
+
});
|
|
303
|
+
await writeFeatureAssignment({ assignmentPath }, current.actor_override ? current.actor_engine : executionRoles.actor.engine, current.critic_override ? current.critic_engine : executionRoles.critic.engine, current.actor_override ? current.actor_model : executionRoles.actor.model, current.critic_override ? current.critic_model : executionRoles.critic.model, {
|
|
304
|
+
actorOverride: current.actor_override,
|
|
305
|
+
criticOverride: current.critic_override
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
192
309
|
async resumeTaskRun(input, pausedFeatureId) {
|
|
193
310
|
throwIfCancelled(input.signal);
|
|
194
311
|
const task = this.sessions.taskFromId(input.taskId);
|
|
@@ -226,11 +343,15 @@ export class Orchestrator {
|
|
|
226
343
|
throw new Error(`Task ${input.taskId} turn ${turn.turnId} has no request to retry.`);
|
|
227
344
|
}
|
|
228
345
|
const route = await readJson(turn.routePath, RouteDecisionSchema);
|
|
346
|
+
const roleSelection = await this.roleConfiguration.readTurnSelection(turn.dir)
|
|
347
|
+
?? roleSelectionWithEngines(route, await this.roleConfiguration.selectionForTask(task.dir));
|
|
348
|
+
await this.roleConfiguration.writeTurnSelection(turn.dir, roleSelection);
|
|
229
349
|
const executionInput = {
|
|
230
350
|
...input,
|
|
231
351
|
request,
|
|
232
352
|
cwd: meta.cwd,
|
|
233
|
-
retry: true
|
|
353
|
+
retry: true,
|
|
354
|
+
roleSelection
|
|
234
355
|
};
|
|
235
356
|
const workers = [];
|
|
236
357
|
input.onRoute?.(route);
|
|
@@ -311,14 +432,18 @@ export class Orchestrator {
|
|
|
311
432
|
}
|
|
312
433
|
async routeTaskFollowUp(input) {
|
|
313
434
|
throwIfCancelled(input.signal);
|
|
314
|
-
const
|
|
435
|
+
const routed = await this.routeRequest(input.request, input.cwd, input.signal, "follow-up", input.onRouteStart, input.onRouteFallback, input.onRouteProgress);
|
|
436
|
+
const task = this.sessions.taskFromId(input.taskId);
|
|
437
|
+
const roleSelection = input.roleSelection ?? await this.roleConfiguration.selectionForRequest(task.dir);
|
|
438
|
+
const route = routeWithRoleSelection(routed, roleSelection);
|
|
315
439
|
input.onRoute?.(route);
|
|
316
440
|
throwIfCancelled(input.signal);
|
|
317
441
|
return {
|
|
318
442
|
mode: route.mode,
|
|
319
443
|
taskId: route.mode === "complex" ? input.taskId : null,
|
|
320
444
|
reason: route.reason,
|
|
321
|
-
route
|
|
445
|
+
route,
|
|
446
|
+
roleSelection
|
|
322
447
|
};
|
|
323
448
|
}
|
|
324
449
|
async answerTaskQuestion(input) {
|
|
@@ -327,7 +452,11 @@ export class Orchestrator {
|
|
|
327
452
|
if (!(await pathExists(task.dir))) {
|
|
328
453
|
throw new Error(`Task session not found: ${input.taskId}`);
|
|
329
454
|
}
|
|
330
|
-
|
|
455
|
+
const roleSelection = input.roleSelection ?? await this.roleConfiguration.selectionForRequest(task.dir);
|
|
456
|
+
return this.withTaskRunLease(task, () => this.answerTaskQuestionWithLease({
|
|
457
|
+
...input,
|
|
458
|
+
roleSelection
|
|
459
|
+
}, task));
|
|
331
460
|
}
|
|
332
461
|
async answerTaskQuestionWithLease(input, task) {
|
|
333
462
|
throwIfCancelled(input.signal);
|
|
@@ -437,6 +566,8 @@ export class Orchestrator {
|
|
|
437
566
|
feature,
|
|
438
567
|
actorEngine: route.actor_engine,
|
|
439
568
|
criticEngine: route.critic_engine,
|
|
569
|
+
actorModel: selectedRoleModel(input.roleSelection, "actor", route.actor_engine),
|
|
570
|
+
criticModel: selectedRoleModel(input.roleSelection, "critic", route.critic_engine),
|
|
440
571
|
resume: input.retry
|
|
441
572
|
})));
|
|
442
573
|
return await this.runFeaturePlanWithConflictReplan(input, task, route, turn, workers, judge, featurePlan, features);
|
|
@@ -448,6 +579,8 @@ export class Orchestrator {
|
|
|
448
579
|
judgeDir: judge.dir,
|
|
449
580
|
actorEngine: route.actor_engine,
|
|
450
581
|
criticEngine: route.critic_engine,
|
|
582
|
+
actorModel: selectedRoleModel(input.roleSelection, "actor", route.actor_engine),
|
|
583
|
+
criticModel: selectedRoleModel(input.roleSelection, "critic", route.critic_engine),
|
|
451
584
|
resume: input.retry
|
|
452
585
|
});
|
|
453
586
|
features = [feature];
|
|
@@ -483,6 +616,8 @@ export class Orchestrator {
|
|
|
483
616
|
feature,
|
|
484
617
|
actorEngine: route.actor_engine,
|
|
485
618
|
criticEngine: route.critic_engine,
|
|
619
|
+
actorModel: selectedRoleModel(input.roleSelection, "actor", route.actor_engine),
|
|
620
|
+
criticModel: selectedRoleModel(input.roleSelection, "critic", route.critic_engine),
|
|
486
621
|
resume: input.retry
|
|
487
622
|
})));
|
|
488
623
|
return await this.runFeaturePlanWithConflictReplan(input, task, route, turn, workers, judge, featurePlan, features);
|
|
@@ -494,6 +629,8 @@ export class Orchestrator {
|
|
|
494
629
|
judgeDir: judge.dir,
|
|
495
630
|
actorEngine: route.actor_engine,
|
|
496
631
|
criticEngine: route.critic_engine,
|
|
632
|
+
actorModel: selectedRoleModel(input.roleSelection, "actor", route.actor_engine),
|
|
633
|
+
criticModel: selectedRoleModel(input.roleSelection, "critic", route.critic_engine),
|
|
497
634
|
resume: input.retry
|
|
498
635
|
});
|
|
499
636
|
features = [feature];
|
|
@@ -662,6 +799,8 @@ export class Orchestrator {
|
|
|
662
799
|
feature,
|
|
663
800
|
actorEngine: route.actor_engine,
|
|
664
801
|
criticEngine: route.critic_engine,
|
|
802
|
+
actorModel: selectedRoleModel(currentInput.roleSelection, "actor", route.actor_engine),
|
|
803
|
+
criticModel: selectedRoleModel(currentInput.roleSelection, "critic", route.critic_engine),
|
|
665
804
|
resume: true,
|
|
666
805
|
refreshDefinition: true
|
|
667
806
|
})));
|
|
@@ -746,7 +885,11 @@ export class Orchestrator {
|
|
|
746
885
|
await this.sessions.updateTaskStatus(task, "actor_running");
|
|
747
886
|
const actorRunById = new Map();
|
|
748
887
|
if (restoredWave) {
|
|
749
|
-
const restoredActors = await Promise.all(wave.map((definition) =>
|
|
888
|
+
const restoredActors = await Promise.all(wave.map((definition) => {
|
|
889
|
+
const channel = requiredChannel(channels, definition);
|
|
890
|
+
const assignment = requiredFeatureAssignment(assignments, channel);
|
|
891
|
+
return this.loadCompletedFeatureActor(task, assignment.actor_engine, assignment.actor_model, definition, channel);
|
|
892
|
+
}));
|
|
750
893
|
for (const actorRun of restoredActors) {
|
|
751
894
|
if (actorRun) {
|
|
752
895
|
actorRunById.set(actorRun.definition.id, actorRun);
|
|
@@ -765,7 +908,8 @@ export class Orchestrator {
|
|
|
765
908
|
}
|
|
766
909
|
const freshActorRuns = await mapWithConcurrency(pendingActors, concurrency, async (definition) => {
|
|
767
910
|
const channel = requiredChannel(channels, definition);
|
|
768
|
-
const
|
|
911
|
+
const assignment = requiredFeatureAssignment(assignments, channel);
|
|
912
|
+
const actor = await this.runActor(input, task, assignment.actor_engine, judge.dir, workers, turn, channel, assignment.actor_model, undefined, true, requiredFeatureWorkspace(workspaceWave.featureDirs, channel), true);
|
|
769
913
|
actorCompleted += 1;
|
|
770
914
|
reportProgress("actor", actorCompleted, wave.length, actorCompleted === wave.length ? "done" : "running", "waiting");
|
|
771
915
|
return { definition, channel, actor };
|
|
@@ -784,7 +928,10 @@ export class Orchestrator {
|
|
|
784
928
|
await this.sessions.updateTaskStatus(task, "critic_running");
|
|
785
929
|
const pairRunById = new Map();
|
|
786
930
|
if (restoredWave) {
|
|
787
|
-
const restoredPairs = await Promise.all(actorRuns.map((actorRun) =>
|
|
931
|
+
const restoredPairs = await Promise.all(actorRuns.map((actorRun) => {
|
|
932
|
+
const assignment = requiredFeatureAssignment(assignments, actorRun.channel);
|
|
933
|
+
return this.loadCompletedFeaturePair(task, assignment.critic_engine, assignment.critic_model, actorRun);
|
|
934
|
+
}));
|
|
788
935
|
for (const pairRun of restoredPairs) {
|
|
789
936
|
if (pairRun) {
|
|
790
937
|
pairRunById.set(pairRun.definition.id, pairRun);
|
|
@@ -799,8 +946,9 @@ export class Orchestrator {
|
|
|
799
946
|
await this.sessions.appendEvent(task, "feature.wave_critic_checkpoints_reused", `Reused ${criticCompleted}/${actorRuns.length} completed Critic checkpoints in wave ${waveNumber}/${featureWaves.length}`);
|
|
800
947
|
}
|
|
801
948
|
const freshPairRuns = await mapWithConcurrency(pendingCritics, concurrency, async (actorRun) => {
|
|
949
|
+
const assignment = requiredFeatureAssignment(assignments, actorRun.channel);
|
|
802
950
|
const reviewWorkspace = await workspaceManager.prepareFeatureReviewWorkspace(workspaceWave, actorRun.channel.id);
|
|
803
|
-
const critic = await this.runCritic(input, task,
|
|
951
|
+
const critic = await this.runCritic(input, task, assignment.critic_engine, judge.dir, actorRun.actor.dir, workers, turn, actorRun.channel, assignment.critic_model, true, reviewWorkspace);
|
|
804
952
|
criticCompleted += 1;
|
|
805
953
|
reportProgress("critic", criticCompleted, actorRuns.length, "done", criticCompleted === actorRuns.length ? "done" : "running");
|
|
806
954
|
return { ...actorRun, critic };
|
|
@@ -860,7 +1008,8 @@ export class Orchestrator {
|
|
|
860
1008
|
let revisionCompleted = 0;
|
|
861
1009
|
reportProgress("revision", revisionCompleted, revisionRuns.length, "revision", "done");
|
|
862
1010
|
const revisedActors = await mapWithConcurrency(revisionRuns, concurrency, async (pair) => {
|
|
863
|
-
const
|
|
1011
|
+
const assignment = requiredFeatureAssignment(assignments, pair.channel);
|
|
1012
|
+
const actor = await this.runActor(input, task, assignment.actor_engine, judge.dir, workers, turn, pair.channel, assignment.actor_model, buildRevisionRequest(pair.review, pair.channel, revisionRound, this.config.orchestration.maxRevisionRounds), true, requiredFeatureWorkspace(workspaceWave.featureDirs, pair.channel));
|
|
864
1013
|
await requireActorFindingReplies(pair.channel, [...(revisionFindingIds.get(pair.channel.id) ?? [])]);
|
|
865
1014
|
revisionCompleted += 1;
|
|
866
1015
|
reportProgress("revision", revisionCompleted, revisionRuns.length, "revision", "done");
|
|
@@ -871,8 +1020,9 @@ export class Orchestrator {
|
|
|
871
1020
|
let recheckCompleted = 0;
|
|
872
1021
|
reportProgress("critic", recheckCompleted, revisedActors.length, "done", "rerunning");
|
|
873
1022
|
const revisedPairs = await mapWithConcurrency(revisedActors, concurrency, async (pair) => {
|
|
1023
|
+
const assignment = requiredFeatureAssignment(assignments, pair.channel);
|
|
874
1024
|
const reviewWorkspace = await workspaceManager.prepareFeatureReviewWorkspace(workspaceWave, pair.channel.id);
|
|
875
|
-
const critic = await this.runCritic(input, task,
|
|
1025
|
+
const critic = await this.runCritic(input, task, assignment.critic_engine, judge.dir, pair.actor.dir, workers, turn, pair.channel, assignment.critic_model, true, reviewWorkspace);
|
|
876
1026
|
recheckCompleted += 1;
|
|
877
1027
|
reportProgress("critic", recheckCompleted, revisedActors.length, "done", "rerunning");
|
|
878
1028
|
return {
|
|
@@ -1002,6 +1152,10 @@ export class Orchestrator {
|
|
|
1002
1152
|
};
|
|
1003
1153
|
const workspaceRootDir = join(task.dir, "workspaces", `turn-${turn.turnId}`, "wave-0001");
|
|
1004
1154
|
const integratedCheckpoint = input.retry && await waveIntegrationCheckpointMatches(workspaceRootDir, turn.turnId, 1, [feature.id]);
|
|
1155
|
+
const [actorExecution, criticExecution] = await Promise.all([
|
|
1156
|
+
this.featureWorkerExecution(turn, "actor", assignment.actor_engine, assignment.actor_model),
|
|
1157
|
+
this.featureWorkerExecution(turn, "critic", assignment.critic_engine, assignment.critic_model)
|
|
1158
|
+
]);
|
|
1005
1159
|
if (integratedCheckpoint) {
|
|
1006
1160
|
const recovered = !(await featureIsApproved(feature));
|
|
1007
1161
|
await this.sessions.appendEvent(task, recovered ? "feature.wave_checkpoint_recovered" : "feature.wave_checkpoint_reused", `${recovered ? "Recovered" : "Reused"} integrated checkpoint for single feature: ${feature.id}`);
|
|
@@ -1012,7 +1166,7 @@ export class Orchestrator {
|
|
|
1012
1166
|
critic: "done",
|
|
1013
1167
|
featureProgress: { wave: 1, waves: 1, phase: "integration", completed: 1, total: 1 }
|
|
1014
1168
|
});
|
|
1015
|
-
return this.completeTask(task, turn, judge, this.workerFiles(task, taskWorkerId("actor", assignment.actor_engine, turn.turnId)), this.workerFiles(task, taskWorkerId("critic", assignment.critic_engine, turn.turnId)), feature, input, workers, await integratedWaveChangedPaths(workspaceRootDir), workspaceManager, route.judge_engine);
|
|
1169
|
+
return this.completeTask(task, turn, judge, this.workerFiles(task, taskWorkerId("actor", assignment.actor_engine, turn.turnId, undefined, actorExecution.modelKey)), this.workerFiles(task, taskWorkerId("critic", assignment.critic_engine, turn.turnId, undefined, criticExecution.modelKey)), feature, input, workers, await integratedWaveChangedPaths(workspaceRootDir), workspaceManager, route.judge_engine);
|
|
1016
1170
|
}
|
|
1017
1171
|
const restoredWave = input.retry ? await workspaceManager.restoreWave(workspaceInput) : null;
|
|
1018
1172
|
const workspaceWave = restoredWave ?? await workspaceManager.prepareWave(workspaceInput);
|
|
@@ -1023,7 +1177,7 @@ export class Orchestrator {
|
|
|
1023
1177
|
throwIfCancelled(input.signal);
|
|
1024
1178
|
await this.sessions.updateTaskStatus(task, "actor_running");
|
|
1025
1179
|
const restoredActor = restoredWave
|
|
1026
|
-
? await this.loadCompletedActor(task, assignment.actor_engine, feature, false)
|
|
1180
|
+
? await this.loadCompletedActor(task, assignment.actor_engine, assignment.actor_model, feature, false)
|
|
1027
1181
|
: null;
|
|
1028
1182
|
if (restoredActor) {
|
|
1029
1183
|
await this.sessions.appendEvent(task, "feature.wave_actor_checkpoints_reused", "Reused 1/1 completed Actor checkpoint in wave 1/1");
|
|
@@ -1036,13 +1190,13 @@ export class Orchestrator {
|
|
|
1036
1190
|
});
|
|
1037
1191
|
let actor = restoredActor;
|
|
1038
1192
|
if (!actor) {
|
|
1039
|
-
actor = await this.runActor(input, task, assignment.actor_engine, judge.dir, workers, turn, feature, undefined, false, workspaceDir, true);
|
|
1193
|
+
actor = await this.runActor(input, task, assignment.actor_engine, judge.dir, workers, turn, feature, assignment.actor_model, undefined, false, workspaceDir, true);
|
|
1040
1194
|
}
|
|
1041
1195
|
await updateFeatureStatus(feature, "actor_done");
|
|
1042
1196
|
throwIfCancelled(input.signal);
|
|
1043
1197
|
await this.sessions.updateTaskStatus(task, "critic_running");
|
|
1044
1198
|
const restoredCritic = restoredActor
|
|
1045
|
-
? await this.loadCompletedCritic(task, assignment.critic_engine, feature, false)
|
|
1199
|
+
? await this.loadCompletedCritic(task, assignment.critic_engine, assignment.critic_model, feature, false)
|
|
1046
1200
|
: null;
|
|
1047
1201
|
if (restoredCritic) {
|
|
1048
1202
|
await this.sessions.appendEvent(task, "feature.wave_critic_checkpoints_reused", "Reused 1/1 completed Critic checkpoint in wave 1/1");
|
|
@@ -1057,7 +1211,7 @@ export class Orchestrator {
|
|
|
1057
1211
|
let critic = restoredCritic;
|
|
1058
1212
|
if (!critic) {
|
|
1059
1213
|
reviewWorkspace = await workspaceManager.prepareFeatureReviewWorkspace(workspaceWave, feature.id);
|
|
1060
|
-
critic = await this.runCritic(input, task, assignment.critic_engine, judge.dir, actor.dir, workers, turn, feature, false, reviewWorkspace, true);
|
|
1214
|
+
critic = await this.runCritic(input, task, assignment.critic_engine, judge.dir, actor.dir, workers, turn, feature, assignment.critic_model, false, reviewWorkspace, true);
|
|
1061
1215
|
}
|
|
1062
1216
|
await updateFeatureStatus(feature, "critic_done");
|
|
1063
1217
|
throwIfCancelled(input.signal);
|
|
@@ -1088,14 +1242,14 @@ export class Orchestrator {
|
|
|
1088
1242
|
critic: "done"
|
|
1089
1243
|
});
|
|
1090
1244
|
await this.sessions.updateTaskStatus(task, "actor_running");
|
|
1091
|
-
actor = await this.runActor(input, task, assignment.actor_engine, judge.dir, workers, turn, feature, buildRevisionRequest(review, feature, revisionRound, this.config.orchestration.maxRevisionRounds), false, workspaceDir, true);
|
|
1245
|
+
actor = await this.runActor(input, task, assignment.actor_engine, judge.dir, workers, turn, feature, assignment.actor_model, buildRevisionRequest(review, feature, revisionRound, this.config.orchestration.maxRevisionRounds), false, workspaceDir, true);
|
|
1092
1246
|
await requireActorFindingReplies(feature, [...revisionFindingIds]);
|
|
1093
1247
|
await updateFeatureStatus(feature, "actor_done");
|
|
1094
1248
|
throwIfCancelled(input.signal);
|
|
1095
1249
|
reviewWorkspace = await workspaceManager.prepareFeatureReviewWorkspace(workspaceWave, feature.id);
|
|
1096
1250
|
await this.sessions.updateTaskStatus(task, "critic_running");
|
|
1097
1251
|
input.onStatus?.({ taskId: task.id, judge: "done", actor: "done", critic: "rerunning" });
|
|
1098
|
-
critic = await this.runCritic(input, task, assignment.critic_engine, judge.dir, actor.dir, workers, turn, feature, false, reviewWorkspace, true);
|
|
1252
|
+
critic = await this.runCritic(input, task, assignment.critic_engine, judge.dir, actor.dir, workers, turn, feature, assignment.critic_model, false, reviewWorkspace, true);
|
|
1099
1253
|
await updateFeatureStatus(feature, "critic_done");
|
|
1100
1254
|
throwIfCancelled(input.signal);
|
|
1101
1255
|
review = await readTextIfExists(`${critic.dir}/review.md`);
|
|
@@ -1357,7 +1511,8 @@ export class Orchestrator {
|
|
|
1357
1511
|
}
|
|
1358
1512
|
async runMain(input, workers, context, taskId = null) {
|
|
1359
1513
|
throwIfCancelled(input.signal);
|
|
1360
|
-
const
|
|
1514
|
+
const target = input.roleSelection?.main ?? this.roleConfiguration.futureRoles().main;
|
|
1515
|
+
const engine = target.engine;
|
|
1361
1516
|
const dir = this.sessions.mainSessionDir();
|
|
1362
1517
|
let lease;
|
|
1363
1518
|
try {
|
|
@@ -1427,6 +1582,7 @@ export class Orchestrator {
|
|
|
1427
1582
|
outputLogPath,
|
|
1428
1583
|
statusPath,
|
|
1429
1584
|
prompt,
|
|
1585
|
+
modelConfig: this.roleConfiguration.modelForTarget(input.roleSelection?.main ?? this.roleConfiguration.futureRoles().main),
|
|
1430
1586
|
signal: input.signal,
|
|
1431
1587
|
onStatus: (runtimeStatus) => {
|
|
1432
1588
|
this.recordWorker(input, workers, { ...worker, runtimeStatus });
|
|
@@ -1473,6 +1629,7 @@ export class Orchestrator {
|
|
|
1473
1629
|
outputLogPath: judge.outputLogPath,
|
|
1474
1630
|
statusPath: judge.statusPath,
|
|
1475
1631
|
prompt: await readTextIfExists(judge.promptPath),
|
|
1632
|
+
modelConfig: await this.roleConfiguration.modelForTurn(turn.dir, "judge", engine),
|
|
1476
1633
|
signal: input.signal
|
|
1477
1634
|
}, "task", await this.previousTurnWorker(task, "judge", engine, turn.turnId));
|
|
1478
1635
|
ensureWorkerSuccess(result);
|
|
@@ -1548,6 +1705,7 @@ export class Orchestrator {
|
|
|
1548
1705
|
outputLogPath: finalJudge.outputLogPath,
|
|
1549
1706
|
statusPath: finalJudge.statusPath,
|
|
1550
1707
|
prompt: await readTextIfExists(finalJudge.promptPath),
|
|
1708
|
+
modelConfig: await this.roleConfiguration.modelForTurn(turn.dir, "judge", engine),
|
|
1551
1709
|
signal: input.signal
|
|
1552
1710
|
}, "task", initialJudge);
|
|
1553
1711
|
ensureWorkerSuccess(result);
|
|
@@ -1576,8 +1734,9 @@ export class Orchestrator {
|
|
|
1576
1734
|
await this.sessions.appendEvent(task, "judge.final_approved", `Final Judge approved ${criterionIds.length} acceptance criteria`);
|
|
1577
1735
|
input.onStatus?.({ taskId: task.id, judge: "done", actor: "done", critic: "done" });
|
|
1578
1736
|
}
|
|
1579
|
-
async runActor(input, task, engine, judgeDir, workers, turn, feature, revision, featureScoped = false, workspaceDir = input.cwd, isolatedWorkspace = featureScoped) {
|
|
1580
|
-
const
|
|
1737
|
+
async runActor(input, task, engine, judgeDir, workers, turn, feature, modelName = "", revision, featureScoped = false, workspaceDir = input.cwd, isolatedWorkspace = featureScoped) {
|
|
1738
|
+
const execution = await this.featureWorkerExecution(turn, "actor", engine, modelName);
|
|
1739
|
+
const workerId = taskWorkerId("actor", engine, turn.turnId, featureScoped ? feature.id : undefined, execution.modelKey);
|
|
1581
1740
|
const workerFiles = this.workerFiles(task, workerId);
|
|
1582
1741
|
const actor = await this.sessions.initializeWorker(task, {
|
|
1583
1742
|
workerId,
|
|
@@ -1623,6 +1782,7 @@ export class Orchestrator {
|
|
|
1623
1782
|
outputLogPath: actor.outputLogPath,
|
|
1624
1783
|
statusPath: actor.statusPath,
|
|
1625
1784
|
prompt: await readTextIfExists(actor.promptPath),
|
|
1785
|
+
modelConfig: execution.modelConfig,
|
|
1626
1786
|
signal: input.signal
|
|
1627
1787
|
}, task, feature, featureScoped
|
|
1628
1788
|
? undefined
|
|
@@ -1636,8 +1796,9 @@ export class Orchestrator {
|
|
|
1636
1796
|
});
|
|
1637
1797
|
return actor;
|
|
1638
1798
|
}
|
|
1639
|
-
async runCritic(input, task, engine, judgeDir, actorDir, workers, turn, feature, featureScoped = false, workspaceDir = input.cwd, isolatedWorkspace = featureScoped) {
|
|
1640
|
-
const
|
|
1799
|
+
async runCritic(input, task, engine, judgeDir, actorDir, workers, turn, feature, modelName = "", featureScoped = false, workspaceDir = input.cwd, isolatedWorkspace = featureScoped) {
|
|
1800
|
+
const execution = await this.featureWorkerExecution(turn, "critic", engine, modelName);
|
|
1801
|
+
const workerId = taskWorkerId("critic", engine, turn.turnId, featureScoped ? feature.id : undefined, execution.modelKey);
|
|
1641
1802
|
const workerFiles = this.workerFiles(task, workerId);
|
|
1642
1803
|
const critic = await this.sessions.initializeWorker(task, {
|
|
1643
1804
|
workerId,
|
|
@@ -1683,6 +1844,7 @@ export class Orchestrator {
|
|
|
1683
1844
|
outputLogPath: critic.outputLogPath,
|
|
1684
1845
|
statusPath: critic.statusPath,
|
|
1685
1846
|
prompt: await readTextIfExists(critic.promptPath),
|
|
1847
|
+
modelConfig: execution.modelConfig,
|
|
1686
1848
|
signal: input.signal
|
|
1687
1849
|
}, task, feature, featureScoped
|
|
1688
1850
|
? undefined
|
|
@@ -1741,6 +1903,7 @@ export class Orchestrator {
|
|
|
1741
1903
|
outputLogPath: critic.outputLogPath,
|
|
1742
1904
|
statusPath: critic.statusPath,
|
|
1743
1905
|
prompt: await readTextIfExists(critic.promptPath),
|
|
1906
|
+
modelConfig: await this.roleConfiguration.modelForTurn(turn.dir, "critic", engine),
|
|
1744
1907
|
signal: input.signal
|
|
1745
1908
|
});
|
|
1746
1909
|
ensureWorkerSuccess(result);
|
|
@@ -1791,6 +1954,7 @@ export class Orchestrator {
|
|
|
1791
1954
|
outputLogPath: actor.outputLogPath,
|
|
1792
1955
|
statusPath: actor.statusPath,
|
|
1793
1956
|
prompt: await readTextIfExists(actor.promptPath),
|
|
1957
|
+
modelConfig: await this.roleConfiguration.modelForTurn(turn.dir, "actor", engine),
|
|
1794
1958
|
signal: input.signal
|
|
1795
1959
|
});
|
|
1796
1960
|
ensureWorkerSuccess(result);
|
|
@@ -1896,6 +2060,7 @@ export class Orchestrator {
|
|
|
1896
2060
|
}
|
|
1897
2061
|
return adapter.run({
|
|
1898
2062
|
...spec,
|
|
2063
|
+
modelConfig: spec.modelConfig ?? workerProvider(this.config, engine).config.model,
|
|
1899
2064
|
nativeSession: existing,
|
|
1900
2065
|
nativeSessionConfig: workerProvider(this.config, engine).config.nativeSession,
|
|
1901
2066
|
onNativeSession: async (sessionId) => {
|
|
@@ -1950,35 +2115,39 @@ export class Orchestrator {
|
|
|
1950
2115
|
}
|
|
1951
2116
|
return undefined;
|
|
1952
2117
|
}
|
|
1953
|
-
async loadCompletedFeatureActor(task, engine, definition, channel) {
|
|
1954
|
-
const actor = await this.loadCompletedActor(task, engine, channel, true);
|
|
2118
|
+
async loadCompletedFeatureActor(task, engine, modelName, definition, channel) {
|
|
2119
|
+
const actor = await this.loadCompletedActor(task, engine, modelName, channel, true);
|
|
1955
2120
|
if (!actor) {
|
|
1956
2121
|
return null;
|
|
1957
2122
|
}
|
|
1958
2123
|
return { definition, channel, actor };
|
|
1959
2124
|
}
|
|
1960
|
-
async loadCompletedFeaturePair(task, engine, actorRun) {
|
|
1961
|
-
const critic = await this.loadCompletedCritic(task, engine, actorRun.channel, true);
|
|
2125
|
+
async loadCompletedFeaturePair(task, engine, modelName, actorRun) {
|
|
2126
|
+
const critic = await this.loadCompletedCritic(task, engine, modelName, actorRun.channel, true);
|
|
1962
2127
|
return critic ? { ...actorRun, critic } : null;
|
|
1963
2128
|
}
|
|
1964
|
-
async loadCompletedActor(task, engine, channel, featureScoped) {
|
|
1965
|
-
const
|
|
2129
|
+
async loadCompletedActor(task, engine, modelName, channel, featureScoped) {
|
|
2130
|
+
const execution = await this.featureWorkerExecution({ dir: join(task.dir, "turns", channel.turnId) }, "actor", engine, modelName);
|
|
2131
|
+
const actor = this.workerFiles(task, taskWorkerId("actor", engine, channel.turnId, featureScoped ? channel.id : undefined, execution.modelKey));
|
|
1966
2132
|
const status = await readWorkerStatusIfValid(actor.statusPath);
|
|
1967
2133
|
if (status?.state !== "done"
|
|
1968
2134
|
|| status.role !== "actor"
|
|
1969
2135
|
|| status.engine !== engine
|
|
2136
|
+
|| (execution.modelKey && status.model_name !== execution.modelConfig.name)
|
|
1970
2137
|
|| (featureScoped ? status.feature_id !== channel.id : Boolean(status.feature_id))) {
|
|
1971
2138
|
return null;
|
|
1972
2139
|
}
|
|
1973
2140
|
await mirrorWorkerFileToFeature(join(actor.dir, "worklog.md"), channel.actorWorklogPath);
|
|
1974
2141
|
return actor;
|
|
1975
2142
|
}
|
|
1976
|
-
async loadCompletedCritic(task, engine, channel, featureScoped) {
|
|
1977
|
-
const
|
|
2143
|
+
async loadCompletedCritic(task, engine, modelName, channel, featureScoped) {
|
|
2144
|
+
const execution = await this.featureWorkerExecution({ dir: join(task.dir, "turns", channel.turnId) }, "critic", engine, modelName);
|
|
2145
|
+
const critic = this.workerFiles(task, taskWorkerId("critic", engine, channel.turnId, featureScoped ? channel.id : undefined, execution.modelKey));
|
|
1978
2146
|
const status = await readWorkerStatusIfValid(critic.statusPath);
|
|
1979
2147
|
if (status?.state !== "done"
|
|
1980
2148
|
|| status.role !== "critic"
|
|
1981
2149
|
|| status.engine !== engine
|
|
2150
|
+
|| (execution.modelKey && status.model_name !== execution.modelConfig.name)
|
|
1982
2151
|
|| (featureScoped ? status.feature_id !== channel.id : Boolean(status.feature_id))) {
|
|
1983
2152
|
return null;
|
|
1984
2153
|
}
|
|
@@ -1988,6 +2157,17 @@ export class Orchestrator {
|
|
|
1988
2157
|
}
|
|
1989
2158
|
return await featureCriticCheckpointIsReusable(channel, decision) ? critic : null;
|
|
1990
2159
|
}
|
|
2160
|
+
async featureWorkerExecution(turn, role, engine, assignedModel) {
|
|
2161
|
+
const inherited = await this.roleConfiguration.modelForTurn(turn.dir, role, engine);
|
|
2162
|
+
const requested = assignedModel.trim();
|
|
2163
|
+
const modelConfig = requested
|
|
2164
|
+
? this.roleConfiguration.modelForTarget({ engine, model: requested })
|
|
2165
|
+
: inherited;
|
|
2166
|
+
return {
|
|
2167
|
+
modelConfig,
|
|
2168
|
+
modelKey: requested && requested !== inherited.name ? requested : ""
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
1991
2171
|
async promptTurnContext(task, turn) {
|
|
1992
2172
|
return {
|
|
1993
2173
|
turnId: turn.turnId,
|
|
@@ -2260,6 +2440,14 @@ async function readWorkerStatusIfValid(statusPath) {
|
|
|
2260
2440
|
return null;
|
|
2261
2441
|
}
|
|
2262
2442
|
}
|
|
2443
|
+
async function readFeatureStatusIfValid(statusPath) {
|
|
2444
|
+
try {
|
|
2445
|
+
return await readJson(statusPath, FeatureStatusSchema);
|
|
2446
|
+
}
|
|
2447
|
+
catch {
|
|
2448
|
+
return null;
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2263
2451
|
async function readTaskMetaIfValid(metaPath) {
|
|
2264
2452
|
if (!(await pathExists(metaPath))) {
|
|
2265
2453
|
return null;
|
|
@@ -2363,15 +2551,22 @@ function workerLabelForStatus(status) {
|
|
|
2363
2551
|
return workerLabel(status.role, status.engine);
|
|
2364
2552
|
}
|
|
2365
2553
|
const base = `${status.role}-${status.engine}`;
|
|
2366
|
-
const turnId = status.worker_id.match(new RegExp(`^${base}-(\\d{4,})
|
|
2554
|
+
const turnId = status.worker_id.match(new RegExp(`^${base}-(\\d{4,})(?:-model-[a-f0-9]{8})?$`))?.[1];
|
|
2367
2555
|
return taskWorkerLabel(status.role, status.engine, turnId ?? "0001");
|
|
2368
2556
|
}
|
|
2369
|
-
function taskWorkerId(role, engine, turnId, featureId) {
|
|
2557
|
+
function taskWorkerId(role, engine, turnId, featureId, modelKey = "") {
|
|
2370
2558
|
const base = `${role}-${engine}`;
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
return
|
|
2559
|
+
const scoped = featureId
|
|
2560
|
+
? `${base}-${featureId}`
|
|
2561
|
+
: turnId === "0001" ? base : `${base}-${turnId}`;
|
|
2562
|
+
return modelKey ? `${scoped}-model-${workerModelKey(modelKey)}` : scoped;
|
|
2563
|
+
}
|
|
2564
|
+
function workerModelKey(model) {
|
|
2565
|
+
return createHash("sha256").update(model).digest("hex").slice(0, 8);
|
|
2566
|
+
}
|
|
2567
|
+
function selectedRoleModel(selection, role, engine) {
|
|
2568
|
+
const target = selection?.[role];
|
|
2569
|
+
return target?.engine === engine ? target.model : "";
|
|
2375
2570
|
}
|
|
2376
2571
|
function capitalize(value) {
|
|
2377
2572
|
return `${value.slice(0, 1).toUpperCase()}${value.slice(1)}`;
|
|
@@ -2387,7 +2582,7 @@ function workerStageOrder(worker) {
|
|
|
2387
2582
|
function workerTurnOrder(worker) {
|
|
2388
2583
|
const featureTurn = worker.featureId?.match(/^(\d{4,})(?:-|$)/)?.[1];
|
|
2389
2584
|
const waveTurn = worker.id.match(/-wave-(\d{4,})-/)?.[1];
|
|
2390
|
-
const taskTurn = worker.id.match(/-(\d{4,})
|
|
2585
|
+
const taskTurn = worker.id.match(/-(\d{4,})(?:-model-[a-f0-9]{8})?$/)?.[1];
|
|
2391
2586
|
const parsed = Number(featureTurn ?? waveTurn ?? taskTurn ?? "1");
|
|
2392
2587
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : 1;
|
|
2393
2588
|
}
|
|
@@ -2698,3 +2893,11 @@ function judgeValidationError(turn, report) {
|
|
|
2698
2893
|
function errorMessage(error) {
|
|
2699
2894
|
return error instanceof Error ? error.message : String(error);
|
|
2700
2895
|
}
|
|
2896
|
+
function routeWithRoleSelection(route, roles) {
|
|
2897
|
+
return RouteDecisionSchema.parse({
|
|
2898
|
+
...route,
|
|
2899
|
+
judge_engine: roles.judge.engine,
|
|
2900
|
+
actor_engine: roles.actor.engine,
|
|
2901
|
+
critic_engine: roles.critic.engine
|
|
2902
|
+
});
|
|
2903
|
+
}
|