parallel-codex-tui 0.2.10 → 0.3.0
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 +21 -6
- package/dist/bootstrap.js +8 -1
- package/dist/cli.js +10 -1
- package/dist/core/role-configuration.js +238 -0
- package/dist/orchestrator/orchestrator.js +135 -11
- package/dist/tui/App.js +397 -4
- package/dist/tui/AppShell.js +7 -1
- package/dist/tui/InputBar.js +27 -3
- package/dist/tui/RoleConfigurationView.js +96 -0
- package/dist/tui/StatusDetailView.js +9 -6
- 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
|
@@ -5,6 +5,7 @@ import { runWithLeaseFinalization } from "../core/lease-finalization.js";
|
|
|
5
5
|
import { extractMainResponse } from "../core/main-response.js";
|
|
6
6
|
import { claimTaskRunLease, TaskRunLeaseConflictError } from "../core/process-ownership.js";
|
|
7
7
|
import { routerRuntimeDir } from "../core/paths.js";
|
|
8
|
+
import { RoleConfigurationManager, roleSelectionWithEngines } from "../core/role-configuration.js";
|
|
8
9
|
import { classifyRouterFailure, routerFallbackIsTransient } from "../core/router-audit.js";
|
|
9
10
|
import { sanitizeRouterText } from "../core/router-redaction.js";
|
|
10
11
|
import { routeRequestWithCodex, routerCommandLabel, routerProxyContext } from "../core/router.js";
|
|
@@ -59,6 +60,7 @@ export class Orchestrator {
|
|
|
59
60
|
routerConfigLoader;
|
|
60
61
|
dependencies;
|
|
61
62
|
activeFeatureRuns = new Map();
|
|
63
|
+
roleConfiguration;
|
|
62
64
|
constructor(config, sessions, workers, routeRunner, routerCwd = routerRuntimeDir(config.projectRoot, config.dataDir), routerConfigLoader, dependencies = {}) {
|
|
63
65
|
this.config = config;
|
|
64
66
|
this.sessions = sessions;
|
|
@@ -67,17 +69,21 @@ export class Orchestrator {
|
|
|
67
69
|
this.routerCwd = routerCwd;
|
|
68
70
|
this.routerConfigLoader = routerConfigLoader;
|
|
69
71
|
this.dependencies = dependencies;
|
|
72
|
+
this.roleConfiguration = dependencies.roleConfiguration ?? RoleConfigurationManager.transient(config);
|
|
70
73
|
}
|
|
71
74
|
async handleRequest(input) {
|
|
72
75
|
throwIfCancelled(input.signal);
|
|
73
|
-
const
|
|
76
|
+
const routed = await this.routeRequest(input.request, input.cwd, input.signal, "initial", input.onRouteStart, input.onRouteFallback, input.onRouteProgress);
|
|
77
|
+
const roleSelection = input.roleSelection ?? await this.roleConfiguration.selectionForRequest();
|
|
78
|
+
const route = routeWithRoleSelection(routed, roleSelection);
|
|
74
79
|
input.onRoute?.(route);
|
|
75
80
|
throwIfCancelled(input.signal);
|
|
76
81
|
const workers = [];
|
|
82
|
+
const executionInput = { ...input, roleSelection };
|
|
77
83
|
if (route.mode === "simple") {
|
|
78
84
|
try {
|
|
79
85
|
input.onStatus?.({ taskId: "main", main: "starting" });
|
|
80
|
-
const output = await this.runMain(
|
|
86
|
+
const output = await this.runMain(executionInput, workers);
|
|
81
87
|
input.onStatus?.({ taskId: "main", main: "done" });
|
|
82
88
|
return {
|
|
83
89
|
mode: "simple",
|
|
@@ -104,18 +110,20 @@ export class Orchestrator {
|
|
|
104
110
|
userPath: join(task.dir, "turns", "0001", "user.md"),
|
|
105
111
|
routePath: join(task.dir, "turns", "0001", "route.json")
|
|
106
112
|
};
|
|
107
|
-
|
|
113
|
+
await this.roleConfiguration.writeTurnSelection(turn.dir, roleSelectionWithEngines(route, roleSelection));
|
|
114
|
+
return this.withTaskRunLease(task, () => this.runInitialTask(executionInput, task, route, turn, workers));
|
|
108
115
|
}
|
|
109
116
|
async handleTaskTurn(input) {
|
|
110
117
|
throwIfCancelled(input.signal);
|
|
111
118
|
const task = this.sessions.taskFromId(input.taskId);
|
|
112
|
-
const
|
|
119
|
+
const roleSelection = input.roleSelection ?? await this.roleConfiguration.selectionForRequest(task.dir);
|
|
120
|
+
const route = routeWithRoleSelection(input.route ?? await this.routeRequest(input.request, input.cwd, input.signal, "follow-up", input.onRouteStart, input.onRouteFallback, input.onRouteProgress), roleSelection);
|
|
113
121
|
if (!input.route) {
|
|
114
122
|
input.onRoute?.(route);
|
|
115
123
|
}
|
|
116
124
|
throwIfCancelled(input.signal);
|
|
117
125
|
if (route.mode === "simple") {
|
|
118
|
-
return this.answerTaskQuestion({ ...input, route });
|
|
126
|
+
return this.answerTaskQuestion({ ...input, route, roleSelection });
|
|
119
127
|
}
|
|
120
128
|
return this.withTaskRunLease(task, async () => {
|
|
121
129
|
throwIfCancelled(input.signal);
|
|
@@ -124,8 +132,9 @@ export class Orchestrator {
|
|
|
124
132
|
request: input.request,
|
|
125
133
|
route
|
|
126
134
|
});
|
|
135
|
+
await this.roleConfiguration.writeTurnSelection(turn.dir, roleSelectionWithEngines(route, roleSelection));
|
|
127
136
|
const workers = [];
|
|
128
|
-
return this.runPairTask(input, task, route, turn, workers);
|
|
137
|
+
return this.runPairTask({ ...input, roleSelection }, task, route, turn, workers);
|
|
129
138
|
});
|
|
130
139
|
}
|
|
131
140
|
async retryTask(input) {
|
|
@@ -189,6 +198,84 @@ export class Orchestrator {
|
|
|
189
198
|
return { featureId: input.featureId, assignment };
|
|
190
199
|
});
|
|
191
200
|
}
|
|
201
|
+
async roleConfigurationSnapshot(taskId) {
|
|
202
|
+
const task = taskId ? this.sessions.taskFromId(taskId) : null;
|
|
203
|
+
if (task && !(await pathExists(task.dir))) {
|
|
204
|
+
throw new Error(`Task session not found: ${taskId}`);
|
|
205
|
+
}
|
|
206
|
+
return this.roleConfiguration.snapshot(task?.dir);
|
|
207
|
+
}
|
|
208
|
+
async updateRoleConfiguration(input) {
|
|
209
|
+
if (input.scope !== "task") {
|
|
210
|
+
await this.roleConfiguration.apply(input.scope, input.roles);
|
|
211
|
+
return this.roleConfigurationSnapshot(input.taskId);
|
|
212
|
+
}
|
|
213
|
+
const taskId = input.taskId;
|
|
214
|
+
if (!taskId) {
|
|
215
|
+
throw new Error("No active Task is available for a current-task role configuration.");
|
|
216
|
+
}
|
|
217
|
+
const task = this.sessions.taskFromId(taskId);
|
|
218
|
+
if (!(await pathExists(task.dir))) {
|
|
219
|
+
throw new Error(`Task session not found: ${taskId}`);
|
|
220
|
+
}
|
|
221
|
+
return this.withTaskRunLease(task, async () => {
|
|
222
|
+
await this.roleConfiguration.apply("task", input.roles, task.dir);
|
|
223
|
+
await this.synchronizeRetryRoleConfiguration(task, input.roles);
|
|
224
|
+
await this.sessions.appendEvent(task, "task.role_configuration_changed", "Current Task role configuration updated");
|
|
225
|
+
return this.roleConfiguration.snapshot(task.dir);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
async clearRoleConfiguration(scope, taskId) {
|
|
229
|
+
if (scope !== "task") {
|
|
230
|
+
await this.roleConfiguration.clear(scope);
|
|
231
|
+
return this.roleConfigurationSnapshot(taskId);
|
|
232
|
+
}
|
|
233
|
+
if (!taskId) {
|
|
234
|
+
throw new Error("No active Task is available for a current-task role configuration.");
|
|
235
|
+
}
|
|
236
|
+
const task = this.sessions.taskFromId(taskId);
|
|
237
|
+
if (!(await pathExists(task.dir))) {
|
|
238
|
+
throw new Error(`Task session not found: ${taskId}`);
|
|
239
|
+
}
|
|
240
|
+
return this.withTaskRunLease(task, async () => {
|
|
241
|
+
await this.roleConfiguration.clear("task", task.dir);
|
|
242
|
+
const roles = await this.roleConfiguration.selectionForTask(task.dir);
|
|
243
|
+
await this.synchronizeRetryRoleConfiguration(task, roles);
|
|
244
|
+
await this.sessions.appendEvent(task, "task.role_configuration_cleared", "Current Task role configuration reset");
|
|
245
|
+
return this.roleConfiguration.snapshot(task.dir);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
async synchronizeRetryRoleConfiguration(task, roles) {
|
|
249
|
+
const meta = await readTaskMetaIfValid(task.metaPath);
|
|
250
|
+
if (!meta || !new Set(["failed", "cancelled", "paused"]).has(meta.status)) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const turn = await this.sessions.latestTurn(task);
|
|
254
|
+
if (!turn) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const route = routeWithRoleSelection(await readJson(turn.routePath, RouteDecisionSchema), roles);
|
|
258
|
+
const executionRoles = roleSelectionWithEngines(route, roles);
|
|
259
|
+
await writeJson(turn.routePath, route);
|
|
260
|
+
await this.sessions.recordLatestRoute(task, route);
|
|
261
|
+
await this.roleConfiguration.writeTurnSelection(turn.dir, executionRoles);
|
|
262
|
+
const featuresDir = join(task.dir, "features");
|
|
263
|
+
if (!(await pathExists(featuresDir))) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const entries = await readdir(featuresDir, { withFileTypes: true });
|
|
267
|
+
for (const entry of entries) {
|
|
268
|
+
if (!entry.isDirectory()) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const featureDir = join(featuresDir, entry.name);
|
|
272
|
+
const status = await readFeatureStatusIfValid(join(featureDir, "status.json"));
|
|
273
|
+
if (!status || status.task_id !== task.id || status.state === "approved") {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
await writeFeatureAssignment({ assignmentPath: join(featureDir, "assignment.json") }, executionRoles.actor.engine, executionRoles.critic.engine);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
192
279
|
async resumeTaskRun(input, pausedFeatureId) {
|
|
193
280
|
throwIfCancelled(input.signal);
|
|
194
281
|
const task = this.sessions.taskFromId(input.taskId);
|
|
@@ -226,11 +313,15 @@ export class Orchestrator {
|
|
|
226
313
|
throw new Error(`Task ${input.taskId} turn ${turn.turnId} has no request to retry.`);
|
|
227
314
|
}
|
|
228
315
|
const route = await readJson(turn.routePath, RouteDecisionSchema);
|
|
316
|
+
const roleSelection = await this.roleConfiguration.readTurnSelection(turn.dir)
|
|
317
|
+
?? roleSelectionWithEngines(route, await this.roleConfiguration.selectionForTask(task.dir));
|
|
318
|
+
await this.roleConfiguration.writeTurnSelection(turn.dir, roleSelection);
|
|
229
319
|
const executionInput = {
|
|
230
320
|
...input,
|
|
231
321
|
request,
|
|
232
322
|
cwd: meta.cwd,
|
|
233
|
-
retry: true
|
|
323
|
+
retry: true,
|
|
324
|
+
roleSelection
|
|
234
325
|
};
|
|
235
326
|
const workers = [];
|
|
236
327
|
input.onRoute?.(route);
|
|
@@ -311,14 +402,18 @@ export class Orchestrator {
|
|
|
311
402
|
}
|
|
312
403
|
async routeTaskFollowUp(input) {
|
|
313
404
|
throwIfCancelled(input.signal);
|
|
314
|
-
const
|
|
405
|
+
const routed = await this.routeRequest(input.request, input.cwd, input.signal, "follow-up", input.onRouteStart, input.onRouteFallback, input.onRouteProgress);
|
|
406
|
+
const task = this.sessions.taskFromId(input.taskId);
|
|
407
|
+
const roleSelection = input.roleSelection ?? await this.roleConfiguration.selectionForRequest(task.dir);
|
|
408
|
+
const route = routeWithRoleSelection(routed, roleSelection);
|
|
315
409
|
input.onRoute?.(route);
|
|
316
410
|
throwIfCancelled(input.signal);
|
|
317
411
|
return {
|
|
318
412
|
mode: route.mode,
|
|
319
413
|
taskId: route.mode === "complex" ? input.taskId : null,
|
|
320
414
|
reason: route.reason,
|
|
321
|
-
route
|
|
415
|
+
route,
|
|
416
|
+
roleSelection
|
|
322
417
|
};
|
|
323
418
|
}
|
|
324
419
|
async answerTaskQuestion(input) {
|
|
@@ -327,7 +422,11 @@ export class Orchestrator {
|
|
|
327
422
|
if (!(await pathExists(task.dir))) {
|
|
328
423
|
throw new Error(`Task session not found: ${input.taskId}`);
|
|
329
424
|
}
|
|
330
|
-
|
|
425
|
+
const roleSelection = input.roleSelection ?? await this.roleConfiguration.selectionForRequest(task.dir);
|
|
426
|
+
return this.withTaskRunLease(task, () => this.answerTaskQuestionWithLease({
|
|
427
|
+
...input,
|
|
428
|
+
roleSelection
|
|
429
|
+
}, task));
|
|
331
430
|
}
|
|
332
431
|
async answerTaskQuestionWithLease(input, task) {
|
|
333
432
|
throwIfCancelled(input.signal);
|
|
@@ -1357,7 +1456,8 @@ export class Orchestrator {
|
|
|
1357
1456
|
}
|
|
1358
1457
|
async runMain(input, workers, context, taskId = null) {
|
|
1359
1458
|
throwIfCancelled(input.signal);
|
|
1360
|
-
const
|
|
1459
|
+
const target = input.roleSelection?.main ?? this.roleConfiguration.futureRoles().main;
|
|
1460
|
+
const engine = target.engine;
|
|
1361
1461
|
const dir = this.sessions.mainSessionDir();
|
|
1362
1462
|
let lease;
|
|
1363
1463
|
try {
|
|
@@ -1427,6 +1527,7 @@ export class Orchestrator {
|
|
|
1427
1527
|
outputLogPath,
|
|
1428
1528
|
statusPath,
|
|
1429
1529
|
prompt,
|
|
1530
|
+
modelConfig: this.roleConfiguration.modelForTarget(input.roleSelection?.main ?? this.roleConfiguration.futureRoles().main),
|
|
1430
1531
|
signal: input.signal,
|
|
1431
1532
|
onStatus: (runtimeStatus) => {
|
|
1432
1533
|
this.recordWorker(input, workers, { ...worker, runtimeStatus });
|
|
@@ -1473,6 +1574,7 @@ export class Orchestrator {
|
|
|
1473
1574
|
outputLogPath: judge.outputLogPath,
|
|
1474
1575
|
statusPath: judge.statusPath,
|
|
1475
1576
|
prompt: await readTextIfExists(judge.promptPath),
|
|
1577
|
+
modelConfig: await this.roleConfiguration.modelForTurn(turn.dir, "judge", engine),
|
|
1476
1578
|
signal: input.signal
|
|
1477
1579
|
}, "task", await this.previousTurnWorker(task, "judge", engine, turn.turnId));
|
|
1478
1580
|
ensureWorkerSuccess(result);
|
|
@@ -1548,6 +1650,7 @@ export class Orchestrator {
|
|
|
1548
1650
|
outputLogPath: finalJudge.outputLogPath,
|
|
1549
1651
|
statusPath: finalJudge.statusPath,
|
|
1550
1652
|
prompt: await readTextIfExists(finalJudge.promptPath),
|
|
1653
|
+
modelConfig: await this.roleConfiguration.modelForTurn(turn.dir, "judge", engine),
|
|
1551
1654
|
signal: input.signal
|
|
1552
1655
|
}, "task", initialJudge);
|
|
1553
1656
|
ensureWorkerSuccess(result);
|
|
@@ -1623,6 +1726,7 @@ export class Orchestrator {
|
|
|
1623
1726
|
outputLogPath: actor.outputLogPath,
|
|
1624
1727
|
statusPath: actor.statusPath,
|
|
1625
1728
|
prompt: await readTextIfExists(actor.promptPath),
|
|
1729
|
+
modelConfig: await this.roleConfiguration.modelForTurn(turn.dir, "actor", engine),
|
|
1626
1730
|
signal: input.signal
|
|
1627
1731
|
}, task, feature, featureScoped
|
|
1628
1732
|
? undefined
|
|
@@ -1683,6 +1787,7 @@ export class Orchestrator {
|
|
|
1683
1787
|
outputLogPath: critic.outputLogPath,
|
|
1684
1788
|
statusPath: critic.statusPath,
|
|
1685
1789
|
prompt: await readTextIfExists(critic.promptPath),
|
|
1790
|
+
modelConfig: await this.roleConfiguration.modelForTurn(turn.dir, "critic", engine),
|
|
1686
1791
|
signal: input.signal
|
|
1687
1792
|
}, task, feature, featureScoped
|
|
1688
1793
|
? undefined
|
|
@@ -1741,6 +1846,7 @@ export class Orchestrator {
|
|
|
1741
1846
|
outputLogPath: critic.outputLogPath,
|
|
1742
1847
|
statusPath: critic.statusPath,
|
|
1743
1848
|
prompt: await readTextIfExists(critic.promptPath),
|
|
1849
|
+
modelConfig: await this.roleConfiguration.modelForTurn(turn.dir, "critic", engine),
|
|
1744
1850
|
signal: input.signal
|
|
1745
1851
|
});
|
|
1746
1852
|
ensureWorkerSuccess(result);
|
|
@@ -1791,6 +1897,7 @@ export class Orchestrator {
|
|
|
1791
1897
|
outputLogPath: actor.outputLogPath,
|
|
1792
1898
|
statusPath: actor.statusPath,
|
|
1793
1899
|
prompt: await readTextIfExists(actor.promptPath),
|
|
1900
|
+
modelConfig: await this.roleConfiguration.modelForTurn(turn.dir, "actor", engine),
|
|
1794
1901
|
signal: input.signal
|
|
1795
1902
|
});
|
|
1796
1903
|
ensureWorkerSuccess(result);
|
|
@@ -1896,6 +2003,7 @@ export class Orchestrator {
|
|
|
1896
2003
|
}
|
|
1897
2004
|
return adapter.run({
|
|
1898
2005
|
...spec,
|
|
2006
|
+
modelConfig: spec.modelConfig ?? workerProvider(this.config, engine).config.model,
|
|
1899
2007
|
nativeSession: existing,
|
|
1900
2008
|
nativeSessionConfig: workerProvider(this.config, engine).config.nativeSession,
|
|
1901
2009
|
onNativeSession: async (sessionId) => {
|
|
@@ -2260,6 +2368,14 @@ async function readWorkerStatusIfValid(statusPath) {
|
|
|
2260
2368
|
return null;
|
|
2261
2369
|
}
|
|
2262
2370
|
}
|
|
2371
|
+
async function readFeatureStatusIfValid(statusPath) {
|
|
2372
|
+
try {
|
|
2373
|
+
return await readJson(statusPath, FeatureStatusSchema);
|
|
2374
|
+
}
|
|
2375
|
+
catch {
|
|
2376
|
+
return null;
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2263
2379
|
async function readTaskMetaIfValid(metaPath) {
|
|
2264
2380
|
if (!(await pathExists(metaPath))) {
|
|
2265
2381
|
return null;
|
|
@@ -2698,3 +2814,11 @@ function judgeValidationError(turn, report) {
|
|
|
2698
2814
|
function errorMessage(error) {
|
|
2699
2815
|
return error instanceof Error ? error.message : String(error);
|
|
2700
2816
|
}
|
|
2817
|
+
function routeWithRoleSelection(route, roles) {
|
|
2818
|
+
return RouteDecisionSchema.parse({
|
|
2819
|
+
...route,
|
|
2820
|
+
judge_engine: roles.judge.engine,
|
|
2821
|
+
actor_engine: roles.actor.engine,
|
|
2822
|
+
critic_engine: roles.critic.engine
|
|
2823
|
+
});
|
|
2824
|
+
}
|