feat(reactive): graph diagnostics and subagent_model config
Add getMissingAnnotationTasks() to surface which tasks lack IO annotations and prevent parallel dispatch. Also add subagent_model to ReactiveExecutionConfig for overriding the model used by subagents during parallel task execution. - getMissingAnnotationTasks() with 4 tests - subagent_model field on ReactiveExecutionConfig type - Validation for reactive_execution.subagent_model preference
This commit is contained in:
parent
0a2c9b64c6
commit
3c7ec7a8da
4 changed files with 74 additions and 1 deletions
|
|
@ -523,7 +523,15 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|||
}
|
||||
}
|
||||
|
||||
const knownReKeys = new Set(["enabled", "max_parallel", "isolation_mode"]);
|
||||
if (re.subagent_model !== undefined) {
|
||||
if (typeof re.subagent_model === "string" && re.subagent_model.length > 0) {
|
||||
validRe.subagent_model = re.subagent_model;
|
||||
} else {
|
||||
errors.push("reactive_execution.subagent_model must be a non-empty string");
|
||||
}
|
||||
}
|
||||
|
||||
const knownReKeys = new Set(["enabled", "max_parallel", "isolation_mode", "subagent_model"]);
|
||||
for (const key of Object.keys(re)) {
|
||||
if (!knownReKeys.has(key)) {
|
||||
warnings.push(`unknown reactive_execution key "${key}" — ignored`);
|
||||
|
|
|
|||
|
|
@ -131,6 +131,24 @@ export function isGraphAmbiguous(graph: DerivedTaskNode[]): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns tasks that are missing IO annotations (no inputFiles and no outputFiles).
|
||||
* These tasks prevent parallel dispatch by making the graph ambiguous.
|
||||
* Used to surface actionable diagnostics when parallel execution falls back to sequential.
|
||||
*/
|
||||
export function getMissingAnnotationTasks(
|
||||
graph: DerivedTaskNode[],
|
||||
): Array<{ id: string; title: string }> {
|
||||
return graph
|
||||
.filter(
|
||||
(node) =>
|
||||
!node.done &&
|
||||
node.inputFiles.length === 0 &&
|
||||
node.outputFiles.length === 0,
|
||||
)
|
||||
.map((node) => ({ id: node.id, title: node.title }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect deadlock: no tasks are ready and none are in-flight, yet incomplete
|
||||
* tasks remain. This indicates a circular dependency or impossible state.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
getReadyTasks,
|
||||
chooseNonConflictingSubset,
|
||||
isGraphAmbiguous,
|
||||
getMissingAnnotationTasks,
|
||||
detectDeadlock,
|
||||
graphMetrics,
|
||||
} from "../reactive-graph.ts";
|
||||
|
|
@ -297,3 +298,47 @@ test("graphMetrics computes correct values", () => {
|
|||
assert.equal(metrics.readySetSize, 2); // T02 (T01 done) and T03 (no deps)
|
||||
assert.equal(metrics.ambiguous, false);
|
||||
});
|
||||
|
||||
// ─── getMissingAnnotationTasks ─────────────────────────────────────────────
|
||||
|
||||
test("getMissingAnnotationTasks: returns empty array when all tasks have annotations", () => {
|
||||
const graph: DerivedTaskNode[] = [
|
||||
{ id: "T01", title: "A", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false, dependsOn: [] },
|
||||
{ id: "T02", title: "B", inputFiles: [], outputFiles: ["src/c.ts"], done: false, dependsOn: [] },
|
||||
];
|
||||
assert.deepEqual(getMissingAnnotationTasks(graph), []);
|
||||
});
|
||||
|
||||
test("getMissingAnnotationTasks: returns tasks with missing annotations", () => {
|
||||
const graph: DerivedTaskNode[] = [
|
||||
{ id: "T01", title: "A", inputFiles: [], outputFiles: [], done: false, dependsOn: [] },
|
||||
{ id: "T02", title: "B", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false, dependsOn: [] },
|
||||
{ id: "T03", title: "C", inputFiles: [], outputFiles: [], done: false, dependsOn: [] },
|
||||
];
|
||||
assert.deepEqual(getMissingAnnotationTasks(graph), [
|
||||
{ id: "T01", title: "A" },
|
||||
{ id: "T03", title: "C" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("getMissingAnnotationTasks: skips done tasks", () => {
|
||||
const graph: DerivedTaskNode[] = [
|
||||
{ id: "T01", title: "A", inputFiles: [], outputFiles: [], done: true, dependsOn: [] },
|
||||
{ id: "T02", title: "B", inputFiles: [], outputFiles: [], done: false, dependsOn: [] },
|
||||
];
|
||||
assert.deepEqual(getMissingAnnotationTasks(graph), [
|
||||
{ id: "T02", title: "B" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("getMissingAnnotationTasks: returns only tasks missing BOTH inputFiles and outputFiles", () => {
|
||||
const graph: DerivedTaskNode[] = [
|
||||
{ id: "T01", title: "InputOnly", inputFiles: ["src/a.ts"], outputFiles: [], done: false, dependsOn: [] },
|
||||
{ id: "T02", title: "OutputOnly", inputFiles: [], outputFiles: ["src/b.ts"], done: false, dependsOn: [] },
|
||||
{ id: "T03", title: "Neither", inputFiles: [], outputFiles: [], done: false, dependsOn: [] },
|
||||
{ id: "T04", title: "Both", inputFiles: ["src/c.ts"], outputFiles: ["src/d.ts"], done: false, dependsOn: [] },
|
||||
];
|
||||
assert.deepEqual(getMissingAnnotationTasks(graph), [
|
||||
{ id: "T03", title: "Neither" },
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -476,6 +476,8 @@ export interface ReactiveExecutionConfig {
|
|||
max_parallel: number;
|
||||
/** Isolation mode for parallel tasks within a slice. Currently only "same-tree" is supported. */
|
||||
isolation_mode: "same-tree";
|
||||
/** Optional model override for subagents spawned during parallel execution. */
|
||||
subagent_model?: string;
|
||||
}
|
||||
|
||||
/** Per-slice reactive execution runtime state, persisted to disk. */
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue