substrate-ai 0.18.1 → 0.19.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.
|
@@ -6671,6 +6671,43 @@ function defaultFailResult(error, tokenUsage) {
|
|
|
6671
6671
|
};
|
|
6672
6672
|
}
|
|
6673
6673
|
/**
|
|
6674
|
+
* Count test cases (`it(`, `test(`, `it.each(`, `test.each(`) in modified
|
|
6675
|
+
* test files. Returns a structured summary the reviewer can use as ground
|
|
6676
|
+
* truth instead of manually estimating test counts from code inspection.
|
|
6677
|
+
*/
|
|
6678
|
+
async function countTestMetrics(filesModified, cwd) {
|
|
6679
|
+
if (!filesModified || filesModified.length === 0) return "";
|
|
6680
|
+
const testFiles = filesModified.filter((f$1) => f$1.includes(".test.") || f$1.includes(".spec.") || f$1.includes("__tests__"));
|
|
6681
|
+
if (testFiles.length === 0) return "";
|
|
6682
|
+
const results = [];
|
|
6683
|
+
let totalCount = 0;
|
|
6684
|
+
for (const file of testFiles) try {
|
|
6685
|
+
const out = execSync(`grep -cE "^\\s*(it|test|it\\.each|test\\.each)\\s*\\(" "${file}" 2>/dev/null || echo 0`, {
|
|
6686
|
+
cwd,
|
|
6687
|
+
encoding: "utf-8",
|
|
6688
|
+
timeout: 5e3
|
|
6689
|
+
}).trim();
|
|
6690
|
+
const count = parseInt(out, 10) || 0;
|
|
6691
|
+
if (count > 0) {
|
|
6692
|
+
results.push({
|
|
6693
|
+
file: file.split("/").pop(),
|
|
6694
|
+
count
|
|
6695
|
+
});
|
|
6696
|
+
totalCount += count;
|
|
6697
|
+
}
|
|
6698
|
+
} catch {}
|
|
6699
|
+
if (totalCount === 0) return "";
|
|
6700
|
+
const lines = [
|
|
6701
|
+
`VERIFIED TEST COUNT (automated — use as ground truth):`,
|
|
6702
|
+
`Total test cases: ${totalCount} across ${results.length} test file(s)`,
|
|
6703
|
+
...results.map((r) => ` ${r.file}: ${r.count} test(s)`),
|
|
6704
|
+
"",
|
|
6705
|
+
"IMPORTANT: Use this verified count when evaluating AC test coverage thresholds.",
|
|
6706
|
+
"Do NOT manually estimate test counts — use the numbers above."
|
|
6707
|
+
];
|
|
6708
|
+
return lines.join("\n");
|
|
6709
|
+
}
|
|
6710
|
+
/**
|
|
6674
6711
|
* Execute the compiled code-review workflow.
|
|
6675
6712
|
*
|
|
6676
6713
|
* Steps:
|
|
@@ -6808,6 +6845,8 @@ async function runCodeReview(deps, params) {
|
|
|
6808
6845
|
}, "Injecting prior findings into code-review prompt");
|
|
6809
6846
|
}
|
|
6810
6847
|
} catch {}
|
|
6848
|
+
const testMetricsContent = await countTestMetrics(filesModified, cwd);
|
|
6849
|
+
if (testMetricsContent) logger$12.debug({ storyKey }, "Injecting verified test-count metrics into code-review context");
|
|
6811
6850
|
const sections = [
|
|
6812
6851
|
{
|
|
6813
6852
|
name: "story_content",
|
|
@@ -6819,6 +6858,11 @@ async function runCodeReview(deps, params) {
|
|
|
6819
6858
|
content: gitDiffContent,
|
|
6820
6859
|
priority: "important"
|
|
6821
6860
|
},
|
|
6861
|
+
{
|
|
6862
|
+
name: "test_metrics",
|
|
6863
|
+
content: testMetricsContent,
|
|
6864
|
+
priority: "important"
|
|
6865
|
+
},
|
|
6822
6866
|
{
|
|
6823
6867
|
name: "previous_findings",
|
|
6824
6868
|
content: previousFindingsContent,
|
|
@@ -9715,6 +9759,52 @@ function extractExpectedStoryTitle(shardContent, storyKey) {
|
|
|
9715
9759
|
}
|
|
9716
9760
|
return null;
|
|
9717
9761
|
}
|
|
9762
|
+
/**
|
|
9763
|
+
* Check whether a story's expected NEW files already exist in the working tree,
|
|
9764
|
+
* indicating the story was implicitly implemented by adjacent stories.
|
|
9765
|
+
*
|
|
9766
|
+
* Parses the consolidated epics document for the story's "Files likely touched"
|
|
9767
|
+
* section and checks for files marked as "(new)". If all expected new files
|
|
9768
|
+
* already exist, the story is considered implicitly covered.
|
|
9769
|
+
*
|
|
9770
|
+
* Returns `true` if the story appears already covered, `false` otherwise.
|
|
9771
|
+
*/
|
|
9772
|
+
function isImplicitlyCovered(storyKey, projectRoot) {
|
|
9773
|
+
const planningDir = join$1(projectRoot, "_bmad-output", "planning-artifacts");
|
|
9774
|
+
if (!existsSync(planningDir)) return false;
|
|
9775
|
+
let epicsPath;
|
|
9776
|
+
try {
|
|
9777
|
+
const entries = readdirSync(planningDir, { encoding: "utf-8" });
|
|
9778
|
+
const match$1 = entries.find((e) => /^epics[-.].*\.md$/i.test(e) && !/^epic-\d+/.test(e));
|
|
9779
|
+
if (match$1) epicsPath = join$1(planningDir, match$1);
|
|
9780
|
+
} catch {
|
|
9781
|
+
return false;
|
|
9782
|
+
}
|
|
9783
|
+
if (!epicsPath) return false;
|
|
9784
|
+
let content;
|
|
9785
|
+
try {
|
|
9786
|
+
content = readFileSync(epicsPath, "utf-8");
|
|
9787
|
+
} catch {
|
|
9788
|
+
return false;
|
|
9789
|
+
}
|
|
9790
|
+
const escapedKey = storyKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9791
|
+
const storyHeading = new RegExp(`^###\\s+Story\\s+${escapedKey}[:\\s]`, "m");
|
|
9792
|
+
const headingMatch = storyHeading.exec(content);
|
|
9793
|
+
if (!headingMatch) return false;
|
|
9794
|
+
const sectionStart = headingMatch.index;
|
|
9795
|
+
const nextHeading = content.indexOf("\n### Story ", sectionStart + 1);
|
|
9796
|
+
const section = nextHeading > 0 ? content.slice(sectionStart, nextHeading) : content.slice(sectionStart);
|
|
9797
|
+
const filesIdx = section.indexOf("Files likely touched:");
|
|
9798
|
+
if (filesIdx < 0) return false;
|
|
9799
|
+
const filesBlock = section.slice(filesIdx);
|
|
9800
|
+
const newFilePattern = /^-\s*`([^`]+)`\s*\(new\)/gm;
|
|
9801
|
+
const expectedNewFiles = [];
|
|
9802
|
+
let fm;
|
|
9803
|
+
while ((fm = newFilePattern.exec(filesBlock)) !== null) if (fm[1]) expectedNewFiles.push(fm[1]);
|
|
9804
|
+
if (expectedNewFiles.length === 0) return false;
|
|
9805
|
+
const existCount = expectedNewFiles.filter((f$1) => existsSync(join$1(projectRoot, f$1))).length;
|
|
9806
|
+
return existCount === expectedNewFiles.length;
|
|
9807
|
+
}
|
|
9718
9808
|
const TITLE_OVERLAP_WARNING_THRESHOLD = .3;
|
|
9719
9809
|
/**
|
|
9720
9810
|
* Map a StoryPhase to the corresponding WgStoryStatus for wg_stories writes.
|
|
@@ -10318,6 +10408,25 @@ function createImplementationOrchestrator(deps) {
|
|
|
10318
10408
|
}
|
|
10319
10409
|
}
|
|
10320
10410
|
} catch {}
|
|
10411
|
+
if (storyFilePath === void 0 && projectRoot && isImplicitlyCovered(storyKey, projectRoot)) {
|
|
10412
|
+
logger$21.info({ storyKey }, `Story ${storyKey} appears implicitly covered — all expected new files already exist. Skipping create-story.`);
|
|
10413
|
+
endPhase(storyKey, "create-story");
|
|
10414
|
+
eventBus.emit("orchestrator:story-phase-complete", {
|
|
10415
|
+
storyKey,
|
|
10416
|
+
phase: "IN_STORY_CREATION",
|
|
10417
|
+
result: {
|
|
10418
|
+
result: "success",
|
|
10419
|
+
story_key: storyKey,
|
|
10420
|
+
implicitlyCovered: true
|
|
10421
|
+
}
|
|
10422
|
+
});
|
|
10423
|
+
updateStory(storyKey, {
|
|
10424
|
+
phase: "COMPLETE",
|
|
10425
|
+
completedAt: new Date().toISOString()
|
|
10426
|
+
});
|
|
10427
|
+
await persistState();
|
|
10428
|
+
return;
|
|
10429
|
+
}
|
|
10321
10430
|
if (storyFilePath === void 0) try {
|
|
10322
10431
|
incrementDispatches(storyKey);
|
|
10323
10432
|
const createResult = await runCreateStory({
|
|
@@ -12259,7 +12368,7 @@ function createImplementationOrchestrator(deps) {
|
|
|
12259
12368
|
* @returns Sorted, deduplicated array of story keys in "N-M" format
|
|
12260
12369
|
*/
|
|
12261
12370
|
async function resolveStoryKeys(db, projectRoot, opts) {
|
|
12262
|
-
if (opts?.explicit !== void 0 && opts.explicit.length > 0) return opts.explicit;
|
|
12371
|
+
if (opts?.explicit !== void 0 && opts.explicit.length > 0) return topologicalSortByDependencies(opts.explicit, projectRoot);
|
|
12263
12372
|
let keys = [];
|
|
12264
12373
|
const readyKeys = await db.queryReadyStories();
|
|
12265
12374
|
if (readyKeys.length > 0) {
|
|
@@ -12406,6 +12515,12 @@ function findEpicsFile(projectRoot) {
|
|
|
12406
12515
|
const fullPath = join$1(projectRoot, candidate);
|
|
12407
12516
|
if (existsSync(fullPath)) return fullPath;
|
|
12408
12517
|
}
|
|
12518
|
+
const planningDir = join$1(projectRoot, "_bmad-output", "planning-artifacts");
|
|
12519
|
+
if (existsSync(planningDir)) try {
|
|
12520
|
+
const entries = readdirSync(planningDir, { encoding: "utf-8" });
|
|
12521
|
+
const match$1 = entries.filter((e) => /^epics[-.].*\.md$/i.test(e) && !/^epic-\d+/.test(e)).sort();
|
|
12522
|
+
if (match$1.length > 0) return join$1(planningDir, match$1[0]);
|
|
12523
|
+
} catch {}
|
|
12409
12524
|
return void 0;
|
|
12410
12525
|
}
|
|
12411
12526
|
/**
|
|
@@ -12512,6 +12627,112 @@ function sortStoryKeys(keys) {
|
|
|
12512
12627
|
return a.localeCompare(b);
|
|
12513
12628
|
});
|
|
12514
12629
|
}
|
|
12630
|
+
/**
|
|
12631
|
+
* Parse inter-story dependencies from the consolidated epics document.
|
|
12632
|
+
*
|
|
12633
|
+
* Scans for patterns like:
|
|
12634
|
+
* ### Story 50-2: Title
|
|
12635
|
+
* **Dependencies:** 50-1
|
|
12636
|
+
*
|
|
12637
|
+
* Returns a Map where key=storyKey, value=Set of dependency keys.
|
|
12638
|
+
* Only returns dependencies that are within the provided storyKeys set
|
|
12639
|
+
* (external dependencies to other epics are ignored for ordering purposes).
|
|
12640
|
+
*/
|
|
12641
|
+
function parseEpicsDependencies(projectRoot, storyKeys) {
|
|
12642
|
+
const deps = new Map();
|
|
12643
|
+
const epicsPath = findEpicsFile(projectRoot);
|
|
12644
|
+
if (epicsPath === void 0) return deps;
|
|
12645
|
+
let content;
|
|
12646
|
+
try {
|
|
12647
|
+
content = readFileSync(epicsPath, "utf-8");
|
|
12648
|
+
} catch {
|
|
12649
|
+
return deps;
|
|
12650
|
+
}
|
|
12651
|
+
const storyPattern = /^###\s+Story\s+(\d+)-(\d+)[:\s]/gm;
|
|
12652
|
+
const depPattern = /^\*\*Dependencies:\*\*\s*(.+)$/gm;
|
|
12653
|
+
const storyPositions = [];
|
|
12654
|
+
let match$1;
|
|
12655
|
+
while ((match$1 = storyPattern.exec(content)) !== null) storyPositions.push({
|
|
12656
|
+
key: `${match$1[1]}-${match$1[2]}`,
|
|
12657
|
+
pos: match$1.index
|
|
12658
|
+
});
|
|
12659
|
+
for (let i = 0; i < storyPositions.length; i++) {
|
|
12660
|
+
const story = storyPositions[i];
|
|
12661
|
+
const nextStoryPos = i + 1 < storyPositions.length ? storyPositions[i + 1].pos : content.length;
|
|
12662
|
+
const section = content.slice(story.pos, nextStoryPos);
|
|
12663
|
+
depPattern.lastIndex = 0;
|
|
12664
|
+
const depMatch = depPattern.exec(section);
|
|
12665
|
+
if (depMatch === null || /^none$/i.test(depMatch[1].trim())) continue;
|
|
12666
|
+
const depText = depMatch[1];
|
|
12667
|
+
const storyDeps = new Set();
|
|
12668
|
+
const rangeMatch = /(\d+)-(\d+)\s+through\s+\1-(\d+)/i.exec(depText);
|
|
12669
|
+
if (rangeMatch !== null) {
|
|
12670
|
+
const epic = rangeMatch[1];
|
|
12671
|
+
const start = Number(rangeMatch[2]);
|
|
12672
|
+
const end = Number(rangeMatch[3]);
|
|
12673
|
+
for (let n$1 = start; n$1 <= end; n$1++) {
|
|
12674
|
+
const depKey = `${epic}-${n$1}`;
|
|
12675
|
+
if (storyKeys.has(depKey)) storyDeps.add(depKey);
|
|
12676
|
+
}
|
|
12677
|
+
} else {
|
|
12678
|
+
const keyPattern = /(\d+-\d+[a-z]?)/g;
|
|
12679
|
+
let km;
|
|
12680
|
+
while ((km = keyPattern.exec(depText)) !== null) {
|
|
12681
|
+
const depKey = km[1];
|
|
12682
|
+
if (storyKeys.has(depKey)) storyDeps.add(depKey);
|
|
12683
|
+
}
|
|
12684
|
+
}
|
|
12685
|
+
if (storyDeps.size > 0) deps.set(story.key, storyDeps);
|
|
12686
|
+
}
|
|
12687
|
+
return deps;
|
|
12688
|
+
}
|
|
12689
|
+
/**
|
|
12690
|
+
* Topologically sort explicit story keys by inter-story dependencies.
|
|
12691
|
+
*
|
|
12692
|
+
* Parses the consolidated epics document for dependency metadata, builds
|
|
12693
|
+
* a DAG, and returns keys in dependency-first order using Kahn's algorithm.
|
|
12694
|
+
* Stories with no dependencies come first; stories that depend on others
|
|
12695
|
+
* are placed after their prerequisites.
|
|
12696
|
+
*
|
|
12697
|
+
* Falls back to numeric sort if no epics document exists or no
|
|
12698
|
+
* dependencies are found among the provided keys.
|
|
12699
|
+
*/
|
|
12700
|
+
function topologicalSortByDependencies(keys, projectRoot) {
|
|
12701
|
+
if (keys.length <= 1) return keys;
|
|
12702
|
+
const keySet = new Set(keys);
|
|
12703
|
+
const deps = parseEpicsDependencies(projectRoot, keySet);
|
|
12704
|
+
if (deps.size === 0) return sortStoryKeys(keys);
|
|
12705
|
+
const inDegree = new Map();
|
|
12706
|
+
const successors = new Map();
|
|
12707
|
+
for (const key of keys) {
|
|
12708
|
+
inDegree.set(key, 0);
|
|
12709
|
+
successors.set(key, new Set());
|
|
12710
|
+
}
|
|
12711
|
+
for (const [dependent, depSet] of deps) {
|
|
12712
|
+
if (!keySet.has(dependent)) continue;
|
|
12713
|
+
for (const dep of depSet) {
|
|
12714
|
+
if (!keySet.has(dep)) continue;
|
|
12715
|
+
successors.get(dep).add(dependent);
|
|
12716
|
+
inDegree.set(dependent, (inDegree.get(dependent) ?? 0) + 1);
|
|
12717
|
+
}
|
|
12718
|
+
}
|
|
12719
|
+
const result = [];
|
|
12720
|
+
const processed = new Set();
|
|
12721
|
+
while (processed.size < keys.length) {
|
|
12722
|
+
const wave = [];
|
|
12723
|
+
for (const key of keys) if (!processed.has(key) && (inDegree.get(key) ?? 0) === 0) wave.push(key);
|
|
12724
|
+
if (wave.length === 0) {
|
|
12725
|
+
for (const key of sortStoryKeys(keys)) if (!processed.has(key)) result.push(key);
|
|
12726
|
+
break;
|
|
12727
|
+
}
|
|
12728
|
+
for (const key of sortStoryKeys(wave)) {
|
|
12729
|
+
result.push(key);
|
|
12730
|
+
processed.add(key);
|
|
12731
|
+
for (const succ of successors.get(key) ?? []) inDegree.set(succ, (inDegree.get(succ) ?? 0) - 1);
|
|
12732
|
+
}
|
|
12733
|
+
}
|
|
12734
|
+
return result;
|
|
12735
|
+
}
|
|
12515
12736
|
|
|
12516
12737
|
//#endregion
|
|
12517
12738
|
//#region src/modules/phase-orchestrator/phase-detection.ts
|
|
@@ -21100,7 +21321,9 @@ function buildGraphNode(id, attrs, subgraphClass, defaultMaxRetries) {
|
|
|
21100
21321
|
autoStatus: attrBool(attrs, "auto_status", true),
|
|
21101
21322
|
allowPartial: attrBool(attrs, "allow_partial", false),
|
|
21102
21323
|
toolCommand: attrStr(attrs, "tool_command", ""),
|
|
21103
|
-
backend: attrStr(attrs, "backend", "")
|
|
21324
|
+
backend: attrStr(attrs, "backend", ""),
|
|
21325
|
+
maxParallel: attrInt(attrs, "maxParallel", 0),
|
|
21326
|
+
joinPolicy: attrStr(attrs, "joinPolicy", "")
|
|
21104
21327
|
};
|
|
21105
21328
|
}
|
|
21106
21329
|
function buildGraphEdge(fromNode, toNode, attrs) {
|
|
@@ -21776,7 +21999,7 @@ const conditionSyntaxRule = {
|
|
|
21776
21999
|
const diagnostics = [];
|
|
21777
22000
|
for (let i = 0; i < graph.edges.length; i++) {
|
|
21778
22001
|
const edge = graph.edges[i];
|
|
21779
|
-
if (edge.condition) try {
|
|
22002
|
+
if (edge.condition && !edge.condition.trim().startsWith("llm:")) try {
|
|
21780
22003
|
parseCondition(edge.condition);
|
|
21781
22004
|
} catch (err) {
|
|
21782
22005
|
if (err instanceof ConditionParseError) diagnostics.push({
|
|
@@ -21969,6 +22192,102 @@ function createValidator() {
|
|
|
21969
22192
|
};
|
|
21970
22193
|
}
|
|
21971
22194
|
|
|
22195
|
+
//#endregion
|
|
22196
|
+
//#region packages/factory/dist/graph/llm-evaluator.js
|
|
22197
|
+
/**
|
|
22198
|
+
* LLM-based edge condition evaluator for the factory graph engine.
|
|
22199
|
+
*
|
|
22200
|
+
* Provides pure, side-effect-free functions for detecting `llm:` prefixed
|
|
22201
|
+
* conditions, building evaluation prompts, parsing boolean responses, and
|
|
22202
|
+
* executing an LLM-backed condition evaluation with safe error handling.
|
|
22203
|
+
*
|
|
22204
|
+
* Zero external package imports — all LLM wiring is done by the caller
|
|
22205
|
+
* (edge-selector.ts) via the injectable `llmCall` parameter.
|
|
22206
|
+
*
|
|
22207
|
+
* Story 50-4.
|
|
22208
|
+
*/
|
|
22209
|
+
/**
|
|
22210
|
+
* Returns `true` iff the condition string starts with the `"llm:"` prefix
|
|
22211
|
+
* (after trimming leading/trailing whitespace).
|
|
22212
|
+
*/
|
|
22213
|
+
function isLlmCondition(condition) {
|
|
22214
|
+
return condition.trim().startsWith("llm:");
|
|
22215
|
+
}
|
|
22216
|
+
/**
|
|
22217
|
+
* Strips the `"llm:"` prefix and trims surrounding whitespace from the
|
|
22218
|
+
* remainder of the condition string.
|
|
22219
|
+
*
|
|
22220
|
+
* @param condition - A condition string beginning with `"llm:"`.
|
|
22221
|
+
* @returns The extracted question text, trimmed.
|
|
22222
|
+
*/
|
|
22223
|
+
function extractLlmQuestion(condition) {
|
|
22224
|
+
return condition.trim().slice(4).trim();
|
|
22225
|
+
}
|
|
22226
|
+
/**
|
|
22227
|
+
* Builds an LLM evaluation prompt that includes the question and a JSON
|
|
22228
|
+
* block of the current context snapshot, with an explicit instruction to
|
|
22229
|
+
* answer with only "yes" or "no".
|
|
22230
|
+
*
|
|
22231
|
+
* @param question - The routing question extracted from the condition.
|
|
22232
|
+
* @param contextSnapshot - Shallow copy of the current execution context.
|
|
22233
|
+
* @returns The fully constructed prompt string.
|
|
22234
|
+
*/
|
|
22235
|
+
function buildEvaluationPrompt(question, contextSnapshot) {
|
|
22236
|
+
return [
|
|
22237
|
+
`You are evaluating a routing condition in a software pipeline.`,
|
|
22238
|
+
``,
|
|
22239
|
+
`Context:`,
|
|
22240
|
+
JSON.stringify(contextSnapshot, null, 2),
|
|
22241
|
+
``,
|
|
22242
|
+
`Question: ${question}`,
|
|
22243
|
+
``,
|
|
22244
|
+
`Answer with exactly "yes" or "no".`
|
|
22245
|
+
].join("\n");
|
|
22246
|
+
}
|
|
22247
|
+
/**
|
|
22248
|
+
* Parses an LLM yes/no response into a boolean value.
|
|
22249
|
+
*
|
|
22250
|
+
* Returns `true` if the cleaned (trimmed, lowercased) response starts with or
|
|
22251
|
+
* contains one of: `"yes"`, `"true"`, `"affirmative"`, `"correct"`, `"1"`.
|
|
22252
|
+
* Returns `false` otherwise, including for an empty string.
|
|
22253
|
+
*
|
|
22254
|
+
* @param response - Raw text response from the LLM.
|
|
22255
|
+
* @returns `true` for affirmative responses, `false` for all others.
|
|
22256
|
+
*/
|
|
22257
|
+
function parseLlmBoolResponse(response) {
|
|
22258
|
+
const cleaned = response.trim().toLowerCase();
|
|
22259
|
+
const affirmatives = [
|
|
22260
|
+
"yes",
|
|
22261
|
+
"true",
|
|
22262
|
+
"affirmative",
|
|
22263
|
+
"correct",
|
|
22264
|
+
"1"
|
|
22265
|
+
];
|
|
22266
|
+
return affirmatives.some((token) => cleaned === token || cleaned.startsWith(token + " ") || cleaned.startsWith(token + "\n"));
|
|
22267
|
+
}
|
|
22268
|
+
/**
|
|
22269
|
+
* Evaluates an LLM edge condition asynchronously.
|
|
22270
|
+
*
|
|
22271
|
+
* Builds the evaluation prompt, calls the injectable `llmCall` function,
|
|
22272
|
+
* and parses the response via `parseLlmBoolResponse`. If any step throws,
|
|
22273
|
+
* returns `false` silently — never re-throws.
|
|
22274
|
+
*
|
|
22275
|
+
* @param question - The routing question to evaluate.
|
|
22276
|
+
* @param contextSnapshot - Shallow copy of the current execution context.
|
|
22277
|
+
* @param llmCall - Injectable async function that calls an LLM and
|
|
22278
|
+
* returns the raw text response.
|
|
22279
|
+
* @returns `true` if the LLM responds affirmatively, `false` otherwise or on error.
|
|
22280
|
+
*/
|
|
22281
|
+
async function evaluateLlmCondition(question, contextSnapshot, llmCall) {
|
|
22282
|
+
try {
|
|
22283
|
+
const prompt = buildEvaluationPrompt(question, contextSnapshot);
|
|
22284
|
+
const response = await llmCall(prompt);
|
|
22285
|
+
return parseLlmBoolResponse(response);
|
|
22286
|
+
} catch {
|
|
22287
|
+
return false;
|
|
22288
|
+
}
|
|
22289
|
+
}
|
|
22290
|
+
|
|
21972
22291
|
//#endregion
|
|
21973
22292
|
//#region packages/factory/dist/graph/edge-selector.js
|
|
21974
22293
|
/**
|
|
@@ -22010,6 +22329,7 @@ function bestByWeightThenLexical(edges) {
|
|
|
22010
22329
|
* Select the best outgoing edge from `node` according to the 5-step Attractor spec.
|
|
22011
22330
|
*
|
|
22012
22331
|
* Step 1: Condition-matched edges — highest weight, lexical tiebreak.
|
|
22332
|
+
* Edges with `llm:` prefix conditions are evaluated via LLM call.
|
|
22013
22333
|
* Step 2: Preferred label match on unconditional edges — first match wins.
|
|
22014
22334
|
* Step 3: Suggested next IDs on unconditional edges — first suggestedNextId wins.
|
|
22015
22335
|
* Step 4: Highest weight among all unconditional edges.
|
|
@@ -22019,14 +22339,48 @@ function bestByWeightThenLexical(edges) {
|
|
|
22019
22339
|
* @param outcome - The outcome returned by the node's handler.
|
|
22020
22340
|
* @param context - The current execution context (used for condition evaluation).
|
|
22021
22341
|
* @param graph - The full graph (source of edges).
|
|
22342
|
+
* @param options - Optional injectable overrides (e.g. llmCall for testing).
|
|
22022
22343
|
* @returns The selected `GraphEdge`, or `null` if no outgoing edges exist.
|
|
22023
22344
|
*/
|
|
22024
|
-
function selectEdge(node, outcome, context, graph) {
|
|
22345
|
+
async function selectEdge(node, outcome, context, graph, options) {
|
|
22025
22346
|
const outgoing = graph.edges.filter((e) => e.fromNode === node.id);
|
|
22026
22347
|
if (outgoing.length === 0) return null;
|
|
22348
|
+
const defaultLlmCall$1 = (prompt) => callLLM({
|
|
22349
|
+
model: node.llmModel || "claude-haiku-4-5",
|
|
22350
|
+
provider: node.llmProvider || "anthropic",
|
|
22351
|
+
reasoningEffort: "low",
|
|
22352
|
+
prompt
|
|
22353
|
+
}).then((r) => r.text);
|
|
22354
|
+
const llmCall = options?.llmCall ?? defaultLlmCall$1;
|
|
22027
22355
|
const conditionMatches = [];
|
|
22028
22356
|
const snapshot = context.snapshot();
|
|
22029
|
-
for (const edge of outgoing) if (edge.condition && edge.condition.trim() !== "")
|
|
22357
|
+
for (const edge of outgoing) if (edge.condition && edge.condition.trim() !== "") if (isLlmCondition(edge.condition)) {
|
|
22358
|
+
const question = extractLlmQuestion(edge.condition);
|
|
22359
|
+
let evalError = null;
|
|
22360
|
+
const trackingLlmCall = async (prompt) => {
|
|
22361
|
+
try {
|
|
22362
|
+
return await llmCall(prompt);
|
|
22363
|
+
} catch (err) {
|
|
22364
|
+
evalError = err instanceof Error ? err.message : String(err);
|
|
22365
|
+
throw err;
|
|
22366
|
+
}
|
|
22367
|
+
};
|
|
22368
|
+
const matched = await evaluateLlmCondition(question, snapshot, trackingLlmCall);
|
|
22369
|
+
options?.eventBus?.emit("graph:llm-edge-evaluated", {
|
|
22370
|
+
runId: options.runId ?? "unknown",
|
|
22371
|
+
nodeId: node.id,
|
|
22372
|
+
question,
|
|
22373
|
+
result: matched
|
|
22374
|
+
});
|
|
22375
|
+
context.set("llm.edge_eval_count", context.getNumber("llm.edge_eval_count", 0) + 1);
|
|
22376
|
+
if (evalError !== null) {
|
|
22377
|
+
const existing = context.get("llm.edge_eval_errors");
|
|
22378
|
+
const errors = Array.isArray(existing) ? existing : [];
|
|
22379
|
+
errors.push(evalError);
|
|
22380
|
+
context.set("llm.edge_eval_errors", errors);
|
|
22381
|
+
}
|
|
22382
|
+
if (matched) conditionMatches.push(edge);
|
|
22383
|
+
} else try {
|
|
22030
22384
|
if (evaluateCondition(edge.condition, snapshot)) conditionMatches.push(edge);
|
|
22031
22385
|
} catch {}
|
|
22032
22386
|
if (conditionMatches.length > 0) return bestByWeightThenLexical(conditionMatches);
|
|
@@ -22987,6 +23341,111 @@ function resolveFidelity(node, incomingEdge, graph) {
|
|
|
22987
23341
|
return "";
|
|
22988
23342
|
}
|
|
22989
23343
|
|
|
23344
|
+
//#endregion
|
|
23345
|
+
//#region packages/factory/dist/stylesheet/resolver.js
|
|
23346
|
+
/**
|
|
23347
|
+
* Specificity-based resolver for the model stylesheet.
|
|
23348
|
+
*
|
|
23349
|
+
* Given a parsed stylesheet and a graph node, `resolveNodeStyles` determines
|
|
23350
|
+
* which LLM routing properties apply to that node by:
|
|
23351
|
+
* 1. Filtering rules to only those whose selector matches the node.
|
|
23352
|
+
* 2. Sorting matching rules by specificity ascending (stable sort so that
|
|
23353
|
+
* equal-specificity rules preserve source order).
|
|
23354
|
+
* 3. Iterating the sorted list and letting each rule overwrite properties
|
|
23355
|
+
* — the last rule at the highest specificity wins; for ties the rule
|
|
23356
|
+
* appearing later in the original stylesheet wins.
|
|
23357
|
+
*
|
|
23358
|
+
* **Caller contract**: `resolveNodeStyles` does NOT enforce the "explicit node
|
|
23359
|
+
* attribute wins" rule. The caller (executor / node preparation layer) is
|
|
23360
|
+
* responsible for the final merge:
|
|
23361
|
+
* ```typescript
|
|
23362
|
+
* const resolved = resolveNodeStyles(node, stylesheet)
|
|
23363
|
+
* const finalModel = node.llmModel || resolved.llmModel || graph.defaultLlmModel || ''
|
|
23364
|
+
* ```
|
|
23365
|
+
*
|
|
23366
|
+
* Story 42-7.
|
|
23367
|
+
*/
|
|
23368
|
+
/**
|
|
23369
|
+
* Return `true` if the given `selector` matches the provided `node`.
|
|
23370
|
+
*
|
|
23371
|
+
* Matching rules:
|
|
23372
|
+
* - `universal`: always matches every node.
|
|
23373
|
+
* - `shape`: matches when `node.shape === selector.value`.
|
|
23374
|
+
* - `class`: matches when the node's `class` field, split on commas and
|
|
23375
|
+
* trimmed, contains `selector.value` (case-sensitive).
|
|
23376
|
+
* - `id`: matches when `node.id === selector.value`.
|
|
23377
|
+
*/
|
|
23378
|
+
function matchesNode(node, selector) {
|
|
23379
|
+
switch (selector.type) {
|
|
23380
|
+
case "universal": return true;
|
|
23381
|
+
case "shape": return node.shape === selector.value;
|
|
23382
|
+
case "class": {
|
|
23383
|
+
const tokens = node.class.split(",").map((t) => t.trim());
|
|
23384
|
+
return tokens.includes(selector.value);
|
|
23385
|
+
}
|
|
23386
|
+
case "id": return node.id === selector.value;
|
|
23387
|
+
default: return false;
|
|
23388
|
+
}
|
|
23389
|
+
}
|
|
23390
|
+
/**
|
|
23391
|
+
* Resolve LLM routing properties for a single graph node using the parsed stylesheet.
|
|
23392
|
+
*
|
|
23393
|
+
* @param node - The graph node to resolve properties for.
|
|
23394
|
+
* @param stylesheet - A parsed stylesheet (array of `StylesheetRule` in source order).
|
|
23395
|
+
* @returns A `ResolvedNodeStyles` object containing only the properties that
|
|
23396
|
+
* were resolved from matching rules. Properties absent from matching rules
|
|
23397
|
+
* are omitted (not set to `undefined` explicitly).
|
|
23398
|
+
*
|
|
23399
|
+
* **Important**: this function does NOT check the node's own attributes.
|
|
23400
|
+
* Callers must give explicit node attributes (`node.llmModel`, etc.) priority
|
|
23401
|
+
* over the values returned here.
|
|
23402
|
+
*/
|
|
23403
|
+
function resolveNodeStyles(node, stylesheet) {
|
|
23404
|
+
const matchingRules = stylesheet.filter((rule) => matchesNode(node, rule.selector));
|
|
23405
|
+
matchingRules.sort((a, b) => a.selector.specificity - b.selector.specificity);
|
|
23406
|
+
const resolved = {};
|
|
23407
|
+
for (const rule of matchingRules) for (const decl of rule.declarations) if (decl.property === "llm_model") resolved.llmModel = decl.value;
|
|
23408
|
+
else if (decl.property === "llm_provider") resolved.llmProvider = decl.value;
|
|
23409
|
+
else if (decl.property === "reasoning_effort") resolved.reasoningEffort = decl.value;
|
|
23410
|
+
return resolved;
|
|
23411
|
+
}
|
|
23412
|
+
|
|
23413
|
+
//#endregion
|
|
23414
|
+
//#region packages/factory/dist/graph/transformer.js
|
|
23415
|
+
/**
|
|
23416
|
+
* Apply stylesheet rules to all nodes in the given graph.
|
|
23417
|
+
*
|
|
23418
|
+
* Merges `inheritedStylesheet` (parent rules) with the graph's own
|
|
23419
|
+
* `modelStylesheet` rules, parent rules first so that local child rules win at
|
|
23420
|
+
* equal specificity via source-order tie-breaking (later rule wins).
|
|
23421
|
+
*
|
|
23422
|
+
* For each node, the resolved properties are applied only if the node does NOT
|
|
23423
|
+
* already have a non-empty explicit value (explicit values are preserved).
|
|
23424
|
+
*
|
|
23425
|
+
* This function **mutates** graph nodes in-place — consistent with how the
|
|
23426
|
+
* DOT parser sets node fields after parsing.
|
|
23427
|
+
*
|
|
23428
|
+
* **Idempotency**: calling `applyStylesheet` twice on the same graph is safe
|
|
23429
|
+
* because nodes with an existing non-empty `llmModel` (or `llmProvider` /
|
|
23430
|
+
* `reasoningEffort`) are never overwritten.
|
|
23431
|
+
*
|
|
23432
|
+
* @param graph - The graph whose nodes should be styled.
|
|
23433
|
+
* @param inheritedStylesheet - Optional stylesheet rules inherited from a
|
|
23434
|
+
* parent graph. Prepended before the graph's own rules so that child local
|
|
23435
|
+
* rules win at equal specificity. Pass `undefined` when there is no parent.
|
|
23436
|
+
*/
|
|
23437
|
+
function applyStylesheet(graph, inheritedStylesheet) {
|
|
23438
|
+
const localRules = graph.modelStylesheet ? parseStylesheet(graph.modelStylesheet) : [];
|
|
23439
|
+
const effectiveStylesheet = [...inheritedStylesheet ?? [], ...localRules];
|
|
23440
|
+
if (effectiveStylesheet.length === 0) return;
|
|
23441
|
+
for (const node of graph.nodes.values()) {
|
|
23442
|
+
const resolved = resolveNodeStyles(node, effectiveStylesheet);
|
|
23443
|
+
if (!node.llmModel && resolved.llmModel) node.llmModel = resolved.llmModel;
|
|
23444
|
+
if (!node.llmProvider && resolved.llmProvider) node.llmProvider = resolved.llmProvider;
|
|
23445
|
+
if (!node.reasoningEffort && resolved.reasoningEffort) node.reasoningEffort = resolved.reasoningEffort;
|
|
23446
|
+
}
|
|
23447
|
+
}
|
|
23448
|
+
|
|
22990
23449
|
//#endregion
|
|
22991
23450
|
//#region packages/factory/dist/graph/executor.js
|
|
22992
23451
|
/**
|
|
@@ -23081,6 +23540,7 @@ async function dispatchWithRetry(node, context, graph, config, nodeRetries) {
|
|
|
23081
23540
|
*/
|
|
23082
23541
|
function createGraphExecutor() {
|
|
23083
23542
|
return { async run(graph, config) {
|
|
23543
|
+
applyStylesheet(graph, config.inheritedStylesheet);
|
|
23084
23544
|
const checkpointManager = new CheckpointManager();
|
|
23085
23545
|
const checkpointFilePath = path.join(config.logsRoot, "checkpoint.json");
|
|
23086
23546
|
const controller = createConvergenceController();
|
|
@@ -23176,7 +23636,10 @@ function createGraphExecutor() {
|
|
|
23176
23636
|
if (resumeState.completedNodes.has(checkpoint.currentNode)) {
|
|
23177
23637
|
const lastNode = graph.nodes.get(checkpoint.currentNode);
|
|
23178
23638
|
if (lastNode) {
|
|
23179
|
-
const nextEdge = selectEdge(lastNode, { status: "SUCCESS" }, context, graph
|
|
23639
|
+
const nextEdge = await selectEdge(lastNode, { status: "SUCCESS" }, context, graph, {
|
|
23640
|
+
...config.eventBus !== void 0 ? { eventBus: config.eventBus } : {},
|
|
23641
|
+
runId: config.runId
|
|
23642
|
+
});
|
|
23180
23643
|
if (nextEdge) {
|
|
23181
23644
|
const nextNode = graph.nodes.get(nextEdge.toNode);
|
|
23182
23645
|
if (!nextNode) throw new Error(`Edge target node "${nextEdge.toNode}" not found in graph`);
|
|
@@ -23189,6 +23652,7 @@ function createGraphExecutor() {
|
|
|
23189
23652
|
else currentNode = graph.startNode();
|
|
23190
23653
|
}
|
|
23191
23654
|
} else currentNode = graph.startNode();
|
|
23655
|
+
context.set("__runId", config.runId ?? "unknown");
|
|
23192
23656
|
while (true) {
|
|
23193
23657
|
const sessionResult = sessionManager.checkBudget((config.wallClockCapMs ?? 0) / 1e3);
|
|
23194
23658
|
if (!sessionResult.allowed) {
|
|
@@ -23279,7 +23743,10 @@ function createGraphExecutor() {
|
|
|
23279
23743
|
return { status: "SUCCESS" };
|
|
23280
23744
|
}
|
|
23281
23745
|
if (resumeCompletedSet?.has(currentNode.id)) {
|
|
23282
|
-
const skipEdge = selectEdge(currentNode, { status: "SUCCESS" }, context, graph
|
|
23746
|
+
const skipEdge = await selectEdge(currentNode, { status: "SUCCESS" }, context, graph, {
|
|
23747
|
+
...config.eventBus !== void 0 ? { eventBus: config.eventBus } : {},
|
|
23748
|
+
runId: config.runId
|
|
23749
|
+
});
|
|
23283
23750
|
if (!skipEdge) {
|
|
23284
23751
|
await persistExit("failed", `No outgoing edge from node ${currentNode.id}`);
|
|
23285
23752
|
return {
|
|
@@ -23464,7 +23931,10 @@ function createGraphExecutor() {
|
|
|
23464
23931
|
};
|
|
23465
23932
|
}
|
|
23466
23933
|
}
|
|
23467
|
-
const edge = selectEdge(currentNode, outcome, context, graph
|
|
23934
|
+
const edge = await selectEdge(currentNode, outcome, context, graph, {
|
|
23935
|
+
...config.eventBus !== void 0 ? { eventBus: config.eventBus } : {},
|
|
23936
|
+
runId: config.runId
|
|
23937
|
+
});
|
|
23468
23938
|
if (!edge) {
|
|
23469
23939
|
await persistExit("failed", `No outgoing edge from node ${currentNode.id}`);
|
|
23470
23940
|
return {
|
|
@@ -31087,75 +31557,6 @@ const conditionalHandler = async (_node, _context, _graph) => {
|
|
|
31087
31557
|
return { status: "SUCCESS" };
|
|
31088
31558
|
};
|
|
31089
31559
|
|
|
31090
|
-
//#endregion
|
|
31091
|
-
//#region packages/factory/dist/stylesheet/resolver.js
|
|
31092
|
-
/**
|
|
31093
|
-
* Specificity-based resolver for the model stylesheet.
|
|
31094
|
-
*
|
|
31095
|
-
* Given a parsed stylesheet and a graph node, `resolveNodeStyles` determines
|
|
31096
|
-
* which LLM routing properties apply to that node by:
|
|
31097
|
-
* 1. Filtering rules to only those whose selector matches the node.
|
|
31098
|
-
* 2. Sorting matching rules by specificity ascending (stable sort so that
|
|
31099
|
-
* equal-specificity rules preserve source order).
|
|
31100
|
-
* 3. Iterating the sorted list and letting each rule overwrite properties
|
|
31101
|
-
* — the last rule at the highest specificity wins; for ties the rule
|
|
31102
|
-
* appearing later in the original stylesheet wins.
|
|
31103
|
-
*
|
|
31104
|
-
* **Caller contract**: `resolveNodeStyles` does NOT enforce the "explicit node
|
|
31105
|
-
* attribute wins" rule. The caller (executor / node preparation layer) is
|
|
31106
|
-
* responsible for the final merge:
|
|
31107
|
-
* ```typescript
|
|
31108
|
-
* const resolved = resolveNodeStyles(node, stylesheet)
|
|
31109
|
-
* const finalModel = node.llmModel || resolved.llmModel || graph.defaultLlmModel || ''
|
|
31110
|
-
* ```
|
|
31111
|
-
*
|
|
31112
|
-
* Story 42-7.
|
|
31113
|
-
*/
|
|
31114
|
-
/**
|
|
31115
|
-
* Return `true` if the given `selector` matches the provided `node`.
|
|
31116
|
-
*
|
|
31117
|
-
* Matching rules:
|
|
31118
|
-
* - `universal`: always matches every node.
|
|
31119
|
-
* - `shape`: matches when `node.shape === selector.value`.
|
|
31120
|
-
* - `class`: matches when the node's `class` field, split on commas and
|
|
31121
|
-
* trimmed, contains `selector.value` (case-sensitive).
|
|
31122
|
-
* - `id`: matches when `node.id === selector.value`.
|
|
31123
|
-
*/
|
|
31124
|
-
function matchesNode(node, selector) {
|
|
31125
|
-
switch (selector.type) {
|
|
31126
|
-
case "universal": return true;
|
|
31127
|
-
case "shape": return node.shape === selector.value;
|
|
31128
|
-
case "class": {
|
|
31129
|
-
const tokens = node.class.split(",").map((t) => t.trim());
|
|
31130
|
-
return tokens.includes(selector.value);
|
|
31131
|
-
}
|
|
31132
|
-
case "id": return node.id === selector.value;
|
|
31133
|
-
default: return false;
|
|
31134
|
-
}
|
|
31135
|
-
}
|
|
31136
|
-
/**
|
|
31137
|
-
* Resolve LLM routing properties for a single graph node using the parsed stylesheet.
|
|
31138
|
-
*
|
|
31139
|
-
* @param node - The graph node to resolve properties for.
|
|
31140
|
-
* @param stylesheet - A parsed stylesheet (array of `StylesheetRule` in source order).
|
|
31141
|
-
* @returns A `ResolvedNodeStyles` object containing only the properties that
|
|
31142
|
-
* were resolved from matching rules. Properties absent from matching rules
|
|
31143
|
-
* are omitted (not set to `undefined` explicitly).
|
|
31144
|
-
*
|
|
31145
|
-
* **Important**: this function does NOT check the node's own attributes.
|
|
31146
|
-
* Callers must give explicit node attributes (`node.llmModel`, etc.) priority
|
|
31147
|
-
* over the values returned here.
|
|
31148
|
-
*/
|
|
31149
|
-
function resolveNodeStyles(node, stylesheet) {
|
|
31150
|
-
const matchingRules = stylesheet.filter((rule) => matchesNode(node, rule.selector));
|
|
31151
|
-
matchingRules.sort((a, b) => a.selector.specificity - b.selector.specificity);
|
|
31152
|
-
const resolved = {};
|
|
31153
|
-
for (const rule of matchingRules) for (const decl of rule.declarations) if (decl.property === "llm_model") resolved.llmModel = decl.value;
|
|
31154
|
-
else if (decl.property === "llm_provider") resolved.llmProvider = decl.value;
|
|
31155
|
-
else if (decl.property === "reasoning_effort") resolved.reasoningEffort = decl.value;
|
|
31156
|
-
return resolved;
|
|
31157
|
-
}
|
|
31158
|
-
|
|
31159
31560
|
//#endregion
|
|
31160
31561
|
//#region packages/factory/dist/handlers/codergen-handler.js
|
|
31161
31562
|
const DEFAULT_MODEL = "claude-sonnet-4-5";
|
|
@@ -31422,6 +31823,762 @@ function createWaitHumanHandler(options) {
|
|
|
31422
31823
|
};
|
|
31423
31824
|
}
|
|
31424
31825
|
|
|
31826
|
+
//#endregion
|
|
31827
|
+
//#region packages/factory/dist/handlers/join-policy.js
|
|
31828
|
+
/**
|
|
31829
|
+
* Join policy types, pure evaluator, and branch cancellation manager
|
|
31830
|
+
* for parallel node fan-out/fan-in coordination.
|
|
31831
|
+
*
|
|
31832
|
+
* Design constraints:
|
|
31833
|
+
* - Zero external package imports (pure TypeScript/built-ins only)
|
|
31834
|
+
* - `evaluateJoinPolicy` is a pure synchronous function (no I/O, no side effects)
|
|
31835
|
+
* - `BranchCancellationManager` uses the globally-available AbortController (Node 18+)
|
|
31836
|
+
*
|
|
31837
|
+
* Story 50-3 (AC6).
|
|
31838
|
+
*/
|
|
31839
|
+
/**
|
|
31840
|
+
* Pure synchronous function that decides what the parallel handler should do
|
|
31841
|
+
* after a set of branches has completed.
|
|
31842
|
+
*
|
|
31843
|
+
* Called incrementally after each branch finishes: pass the full `completed`
|
|
31844
|
+
* array (including all branches resolved so far) and the `total` branch count.
|
|
31845
|
+
*
|
|
31846
|
+
* @param config - Join policy configuration parsed from the parallel node.
|
|
31847
|
+
* @param completed - All branch results collected so far (any order).
|
|
31848
|
+
* @param total - Total number of branches in the fan-out set.
|
|
31849
|
+
* @returns A `JoinDecision` indicating whether to continue, wait, or fail.
|
|
31850
|
+
*/
|
|
31851
|
+
function evaluateJoinPolicy(config, completed, total) {
|
|
31852
|
+
const successes = completed.filter((r) => r.outcome === "SUCCESS");
|
|
31853
|
+
const failures = completed.filter((r) => r.outcome === "FAIL");
|
|
31854
|
+
switch (config.policy) {
|
|
31855
|
+
case "wait_all": {
|
|
31856
|
+
if (completed.length < total) return { action: "wait" };
|
|
31857
|
+
return {
|
|
31858
|
+
action: "continue",
|
|
31859
|
+
results: completed
|
|
31860
|
+
};
|
|
31861
|
+
}
|
|
31862
|
+
case "first_success": {
|
|
31863
|
+
if (successes.length >= 1) return {
|
|
31864
|
+
action: "continue",
|
|
31865
|
+
results: completed
|
|
31866
|
+
};
|
|
31867
|
+
if (completed.length >= total) return {
|
|
31868
|
+
action: "fail",
|
|
31869
|
+
reason: `first_success: all ${total} branches failed`
|
|
31870
|
+
};
|
|
31871
|
+
return { action: "wait" };
|
|
31872
|
+
}
|
|
31873
|
+
case "quorum": {
|
|
31874
|
+
const needed = config.quorum_size ?? 1;
|
|
31875
|
+
if (needed <= 0) return {
|
|
31876
|
+
action: "fail",
|
|
31877
|
+
reason: "quorum_size must be >= 1"
|
|
31878
|
+
};
|
|
31879
|
+
if (successes.length >= needed) return {
|
|
31880
|
+
action: "continue",
|
|
31881
|
+
results: completed
|
|
31882
|
+
};
|
|
31883
|
+
const remaining = total - completed.length;
|
|
31884
|
+
if (successes.length + remaining < needed) return {
|
|
31885
|
+
action: "fail",
|
|
31886
|
+
reason: `quorum unreachable: ${failures.length} failed, needed ${needed} of ${total}`
|
|
31887
|
+
};
|
|
31888
|
+
return { action: "wait" };
|
|
31889
|
+
}
|
|
31890
|
+
default: {
|
|
31891
|
+
const _exhaustive = config.policy;
|
|
31892
|
+
return {
|
|
31893
|
+
action: "fail",
|
|
31894
|
+
reason: `unknown join policy: ${String(_exhaustive)}`
|
|
31895
|
+
};
|
|
31896
|
+
}
|
|
31897
|
+
}
|
|
31898
|
+
}
|
|
31899
|
+
/**
|
|
31900
|
+
* Manages one `AbortController` per branch in a parallel fan-out execution.
|
|
31901
|
+
*
|
|
31902
|
+
* Typical usage:
|
|
31903
|
+
* 1. Construct with the total branch count.
|
|
31904
|
+
* 2. Pass `getSignal(i)` to the i-th branch executor.
|
|
31905
|
+
* 3. After the join condition is met, call `cancelRemaining(completedSet)`.
|
|
31906
|
+
* 4. Await `drainAsync(timeoutMs)` to give cancelled branches time to clean up.
|
|
31907
|
+
*/
|
|
31908
|
+
var BranchCancellationManager = class {
|
|
31909
|
+
controllers;
|
|
31910
|
+
/**
|
|
31911
|
+
* @param branchCount - Total number of branches; allocates one AbortController per branch.
|
|
31912
|
+
*/
|
|
31913
|
+
constructor(branchCount) {
|
|
31914
|
+
this.controllers = Array.from({ length: branchCount }, () => new AbortController());
|
|
31915
|
+
}
|
|
31916
|
+
/**
|
|
31917
|
+
* Return the AbortSignal for the branch at the given zero-based index.
|
|
31918
|
+
* The signal is not yet aborted; it becomes aborted when `cancelRemaining` includes the index.
|
|
31919
|
+
*/
|
|
31920
|
+
getSignal(index) {
|
|
31921
|
+
const ctrl = this.controllers[index];
|
|
31922
|
+
if (ctrl === void 0) throw new RangeError(`Branch index ${index} is out of range (branchCount=${this.controllers.length})`);
|
|
31923
|
+
return ctrl.signal;
|
|
31924
|
+
}
|
|
31925
|
+
/**
|
|
31926
|
+
* Abort all branches whose index is NOT in `completedIndices`.
|
|
31927
|
+
* Branches that have already completed are not re-aborted.
|
|
31928
|
+
*
|
|
31929
|
+
* @param completedIndices - Set of branch indices that finished successfully/naturally.
|
|
31930
|
+
*/
|
|
31931
|
+
cancelRemaining(completedIndices) {
|
|
31932
|
+
this.controllers.forEach((ctrl, i) => {
|
|
31933
|
+
if (!completedIndices.has(i)) ctrl.abort();
|
|
31934
|
+
});
|
|
31935
|
+
}
|
|
31936
|
+
/**
|
|
31937
|
+
* Wait `timeoutMs` milliseconds for in-flight branches to honour their AbortSignals
|
|
31938
|
+
* and finish any cleanup work before the parallel handler resolves.
|
|
31939
|
+
*
|
|
31940
|
+
* @param timeoutMs - Drain window in milliseconds (default 5000).
|
|
31941
|
+
*/
|
|
31942
|
+
async drainAsync(timeoutMs) {
|
|
31943
|
+
await new Promise((resolve$6) => setTimeout(resolve$6, timeoutMs));
|
|
31944
|
+
}
|
|
31945
|
+
};
|
|
31946
|
+
|
|
31947
|
+
//#endregion
|
|
31948
|
+
//#region packages/factory/dist/handlers/parallel.js
|
|
31949
|
+
/**
|
|
31950
|
+
* Maps a BranchResult outcome string to a StageStatus for event payloads.
|
|
31951
|
+
*
|
|
31952
|
+
* BranchResult.outcome: 'SUCCESS' | 'FAIL' | 'CANCELLED'
|
|
31953
|
+
* StageStatus: 'SUCCESS' | 'FAIL' | 'PARTIAL_SUCCESS' | 'RETRY' | 'SKIPPED'
|
|
31954
|
+
*
|
|
31955
|
+
* Mapping:
|
|
31956
|
+
* 'SUCCESS' → 'SUCCESS'
|
|
31957
|
+
* 'CANCELLED' → 'SKIPPED'
|
|
31958
|
+
* 'FAIL' → 'FAIL'
|
|
31959
|
+
*/
|
|
31960
|
+
function branchOutcomeToStatus(outcome) {
|
|
31961
|
+
if (outcome === "SUCCESS") return "SUCCESS";
|
|
31962
|
+
if (outcome === "CANCELLED") return "SKIPPED";
|
|
31963
|
+
return "FAIL";
|
|
31964
|
+
}
|
|
31965
|
+
/**
|
|
31966
|
+
* Enrich a join-policy `BranchResult` with fan-in-compatible fields so that
|
|
31967
|
+
* `fan-in.ts` can read `parallel.results` without type-shape mismatch.
|
|
31968
|
+
*
|
|
31969
|
+
* The returned object is a superset of `BranchResult` — it preserves every
|
|
31970
|
+
* original field (index, outcome, contextSnapshot, error) AND adds the fields
|
|
31971
|
+
* that `fan-in.ts` requires (branch_id, status, context_updates, failure_reason).
|
|
31972
|
+
*
|
|
31973
|
+
* Mapping:
|
|
31974
|
+
* index → branch_id (same numeric value)
|
|
31975
|
+
* outcome → status ('SUCCESS' | 'FAIL'/'CANCELLED' → 'SUCCESS' | 'FAILURE')
|
|
31976
|
+
* contextSnapshot → context_updates (shallow context snapshot)
|
|
31977
|
+
* error → failure_reason (human-readable failure description)
|
|
31978
|
+
*/
|
|
31979
|
+
function toBridgeBranchResult(result) {
|
|
31980
|
+
return {
|
|
31981
|
+
...result,
|
|
31982
|
+
branch_id: result.index,
|
|
31983
|
+
status: result.outcome === "SUCCESS" ? "SUCCESS" : "FAILURE",
|
|
31984
|
+
context_updates: result.contextSnapshot,
|
|
31985
|
+
failure_reason: result.error
|
|
31986
|
+
};
|
|
31987
|
+
}
|
|
31988
|
+
/**
|
|
31989
|
+
* Run an array of async task factories with at most `limit` executing
|
|
31990
|
+
* concurrently at any point in time, preserving result order.
|
|
31991
|
+
*/
|
|
31992
|
+
async function runWithConcurrencyLimit(tasks, limit) {
|
|
31993
|
+
const results = new Array(tasks.length);
|
|
31994
|
+
const executing = new Set();
|
|
31995
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
31996
|
+
const idx = i;
|
|
31997
|
+
const p = tasks[idx]().then((r) => {
|
|
31998
|
+
results[idx] = r;
|
|
31999
|
+
}).finally(() => {
|
|
32000
|
+
executing.delete(p);
|
|
32001
|
+
});
|
|
32002
|
+
executing.add(p);
|
|
32003
|
+
if (executing.size >= limit) await Promise.race(executing);
|
|
32004
|
+
}
|
|
32005
|
+
await Promise.all(executing);
|
|
32006
|
+
return results;
|
|
32007
|
+
}
|
|
32008
|
+
function parseJoinPolicyConfig(node) {
|
|
32009
|
+
const policyRaw = node.attrs?.["join_policy"] ?? node.joinPolicy ?? "wait_all";
|
|
32010
|
+
const policy = policyRaw === "first_success" || policyRaw === "quorum" ? policyRaw : "wait_all";
|
|
32011
|
+
const quorumSizeRaw = node.attrs?.["quorum_size"];
|
|
32012
|
+
const quorum_size = quorumSizeRaw !== void 0 ? parseInt(quorumSizeRaw, 10) : void 0;
|
|
32013
|
+
const drainRaw = node.attrs?.["cancel_drain_timeout_ms"];
|
|
32014
|
+
const cancel_drain_timeout_ms = drainRaw !== void 0 ? parseInt(drainRaw, 10) : void 0;
|
|
32015
|
+
return {
|
|
32016
|
+
policy,
|
|
32017
|
+
...quorum_size !== void 0 && { quorum_size },
|
|
32018
|
+
...cancel_drain_timeout_ms !== void 0 && { cancel_drain_timeout_ms }
|
|
32019
|
+
};
|
|
32020
|
+
}
|
|
32021
|
+
/**
|
|
32022
|
+
* Build an async task that executes a single branch node and returns a
|
|
32023
|
+
* `BranchResult`. Isolation is guaranteed via `context.clone()`.
|
|
32024
|
+
*/
|
|
32025
|
+
function makeBranchTask(edge, index, signal, context, graph, options) {
|
|
32026
|
+
return async () => {
|
|
32027
|
+
if (signal.aborted) return {
|
|
32028
|
+
index,
|
|
32029
|
+
outcome: "CANCELLED"
|
|
32030
|
+
};
|
|
32031
|
+
const branchNode = graph.nodes.get(edge.toNode);
|
|
32032
|
+
if (!branchNode) return {
|
|
32033
|
+
index,
|
|
32034
|
+
outcome: "FAIL",
|
|
32035
|
+
error: `Branch node "${edge.toNode}" not found in graph`
|
|
32036
|
+
};
|
|
32037
|
+
const branchCtx = context.clone();
|
|
32038
|
+
branchCtx.set("_branch.abort_signal", signal);
|
|
32039
|
+
branchCtx.set("_branch.index", index);
|
|
32040
|
+
const handler = options.handlerRegistry.resolve(branchNode);
|
|
32041
|
+
let branchOutcome;
|
|
32042
|
+
try {
|
|
32043
|
+
branchOutcome = await handler(branchNode, branchCtx, graph);
|
|
32044
|
+
} catch (err) {
|
|
32045
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
32046
|
+
return {
|
|
32047
|
+
index,
|
|
32048
|
+
outcome: "FAIL",
|
|
32049
|
+
error: msg,
|
|
32050
|
+
contextSnapshot: branchCtx.snapshot()
|
|
32051
|
+
};
|
|
32052
|
+
}
|
|
32053
|
+
if (signal.aborted) return {
|
|
32054
|
+
index,
|
|
32055
|
+
outcome: "CANCELLED",
|
|
32056
|
+
contextSnapshot: branchCtx.snapshot()
|
|
32057
|
+
};
|
|
32058
|
+
const isSuccess = branchOutcome.status === "SUCCESS" || branchOutcome.status === "PARTIAL_SUCCESS";
|
|
32059
|
+
const result = {
|
|
32060
|
+
index,
|
|
32061
|
+
outcome: isSuccess ? "SUCCESS" : "FAIL",
|
|
32062
|
+
contextSnapshot: branchCtx.snapshot()
|
|
32063
|
+
};
|
|
32064
|
+
if (!isSuccess) {
|
|
32065
|
+
const reason = branchOutcome.failureReason;
|
|
32066
|
+
if (reason !== void 0) result.error = reason;
|
|
32067
|
+
}
|
|
32068
|
+
return result;
|
|
32069
|
+
};
|
|
32070
|
+
}
|
|
32071
|
+
/**
|
|
32072
|
+
* Create a `parallel` node handler that fans out to all outgoing branch nodes
|
|
32073
|
+
* concurrently, applies a configurable join policy, and writes results to context.
|
|
32074
|
+
*
|
|
32075
|
+
* @param options.handlerRegistry Registry used to resolve each branch node's
|
|
32076
|
+
* handler at invocation time.
|
|
32077
|
+
*/
|
|
32078
|
+
function createParallelHandler(options) {
|
|
32079
|
+
return async (node, context, graph) => {
|
|
32080
|
+
const branchEdges = graph.outgoingEdges(node.id);
|
|
32081
|
+
const branchCount = branchEdges.length;
|
|
32082
|
+
if (branchCount === 0) {
|
|
32083
|
+
context.set("parallel.results", []);
|
|
32084
|
+
return { status: "SUCCESS" };
|
|
32085
|
+
}
|
|
32086
|
+
const config = parseJoinPolicyConfig(node);
|
|
32087
|
+
const maxParallel = node.maxParallel ?? 0;
|
|
32088
|
+
const drainMs = config.cancel_drain_timeout_ms ?? 5e3;
|
|
32089
|
+
const cancellationManager = new BranchCancellationManager(branchCount);
|
|
32090
|
+
const runId = context.getString("__runId", "unknown");
|
|
32091
|
+
options.eventBus?.emit("graph:parallel-started", {
|
|
32092
|
+
runId,
|
|
32093
|
+
nodeId: node.id,
|
|
32094
|
+
branchCount,
|
|
32095
|
+
maxParallel: maxParallel > 0 ? maxParallel : branchCount,
|
|
32096
|
+
policy: config.policy
|
|
32097
|
+
});
|
|
32098
|
+
if (config.policy === "wait_all") {
|
|
32099
|
+
const tasks = branchEdges.map((edge, index) => {
|
|
32100
|
+
const baseTask = makeBranchTask(edge, index, cancellationManager.getSignal(index), context, graph, options);
|
|
32101
|
+
return async () => {
|
|
32102
|
+
options.eventBus?.emit("graph:parallel-branch-started", {
|
|
32103
|
+
runId,
|
|
32104
|
+
nodeId: node.id,
|
|
32105
|
+
branchIndex: index
|
|
32106
|
+
});
|
|
32107
|
+
const branchStart = Date.now();
|
|
32108
|
+
const result = await baseTask();
|
|
32109
|
+
const durationMs = Date.now() - branchStart;
|
|
32110
|
+
options.eventBus?.emit("graph:parallel-branch-completed", {
|
|
32111
|
+
runId,
|
|
32112
|
+
nodeId: node.id,
|
|
32113
|
+
branchIndex: index,
|
|
32114
|
+
status: branchOutcomeToStatus(result.outcome),
|
|
32115
|
+
durationMs
|
|
32116
|
+
});
|
|
32117
|
+
return result;
|
|
32118
|
+
};
|
|
32119
|
+
});
|
|
32120
|
+
const results = maxParallel > 0 ? await runWithConcurrencyLimit(tasks, maxParallel) : await Promise.all(tasks.map((t) => t()));
|
|
32121
|
+
context.set("parallel.results", results.map(toBridgeBranchResult));
|
|
32122
|
+
const completedCount = results.filter((r) => r.outcome === "SUCCESS").length;
|
|
32123
|
+
const cancelledCount = results.filter((r) => r.outcome === "CANCELLED").length;
|
|
32124
|
+
options.eventBus?.emit("graph:parallel-completed", {
|
|
32125
|
+
runId,
|
|
32126
|
+
nodeId: node.id,
|
|
32127
|
+
completedCount,
|
|
32128
|
+
cancelledCount,
|
|
32129
|
+
policy: config.policy
|
|
32130
|
+
});
|
|
32131
|
+
return { status: "SUCCESS" };
|
|
32132
|
+
}
|
|
32133
|
+
const completed = [];
|
|
32134
|
+
const completedIndices = new Set();
|
|
32135
|
+
const pendingEvents = [];
|
|
32136
|
+
const waiters = [];
|
|
32137
|
+
function push(event) {
|
|
32138
|
+
const w = waiters.shift();
|
|
32139
|
+
if (w !== void 0) w(event);
|
|
32140
|
+
else pendingEvents.push(event);
|
|
32141
|
+
}
|
|
32142
|
+
function waitForNext() {
|
|
32143
|
+
return new Promise((resolve$6) => {
|
|
32144
|
+
const e = pendingEvents.shift();
|
|
32145
|
+
if (e !== void 0) resolve$6(e);
|
|
32146
|
+
else waiters.push(resolve$6);
|
|
32147
|
+
});
|
|
32148
|
+
}
|
|
32149
|
+
const branchStarts = new Map();
|
|
32150
|
+
const activeTasks = branchEdges.map((edge, index) => {
|
|
32151
|
+
const task = makeBranchTask(edge, index, cancellationManager.getSignal(index), context, graph, options);
|
|
32152
|
+
options.eventBus?.emit("graph:parallel-branch-started", {
|
|
32153
|
+
runId,
|
|
32154
|
+
nodeId: node.id,
|
|
32155
|
+
branchIndex: index
|
|
32156
|
+
});
|
|
32157
|
+
branchStarts.set(index, Date.now());
|
|
32158
|
+
return task().then((result) => {
|
|
32159
|
+
const branchStart = branchStarts.get(index) ?? Date.now();
|
|
32160
|
+
const durationMs = Date.now() - branchStart;
|
|
32161
|
+
options.eventBus?.emit("graph:parallel-branch-completed", {
|
|
32162
|
+
runId,
|
|
32163
|
+
nodeId: node.id,
|
|
32164
|
+
branchIndex: index,
|
|
32165
|
+
status: branchOutcomeToStatus(result.outcome),
|
|
32166
|
+
durationMs
|
|
32167
|
+
});
|
|
32168
|
+
push({ result });
|
|
32169
|
+
});
|
|
32170
|
+
});
|
|
32171
|
+
let joinOutcome = "SUCCESS";
|
|
32172
|
+
let settled = false;
|
|
32173
|
+
let joinErrorMsg;
|
|
32174
|
+
for (let i = 0; i < branchCount; i++) {
|
|
32175
|
+
const { result } = await waitForNext();
|
|
32176
|
+
if (completedIndices.has(result.index)) continue;
|
|
32177
|
+
completed.push(result);
|
|
32178
|
+
completedIndices.add(result.index);
|
|
32179
|
+
const decision = evaluateJoinPolicy(config, completed, branchCount);
|
|
32180
|
+
if (decision.action === "wait") continue;
|
|
32181
|
+
settled = true;
|
|
32182
|
+
joinOutcome = decision.action === "continue" ? "SUCCESS" : "FAIL";
|
|
32183
|
+
if (decision.action === "fail") joinErrorMsg = decision.reason;
|
|
32184
|
+
const uncancelledCount = branchCount - completedIndices.size;
|
|
32185
|
+
cancellationManager.cancelRemaining(completedIndices);
|
|
32186
|
+
if (uncancelledCount > 0) {
|
|
32187
|
+
await cancellationManager.drainAsync(drainMs);
|
|
32188
|
+
while (pendingEvents.length > 0) {
|
|
32189
|
+
const e = pendingEvents.shift();
|
|
32190
|
+
if (e !== void 0 && !completedIndices.has(e.result.index)) {
|
|
32191
|
+
completed.push(e.result);
|
|
32192
|
+
completedIndices.add(e.result.index);
|
|
32193
|
+
}
|
|
32194
|
+
}
|
|
32195
|
+
for (let j$1 = 0; j$1 < branchCount; j$1++) if (!completedIndices.has(j$1)) {
|
|
32196
|
+
completed.push({
|
|
32197
|
+
index: j$1,
|
|
32198
|
+
outcome: "CANCELLED"
|
|
32199
|
+
});
|
|
32200
|
+
completedIndices.add(j$1);
|
|
32201
|
+
}
|
|
32202
|
+
}
|
|
32203
|
+
context.set("parallel.results", completed.map(toBridgeBranchResult));
|
|
32204
|
+
if (decision.action === "continue") {
|
|
32205
|
+
if (config.policy === "first_success") {
|
|
32206
|
+
const winner = completed.find((r) => r.outcome === "SUCCESS");
|
|
32207
|
+
if (winner !== void 0) context.set("parallel.winner_index", winner.index);
|
|
32208
|
+
}
|
|
32209
|
+
if (config.policy === "quorum") {
|
|
32210
|
+
const successCount = completed.filter((r) => r.outcome === "SUCCESS").length;
|
|
32211
|
+
context.set("parallel.quorum_reached", successCount);
|
|
32212
|
+
}
|
|
32213
|
+
} else context.set("parallel.join_error", joinErrorMsg ?? decision.reason);
|
|
32214
|
+
break;
|
|
32215
|
+
}
|
|
32216
|
+
Promise.allSettled(activeTasks);
|
|
32217
|
+
if (!settled) context.set("parallel.results", completed.map(toBridgeBranchResult));
|
|
32218
|
+
const finalCompletedCount = completed.filter((r) => r.outcome === "SUCCESS").length;
|
|
32219
|
+
const finalCancelledCount = completed.filter((r) => r.outcome === "CANCELLED").length;
|
|
32220
|
+
options.eventBus?.emit("graph:parallel-completed", {
|
|
32221
|
+
runId,
|
|
32222
|
+
nodeId: node.id,
|
|
32223
|
+
completedCount: finalCompletedCount,
|
|
32224
|
+
cancelledCount: finalCancelledCount,
|
|
32225
|
+
policy: config.policy
|
|
32226
|
+
});
|
|
32227
|
+
return { status: joinOutcome === "SUCCESS" ? "SUCCESS" : "FAILURE" };
|
|
32228
|
+
};
|
|
32229
|
+
}
|
|
32230
|
+
|
|
32231
|
+
//#endregion
|
|
32232
|
+
//#region packages/factory/dist/handlers/fan-in.js
|
|
32233
|
+
/** Lower number = better rank. FAILURE is excluded before ranking. */
|
|
32234
|
+
const OUTCOME_RANK = {
|
|
32235
|
+
SUCCESS: 0,
|
|
32236
|
+
PARTIAL_SUCCESS: 1,
|
|
32237
|
+
NEEDS_RETRY: 2,
|
|
32238
|
+
FAILURE: 3,
|
|
32239
|
+
ESCALATE: 4
|
|
32240
|
+
};
|
|
32241
|
+
/**
|
|
32242
|
+
* Select the best non-FAILURE branch from `results`.
|
|
32243
|
+
*
|
|
32244
|
+
* Sort order (ascending priority):
|
|
32245
|
+
* 1. `OUTCOME_RANK[status]` ascending (SUCCESS wins)
|
|
32246
|
+
* 2. `score` descending (higher is better; `undefined` treated as `0`)
|
|
32247
|
+
* 3. `branch_id` ascending (stable tiebreak)
|
|
32248
|
+
*
|
|
32249
|
+
* Returns `null` when every branch has status `FAILURE`.
|
|
32250
|
+
*/
|
|
32251
|
+
function rankBranches(results) {
|
|
32252
|
+
const eligible = results.filter((r) => r.status !== "FAILURE");
|
|
32253
|
+
if (eligible.length === 0) return null;
|
|
32254
|
+
const sorted = [...eligible].sort((a, b) => {
|
|
32255
|
+
const rankDiff = (OUTCOME_RANK[a.status] ?? 99) - (OUTCOME_RANK[b.status] ?? 99);
|
|
32256
|
+
if (rankDiff !== 0) return rankDiff;
|
|
32257
|
+
const scoreA = a.score ?? 0;
|
|
32258
|
+
const scoreB = b.score ?? 0;
|
|
32259
|
+
if (scoreB !== scoreA) return scoreB - scoreA;
|
|
32260
|
+
return a.branch_id - b.branch_id;
|
|
32261
|
+
});
|
|
32262
|
+
return sorted[0] ?? null;
|
|
32263
|
+
}
|
|
32264
|
+
/**
|
|
32265
|
+
* Build the prompt sent to the LLM for selection-based fan-in.
|
|
32266
|
+
*
|
|
32267
|
+
* The prompt prepends `nodePrompt`, then lists each branch with its
|
|
32268
|
+
* `branch_id`, `status`, `score`, and the keys present in `context_updates`
|
|
32269
|
+
* (values omitted for token efficiency). The LLM is instructed to reply with
|
|
32270
|
+
* just the integer `branch_id` of the best candidate.
|
|
32271
|
+
*/
|
|
32272
|
+
function buildSelectionPrompt(nodePrompt, results) {
|
|
32273
|
+
const branchSummaries = results.map((r) => {
|
|
32274
|
+
const contextKeys = r.context_updates && Object.keys(r.context_updates).length > 0 ? Object.keys(r.context_updates).join(", ") : "(none)";
|
|
32275
|
+
return `Branch ${r.branch_id}: status=${r.status}, score=${r.score ?? 0}, context_update_keys=[${contextKeys}]`;
|
|
32276
|
+
}).join("\n");
|
|
32277
|
+
return `${nodePrompt}\n\nParallel branch results:\n${branchSummaries}\n\nReply with only the integer branch_id of the best candidate.`;
|
|
32278
|
+
}
|
|
32279
|
+
/**
|
|
32280
|
+
* Parse an LLM response to extract the winning `branch_id`.
|
|
32281
|
+
*
|
|
32282
|
+
* Scans the response text for the first integer that matches a valid
|
|
32283
|
+
* `branch_id` in `results`. Returns the matching `BranchResult`, or `null`
|
|
32284
|
+
* (triggering heuristic fallback) if no valid branch_id is found.
|
|
32285
|
+
* Logs a warning on fallback.
|
|
32286
|
+
*/
|
|
32287
|
+
function parseLlmWinnerResponse(response, results) {
|
|
32288
|
+
const validIds = new Set(results.map((r) => r.branch_id));
|
|
32289
|
+
const matches = response.match(/\d+/g);
|
|
32290
|
+
if (matches) for (const m of matches) {
|
|
32291
|
+
const id = parseInt(m, 10);
|
|
32292
|
+
if (validIds.has(id)) return results.find((r) => r.branch_id === id) ?? null;
|
|
32293
|
+
}
|
|
32294
|
+
console.warn(`[fan-in] LLM response did not contain a valid branch_id; falling back to heuristic selection. Response: "${response}"`);
|
|
32295
|
+
return null;
|
|
32296
|
+
}
|
|
32297
|
+
/**
|
|
32298
|
+
* Default `llmCall` implementation that wraps `callLLM` from `@substrate-ai/core`
|
|
32299
|
+
* with fixed default routing parameters.
|
|
32300
|
+
*/
|
|
32301
|
+
async function defaultLlmCall(prompt) {
|
|
32302
|
+
const result = await callLLM({
|
|
32303
|
+
model: "claude-sonnet-4-5",
|
|
32304
|
+
provider: "anthropic",
|
|
32305
|
+
reasoningEffort: "low",
|
|
32306
|
+
prompt
|
|
32307
|
+
});
|
|
32308
|
+
return result.text;
|
|
32309
|
+
}
|
|
32310
|
+
/**
|
|
32311
|
+
* Create a `parallel.fan_in` node handler.
|
|
32312
|
+
*
|
|
32313
|
+
* Execution steps:
|
|
32314
|
+
* 1. Read `parallel.results` from context; return FAILURE if absent or empty.
|
|
32315
|
+
* 2. If `node.prompt` is non-empty, call LLM to select winner; fall back to
|
|
32316
|
+
* heuristic on parse failure.
|
|
32317
|
+
* 3. Use heuristic `rankBranches` when no prompt or LLM fallback.
|
|
32318
|
+
* 4. If all branches failed, return FAILURE with aggregated failure reasons.
|
|
32319
|
+
* 5. Merge winner's `context_updates`, set `parallel.fan_in.best_id` and
|
|
32320
|
+
* `parallel.fan_in.best_outcome`, return SUCCESS.
|
|
32321
|
+
*
|
|
32322
|
+
* @param options - Optional configuration (inject `llmCall` for testing).
|
|
32323
|
+
*/
|
|
32324
|
+
function createFanInHandler(options) {
|
|
32325
|
+
const llmCallFn = options?.llmCall ?? defaultLlmCall;
|
|
32326
|
+
return async (node, context, _graph) => {
|
|
32327
|
+
const rawResults = context.get("parallel.results");
|
|
32328
|
+
if (!rawResults || !Array.isArray(rawResults) || rawResults.length === 0) return {
|
|
32329
|
+
status: "FAILURE",
|
|
32330
|
+
failureReason: "fan-in: no parallel results found in context (parallel.results is absent or empty)"
|
|
32331
|
+
};
|
|
32332
|
+
const results = rawResults;
|
|
32333
|
+
let winner = null;
|
|
32334
|
+
if (node.prompt && node.prompt.trim().length > 0) {
|
|
32335
|
+
const prompt = buildSelectionPrompt(node.prompt, results);
|
|
32336
|
+
try {
|
|
32337
|
+
const response = await llmCallFn(prompt);
|
|
32338
|
+
winner = parseLlmWinnerResponse(response, results);
|
|
32339
|
+
} catch (err) {
|
|
32340
|
+
console.warn(`[fan-in] LLM call failed; falling back to heuristic selection. Error: ${String(err)}`);
|
|
32341
|
+
winner = null;
|
|
32342
|
+
}
|
|
32343
|
+
if (winner === null) winner = rankBranches(results);
|
|
32344
|
+
} else winner = rankBranches(results);
|
|
32345
|
+
if (winner === null) {
|
|
32346
|
+
const reasons = results.map((r) => r.failure_reason ?? `branch ${r.branch_id}: no reason provided`).join("; ");
|
|
32347
|
+
return {
|
|
32348
|
+
status: "FAILURE",
|
|
32349
|
+
failureReason: `fan-in: all branches failed — ${reasons}`
|
|
32350
|
+
};
|
|
32351
|
+
}
|
|
32352
|
+
if (winner.context_updates && Object.keys(winner.context_updates).length > 0) context.applyUpdates(winner.context_updates);
|
|
32353
|
+
context.set("parallel.fan_in.best_id", winner.branch_id);
|
|
32354
|
+
context.set("parallel.fan_in.best_outcome", winner.status);
|
|
32355
|
+
return { status: "SUCCESS" };
|
|
32356
|
+
};
|
|
32357
|
+
}
|
|
32358
|
+
|
|
32359
|
+
//#endregion
|
|
32360
|
+
//#region packages/factory/dist/handlers/subgraph.js
|
|
32361
|
+
/**
|
|
32362
|
+
* Converts the sub-executor's `events.ts:StageStatus` back to the handler
|
|
32363
|
+
* return type `types.ts:OutcomeStatus`.
|
|
32364
|
+
*
|
|
32365
|
+
* Mapping:
|
|
32366
|
+
* 'SUCCESS' → 'SUCCESS'
|
|
32367
|
+
* 'PARTIAL_SUCCESS'→ 'PARTIAL_SUCCESS'
|
|
32368
|
+
* all others → 'FAILURE' (covers 'FAIL', 'RETRY', 'SKIPPED')
|
|
32369
|
+
*/
|
|
32370
|
+
function denormalizeStatus(status) {
|
|
32371
|
+
if (status === "SUCCESS") return "SUCCESS";
|
|
32372
|
+
if (status === "PARTIAL_SUCCESS") return "PARTIAL_SUCCESS";
|
|
32373
|
+
return "FAILURE";
|
|
32374
|
+
}
|
|
32375
|
+
/**
|
|
32376
|
+
* Factory function that creates a handler for `type="subgraph"` nodes.
|
|
32377
|
+
*
|
|
32378
|
+
* Each subgraph node references an external `.dot` file, loads and validates
|
|
32379
|
+
* it, then executes it as a nested sub-pipeline. Parent context is seeded into
|
|
32380
|
+
* the sub-executor and context updates from the subgraph are propagated back.
|
|
32381
|
+
*/
|
|
32382
|
+
function createSubgraphHandler(options) {
|
|
32383
|
+
return async (node, context, graph) => {
|
|
32384
|
+
const graphFile = node.attrs?.["graph_file"];
|
|
32385
|
+
if (!graphFile) return {
|
|
32386
|
+
status: "FAILURE",
|
|
32387
|
+
failureReason: `Subgraph node "${node.id}" is missing required attribute graph_file`
|
|
32388
|
+
};
|
|
32389
|
+
const currentDepth = context.getNumber("subgraph._depth", 0);
|
|
32390
|
+
const maxDepth = options.maxDepth ?? 5;
|
|
32391
|
+
if (currentDepth >= maxDepth) return {
|
|
32392
|
+
status: "FAILURE",
|
|
32393
|
+
failureReason: `Subgraph depth limit exceeded (max ${maxDepth}): node "${node.id}"`
|
|
32394
|
+
};
|
|
32395
|
+
const filePath = path.isAbsolute(graphFile) ? graphFile : path.join(options.baseDir, graphFile);
|
|
32396
|
+
const loader = options.graphFileLoader ?? ((fp) => readFile$1(fp, "utf-8"));
|
|
32397
|
+
let dotSource;
|
|
32398
|
+
try {
|
|
32399
|
+
dotSource = await loader(filePath);
|
|
32400
|
+
} catch (err) {
|
|
32401
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
32402
|
+
return {
|
|
32403
|
+
status: "FAILURE",
|
|
32404
|
+
failureReason: `Subgraph node "${node.id}": failed to load "${filePath}": ${msg}`
|
|
32405
|
+
};
|
|
32406
|
+
}
|
|
32407
|
+
let subgraph;
|
|
32408
|
+
try {
|
|
32409
|
+
subgraph = parseGraph(dotSource);
|
|
32410
|
+
} catch (err) {
|
|
32411
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
32412
|
+
return {
|
|
32413
|
+
status: "FAILURE",
|
|
32414
|
+
failureReason: `Subgraph node "${node.id}": failed to parse "${filePath}": ${msg}`
|
|
32415
|
+
};
|
|
32416
|
+
}
|
|
32417
|
+
try {
|
|
32418
|
+
createValidator().validateOrRaise(subgraph);
|
|
32419
|
+
} catch (err) {
|
|
32420
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
32421
|
+
return {
|
|
32422
|
+
status: "FAILURE",
|
|
32423
|
+
failureReason: `Subgraph node "${node.id}": validation failed for "${filePath}": ${msg}`
|
|
32424
|
+
};
|
|
32425
|
+
}
|
|
32426
|
+
const runId = context.getString("__runId", options.runId ?? "unknown");
|
|
32427
|
+
options.eventBus?.emit("graph:subgraph-started", {
|
|
32428
|
+
runId,
|
|
32429
|
+
nodeId: node.id,
|
|
32430
|
+
graphFile: filePath,
|
|
32431
|
+
depth: currentDepth
|
|
32432
|
+
});
|
|
32433
|
+
const subgraphStart = Date.now();
|
|
32434
|
+
const parentStylesheet = graph.modelStylesheet ? parseStylesheet(graph.modelStylesheet) : void 0;
|
|
32435
|
+
const subConfig = {
|
|
32436
|
+
runId: randomUUID(),
|
|
32437
|
+
logsRoot: options.logsRoot ?? tmpdir(),
|
|
32438
|
+
handlerRegistry: options.handlerRegistry,
|
|
32439
|
+
initialContext: {
|
|
32440
|
+
...context.snapshot(),
|
|
32441
|
+
"subgraph._depth": currentDepth + 1
|
|
32442
|
+
},
|
|
32443
|
+
...options.eventBus !== void 0 ? { eventBus: options.eventBus } : {},
|
|
32444
|
+
...parentStylesheet !== void 0 ? { inheritedStylesheet: parentStylesheet } : {}
|
|
32445
|
+
};
|
|
32446
|
+
let subOutcome;
|
|
32447
|
+
try {
|
|
32448
|
+
subOutcome = await createGraphExecutor().run(subgraph, subConfig);
|
|
32449
|
+
} catch (err) {
|
|
32450
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
32451
|
+
const durationMs$1 = Date.now() - subgraphStart;
|
|
32452
|
+
options.eventBus?.emit("graph:subgraph-completed", {
|
|
32453
|
+
runId,
|
|
32454
|
+
nodeId: node.id,
|
|
32455
|
+
graphFile: filePath,
|
|
32456
|
+
depth: currentDepth,
|
|
32457
|
+
status: "FAIL",
|
|
32458
|
+
durationMs: durationMs$1
|
|
32459
|
+
});
|
|
32460
|
+
return {
|
|
32461
|
+
status: "FAILURE",
|
|
32462
|
+
failureReason: `Subgraph node "${node.id}": executor threw: ${msg}`
|
|
32463
|
+
};
|
|
32464
|
+
}
|
|
32465
|
+
const durationMs = Date.now() - subgraphStart;
|
|
32466
|
+
options.eventBus?.emit("graph:subgraph-completed", {
|
|
32467
|
+
runId,
|
|
32468
|
+
nodeId: node.id,
|
|
32469
|
+
graphFile: filePath,
|
|
32470
|
+
depth: currentDepth,
|
|
32471
|
+
status: subOutcome.status === "SUCCESS" ? "SUCCESS" : subOutcome.status === "PARTIAL_SUCCESS" ? "PARTIAL_SUCCESS" : "FAIL",
|
|
32472
|
+
durationMs
|
|
32473
|
+
});
|
|
32474
|
+
if (subOutcome.contextUpdates) context.applyUpdates(subOutcome.contextUpdates);
|
|
32475
|
+
return {
|
|
32476
|
+
status: denormalizeStatus(subOutcome.status),
|
|
32477
|
+
...subOutcome.contextUpdates !== void 0 && { contextUpdates: subOutcome.contextUpdates },
|
|
32478
|
+
...subOutcome.notes !== void 0 && { notes: subOutcome.notes },
|
|
32479
|
+
...subOutcome.failureReason !== void 0 && { failureReason: subOutcome.failureReason }
|
|
32480
|
+
};
|
|
32481
|
+
};
|
|
32482
|
+
}
|
|
32483
|
+
|
|
32484
|
+
//#endregion
|
|
32485
|
+
//#region packages/factory/dist/handlers/manager-loop.js
|
|
32486
|
+
/**
|
|
32487
|
+
* Factory function that creates a handler for `type="stack.manager_loop"` nodes.
|
|
32488
|
+
*
|
|
32489
|
+
* Each invocation loads the body graph once, then executes it in a loop for up
|
|
32490
|
+
* to `max_cycles` cycles (default 10). Stop conditions (context key or LLM-based)
|
|
32491
|
+
* allow early exit. Stall detection injects recovery steering after consecutive
|
|
32492
|
+
* non-SUCCESS body executions.
|
|
32493
|
+
*/
|
|
32494
|
+
function createManagerLoopHandler(options) {
|
|
32495
|
+
return async (node, context, _graph) => {
|
|
32496
|
+
const graphFile = node.attrs?.["graph_file"];
|
|
32497
|
+
if (!graphFile) return {
|
|
32498
|
+
status: "FAILURE",
|
|
32499
|
+
failureReason: `Manager loop node "${node.id}" is missing required attribute graph_file`
|
|
32500
|
+
};
|
|
32501
|
+
const rawMaxCycles = node.attrs?.["max_cycles"];
|
|
32502
|
+
let maxCycles = 10;
|
|
32503
|
+
if (rawMaxCycles !== void 0 && rawMaxCycles !== "") {
|
|
32504
|
+
const parsed = parseInt(rawMaxCycles, 10);
|
|
32505
|
+
maxCycles = isNaN(parsed) ? 10 : Math.max(1, parsed);
|
|
32506
|
+
}
|
|
32507
|
+
const filePath = path.isAbsolute(graphFile) ? graphFile : path.join(options.baseDir ?? process.cwd(), graphFile);
|
|
32508
|
+
const loader = options.graphFileLoader ?? ((fp) => readFile$1(fp, "utf-8"));
|
|
32509
|
+
let dotSource;
|
|
32510
|
+
try {
|
|
32511
|
+
dotSource = await loader(filePath);
|
|
32512
|
+
} catch (err) {
|
|
32513
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
32514
|
+
return {
|
|
32515
|
+
status: "FAILURE",
|
|
32516
|
+
failureReason: `Manager loop node "${node.id}": failed to load "${filePath}": ${msg}`
|
|
32517
|
+
};
|
|
32518
|
+
}
|
|
32519
|
+
let bodyGraph;
|
|
32520
|
+
try {
|
|
32521
|
+
bodyGraph = parseGraph(dotSource);
|
|
32522
|
+
} catch (err) {
|
|
32523
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
32524
|
+
return {
|
|
32525
|
+
status: "FAILURE",
|
|
32526
|
+
failureReason: `Manager loop node "${node.id}": failed to parse "${filePath}": ${msg}`
|
|
32527
|
+
};
|
|
32528
|
+
}
|
|
32529
|
+
try {
|
|
32530
|
+
createValidator().validateOrRaise(bodyGraph);
|
|
32531
|
+
} catch (err) {
|
|
32532
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
32533
|
+
return {
|
|
32534
|
+
status: "FAILURE",
|
|
32535
|
+
failureReason: `Manager loop node "${node.id}": validation failed for "${filePath}": ${msg}`
|
|
32536
|
+
};
|
|
32537
|
+
}
|
|
32538
|
+
const stopCondition = node.attrs?.["stop_condition"];
|
|
32539
|
+
let consecutiveFailures = 0;
|
|
32540
|
+
for (let cycle = 1; cycle <= maxCycles; cycle++) {
|
|
32541
|
+
context.set("manager_loop.cycle", cycle);
|
|
32542
|
+
const bodyConfig = {
|
|
32543
|
+
runId: randomUUID(),
|
|
32544
|
+
logsRoot: options.logsRoot ?? tmpdir(),
|
|
32545
|
+
handlerRegistry: options.handlerRegistry,
|
|
32546
|
+
initialContext: context.snapshot()
|
|
32547
|
+
};
|
|
32548
|
+
const bodyOutcome = await createGraphExecutor().run(bodyGraph, bodyConfig);
|
|
32549
|
+
if (bodyOutcome.contextUpdates) context.applyUpdates(bodyOutcome.contextUpdates);
|
|
32550
|
+
context.set("manager_loop.cycles_completed", cycle);
|
|
32551
|
+
context.set("manager_loop.last_outcome", bodyOutcome.status);
|
|
32552
|
+
if (bodyOutcome.status === "SUCCESS") {
|
|
32553
|
+
consecutiveFailures = 0;
|
|
32554
|
+
context.set("manager_loop.steering.mode", "normal");
|
|
32555
|
+
context.set("manager_loop.steering.hints", []);
|
|
32556
|
+
} else {
|
|
32557
|
+
consecutiveFailures++;
|
|
32558
|
+
if (consecutiveFailures >= (options.maxStallCycles ?? 2)) {
|
|
32559
|
+
context.set("manager_loop.steering.mode", "recovery");
|
|
32560
|
+
context.set("manager_loop.steering.hints", [`Previous ${consecutiveFailures} attempts returned ${bodyOutcome.status}. Consider a different strategy.`, "Review context state and adjust approach before retrying."]);
|
|
32561
|
+
}
|
|
32562
|
+
}
|
|
32563
|
+
if (stopCondition) {
|
|
32564
|
+
let shouldStop = false;
|
|
32565
|
+
if (isLlmCondition(stopCondition)) {
|
|
32566
|
+
if (options.llmCall !== void 0) {
|
|
32567
|
+
const question = extractLlmQuestion(stopCondition);
|
|
32568
|
+
shouldStop = await evaluateLlmCondition(question, context.snapshot(), options.llmCall);
|
|
32569
|
+
}
|
|
32570
|
+
} else shouldStop = Boolean(context.get(stopCondition));
|
|
32571
|
+
if (shouldStop) {
|
|
32572
|
+
context.set("manager_loop.stop_reason", "stop_condition");
|
|
32573
|
+
return { status: "SUCCESS" };
|
|
32574
|
+
}
|
|
32575
|
+
}
|
|
32576
|
+
}
|
|
32577
|
+
context.set("manager_loop.stop_reason", "max_cycles");
|
|
32578
|
+
return { status: "SUCCESS" };
|
|
32579
|
+
};
|
|
32580
|
+
}
|
|
32581
|
+
|
|
31425
32582
|
//#endregion
|
|
31426
32583
|
//#region packages/factory/dist/handlers/registry.js
|
|
31427
32584
|
var HandlerRegistry = class {
|
|
@@ -31480,19 +32637,40 @@ var HandlerRegistry = class {
|
|
|
31480
32637
|
* Story 42-10 adds codergen as the explicit "codergen" type, as the handler for
|
|
31481
32638
|
* `shape=box` nodes, and as the registry-level default for any node that has no
|
|
31482
32639
|
* recognised type or shape mapping.
|
|
32640
|
+
*
|
|
32641
|
+
* Story 50-5 extends options to `DefaultRegistryOptions` and registers the `subgraph` type.
|
|
31483
32642
|
*/
|
|
31484
32643
|
function createDefaultRegistry(options) {
|
|
31485
32644
|
const registry = new HandlerRegistry();
|
|
32645
|
+
registry.register("parallel", createParallelHandler({
|
|
32646
|
+
handlerRegistry: registry,
|
|
32647
|
+
...options?.eventBus !== void 0 ? { eventBus: options.eventBus } : {},
|
|
32648
|
+
...options?.runId !== void 0 ? { runId: options.runId } : {}
|
|
32649
|
+
}));
|
|
32650
|
+
registry.registerShape("component", "parallel");
|
|
31486
32651
|
registry.register("start", startHandler);
|
|
31487
32652
|
registry.register("exit", exitHandler);
|
|
31488
32653
|
registry.register("conditional", conditionalHandler);
|
|
31489
32654
|
registry.register("codergen", createCodergenHandler(options));
|
|
31490
32655
|
registry.register("tool", createToolHandler());
|
|
31491
32656
|
registry.register("wait.human", createWaitHumanHandler());
|
|
32657
|
+
registry.register("parallel.fan_in", createFanInHandler());
|
|
32658
|
+
registry.register("subgraph", createSubgraphHandler({
|
|
32659
|
+
handlerRegistry: registry,
|
|
32660
|
+
baseDir: options?.baseDir ?? process.cwd(),
|
|
32661
|
+
...options?.eventBus !== void 0 ? { eventBus: options.eventBus } : {},
|
|
32662
|
+
...options?.runId !== void 0 ? { runId: options.runId } : {}
|
|
32663
|
+
}));
|
|
32664
|
+
registry.register("stack.manager_loop", createManagerLoopHandler({
|
|
32665
|
+
handlerRegistry: registry,
|
|
32666
|
+
baseDir: options?.baseDir ?? process.cwd(),
|
|
32667
|
+
...options?.llmCall !== void 0 ? { llmCall: options.llmCall } : {}
|
|
32668
|
+
}));
|
|
31492
32669
|
registry.registerShape("Mdiamond", "start");
|
|
31493
32670
|
registry.registerShape("Msquare", "exit");
|
|
31494
32671
|
registry.registerShape("diamond", "conditional");
|
|
31495
32672
|
registry.registerShape("box", "codergen");
|
|
32673
|
+
registry.registerShape("tripleoctagon", "parallel.fan_in");
|
|
31496
32674
|
registry.setDefault(createCodergenHandler(options));
|
|
31497
32675
|
return registry;
|
|
31498
32676
|
}
|
|
@@ -36738,6 +37916,149 @@ function registerContextCommand(factoryCmd, version, storageDir, engineFactory)
|
|
|
36738
37916
|
});
|
|
36739
37917
|
}
|
|
36740
37918
|
|
|
37919
|
+
//#endregion
|
|
37920
|
+
//#region packages/factory/dist/templates/index.js
|
|
37921
|
+
/**
|
|
37922
|
+
* Pipeline Template Catalog — pre-built DOT graph pipeline templates for common patterns.
|
|
37923
|
+
*
|
|
37924
|
+
* Story 50-10.
|
|
37925
|
+
*
|
|
37926
|
+
* Node type strings used:
|
|
37927
|
+
* - `start` — start node (registered by default registry)
|
|
37928
|
+
* - `exit` — exit node (registered by default registry)
|
|
37929
|
+
* - `codergen` — coding agent node (registered by default registry)
|
|
37930
|
+
* - `parallel` — parallel fan-out node (story 50-1, shape=component)
|
|
37931
|
+
* - `parallel.fan_in`— fan-in/merge node (story 50-2, shape=tripleoctagon)
|
|
37932
|
+
*/
|
|
37933
|
+
const trycycleDotContent = `digraph trycycle {
|
|
37934
|
+
// Trycycle Pattern: iterative refinement with eval gates
|
|
37935
|
+
// Flow: define → plan → eval_plan ⇄ implement → eval_impl → exit
|
|
37936
|
+
// eval nodes can loop back to their upstream node on revision_needed.
|
|
37937
|
+
|
|
37938
|
+
start [type="start"];
|
|
37939
|
+
define [type="codergen", label="Define Requirements"];
|
|
37940
|
+
plan [type="codergen", label="Plan Implementation"];
|
|
37941
|
+
eval_plan [type="codergen", label="Evaluate Plan"];
|
|
37942
|
+
implement [type="codergen", label="Implement"];
|
|
37943
|
+
eval_impl [type="codergen", label="Evaluate Implementation"];
|
|
37944
|
+
exit [type="exit"];
|
|
37945
|
+
|
|
37946
|
+
start -> define;
|
|
37947
|
+
define -> plan;
|
|
37948
|
+
plan -> eval_plan;
|
|
37949
|
+
eval_plan -> implement [label="approved"];
|
|
37950
|
+
eval_plan -> plan [label="revision_needed"];
|
|
37951
|
+
implement -> eval_impl;
|
|
37952
|
+
eval_impl -> exit [label="approved"];
|
|
37953
|
+
eval_impl -> implement [label="revision_needed"];
|
|
37954
|
+
}
|
|
37955
|
+
`;
|
|
37956
|
+
const dualReviewDotContent = `digraph dual_review {
|
|
37957
|
+
// Dual-Review Pattern: fan-out to two independent reviewers, then fan-in
|
|
37958
|
+
// Flow: implement → (reviewer_a + reviewer_b) in parallel → merge → exit
|
|
37959
|
+
|
|
37960
|
+
start [type="start"];
|
|
37961
|
+
implement [type="codergen", label="Implement"];
|
|
37962
|
+
review_parallel [type="parallel", label="Fan-Out to Reviewers"];
|
|
37963
|
+
reviewer_a [type="codergen", label="Reviewer A"];
|
|
37964
|
+
reviewer_b [type="codergen", label="Reviewer B"];
|
|
37965
|
+
review_merge [type="parallel.fan_in",label="Merge Reviews"];
|
|
37966
|
+
exit [type="exit"];
|
|
37967
|
+
|
|
37968
|
+
start -> implement;
|
|
37969
|
+
implement -> review_parallel;
|
|
37970
|
+
review_parallel -> reviewer_a;
|
|
37971
|
+
review_parallel -> reviewer_b;
|
|
37972
|
+
reviewer_a -> review_merge;
|
|
37973
|
+
reviewer_b -> review_merge;
|
|
37974
|
+
review_merge -> exit;
|
|
37975
|
+
}
|
|
37976
|
+
`;
|
|
37977
|
+
const parallelExplorationDotContent = `digraph parallel_exploration {
|
|
37978
|
+
// Parallel-Exploration Pattern: dispatch multiple approaches concurrently,
|
|
37979
|
+
// select the best-scoring result, then refine the winner.
|
|
37980
|
+
// Flow: (approach_a + approach_b) in parallel → select best → refine → exit
|
|
37981
|
+
|
|
37982
|
+
start [type="start"];
|
|
37983
|
+
explore_parallel[type="parallel", label="Fan-Out Exploration"];
|
|
37984
|
+
approach_a [type="codergen", label="Approach A"];
|
|
37985
|
+
approach_b [type="codergen", label="Approach B"];
|
|
37986
|
+
select_best [type="parallel.fan_in", label="Select Best Candidate", selection="best"];
|
|
37987
|
+
refine [type="codergen", label="Refine Winner"];
|
|
37988
|
+
exit [type="exit"];
|
|
37989
|
+
|
|
37990
|
+
start -> explore_parallel;
|
|
37991
|
+
explore_parallel -> approach_a;
|
|
37992
|
+
explore_parallel -> approach_b;
|
|
37993
|
+
approach_a -> select_best;
|
|
37994
|
+
approach_b -> select_best;
|
|
37995
|
+
select_best -> refine;
|
|
37996
|
+
refine -> exit;
|
|
37997
|
+
}
|
|
37998
|
+
`;
|
|
37999
|
+
const stagedValidationDotContent = `digraph staged_validation {
|
|
38000
|
+
// Staged-Validation Pattern: sequential quality gates
|
|
38001
|
+
// Flow: implement → lint → test → validate → exit
|
|
38002
|
+
// Each stage is a separate codergen node; no parallel branches.
|
|
38003
|
+
|
|
38004
|
+
start [type="start"];
|
|
38005
|
+
implement[type="codergen", label="Implement"];
|
|
38006
|
+
lint [type="codergen", label="Lint"];
|
|
38007
|
+
test [type="codergen", label="Test"];
|
|
38008
|
+
validate [type="codergen", label="Validate"];
|
|
38009
|
+
exit [type="exit"];
|
|
38010
|
+
|
|
38011
|
+
start -> implement;
|
|
38012
|
+
implement -> lint;
|
|
38013
|
+
lint -> test;
|
|
38014
|
+
test -> validate;
|
|
38015
|
+
validate -> exit;
|
|
38016
|
+
}
|
|
38017
|
+
`;
|
|
38018
|
+
const trycycleTemplate = {
|
|
38019
|
+
name: "trycycle",
|
|
38020
|
+
description: "Iterative refinement loop with plan and implementation eval gates",
|
|
38021
|
+
dotContent: trycycleDotContent
|
|
38022
|
+
};
|
|
38023
|
+
const dualReviewTemplate = {
|
|
38024
|
+
name: "dual-review",
|
|
38025
|
+
description: "Fan-out to two independent reviewers, then fan-in to merge results",
|
|
38026
|
+
dotContent: dualReviewDotContent
|
|
38027
|
+
};
|
|
38028
|
+
const parallelExplorationTemplate = {
|
|
38029
|
+
name: "parallel-exploration",
|
|
38030
|
+
description: "Dispatch parallel implementation approaches and select the best candidate",
|
|
38031
|
+
dotContent: parallelExplorationDotContent
|
|
38032
|
+
};
|
|
38033
|
+
const stagedValidationTemplate = {
|
|
38034
|
+
name: "staged-validation",
|
|
38035
|
+
description: "Sequential quality gate stages: implement → lint → test → validate",
|
|
38036
|
+
dotContent: stagedValidationDotContent
|
|
38037
|
+
};
|
|
38038
|
+
/**
|
|
38039
|
+
* Map of all built-in pipeline templates, keyed by template name.
|
|
38040
|
+
* Insertion order determines display order in `factory templates list`.
|
|
38041
|
+
*/
|
|
38042
|
+
const PIPELINE_TEMPLATES = new Map([
|
|
38043
|
+
[trycycleTemplate.name, trycycleTemplate],
|
|
38044
|
+
[dualReviewTemplate.name, dualReviewTemplate],
|
|
38045
|
+
[parallelExplorationTemplate.name, parallelExplorationTemplate],
|
|
38046
|
+
[stagedValidationTemplate.name, stagedValidationTemplate]
|
|
38047
|
+
]);
|
|
38048
|
+
/**
|
|
38049
|
+
* Returns all available pipeline template entries in insertion order.
|
|
38050
|
+
*/
|
|
38051
|
+
function listPipelineTemplates() {
|
|
38052
|
+
return Array.from(PIPELINE_TEMPLATES.values());
|
|
38053
|
+
}
|
|
38054
|
+
/**
|
|
38055
|
+
* Returns the pipeline template entry for the given name (case-sensitive),
|
|
38056
|
+
* or `undefined` if not found.
|
|
38057
|
+
*/
|
|
38058
|
+
function getPipelineTemplate(name) {
|
|
38059
|
+
return PIPELINE_TEMPLATES.get(name);
|
|
38060
|
+
}
|
|
38061
|
+
|
|
36741
38062
|
//#endregion
|
|
36742
38063
|
//#region packages/factory/dist/twins/schema.js
|
|
36743
38064
|
/**
|
|
@@ -37776,6 +39097,28 @@ function registerFactoryCommand(program, options) {
|
|
|
37776
39097
|
process.exit(1);
|
|
37777
39098
|
}
|
|
37778
39099
|
});
|
|
39100
|
+
const templatesCmd = factoryCmd.command("templates").description("Manage reusable DOT graph pipeline templates");
|
|
39101
|
+
templatesCmd.command("list").description("List available pipeline templates").action(() => {
|
|
39102
|
+
const templates = listPipelineTemplates();
|
|
39103
|
+
for (const t of templates) process.stdout.write(` ${t.name.padEnd(24)} ${t.description}\n`);
|
|
39104
|
+
});
|
|
39105
|
+
templatesCmd.command("init").description("Create a pipeline.dot from a template").requiredOption("--template <name>", "Template name (see: factory templates list)").option("--output <path>", "Output file path (default: pipeline.dot)", "pipeline.dot").action(async (opts) => {
|
|
39106
|
+
const entry = getPipelineTemplate(opts.template);
|
|
39107
|
+
if (!entry) {
|
|
39108
|
+
const available = listPipelineTemplates().map((t) => t.name).join(", ");
|
|
39109
|
+
process.stderr.write(`Error: Unknown template '${opts.template}'. Available: ${available}\n`);
|
|
39110
|
+
process.exit(1);
|
|
39111
|
+
return;
|
|
39112
|
+
}
|
|
39113
|
+
try {
|
|
39114
|
+
await writeFile$1(opts.output, entry.dotContent, "utf-8");
|
|
39115
|
+
process.stdout.write(`Created ${opts.output} from template '${entry.name}'\n`);
|
|
39116
|
+
} catch (err) {
|
|
39117
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
39118
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
39119
|
+
process.exit(1);
|
|
39120
|
+
}
|
|
39121
|
+
});
|
|
37779
39122
|
}
|
|
37780
39123
|
|
|
37781
39124
|
//#endregion
|
|
@@ -38104,6 +39447,7 @@ async function runRunAction(options) {
|
|
|
38104
39447
|
});
|
|
38105
39448
|
} catch {}
|
|
38106
39449
|
let tokenCeilings;
|
|
39450
|
+
let dispatchTimeouts;
|
|
38107
39451
|
let telemetryEnabled = false;
|
|
38108
39452
|
let telemetryPort = 4318;
|
|
38109
39453
|
try {
|
|
@@ -38111,6 +39455,10 @@ async function runRunAction(options) {
|
|
|
38111
39455
|
await configSystem.load();
|
|
38112
39456
|
const cfg = configSystem.getConfig();
|
|
38113
39457
|
tokenCeilings = cfg.token_ceilings;
|
|
39458
|
+
if (cfg.dispatch_timeouts) {
|
|
39459
|
+
dispatchTimeouts = Object.fromEntries(Object.entries(cfg.dispatch_timeouts).filter(([, v]) => v !== void 0));
|
|
39460
|
+
logger.info({ dispatchTimeouts }, "Loaded dispatch timeout overrides from config");
|
|
39461
|
+
}
|
|
38114
39462
|
if (cfg.telemetry?.enabled === true) {
|
|
38115
39463
|
telemetryEnabled = true;
|
|
38116
39464
|
telemetryPort = cfg.telemetry.port ?? 4318;
|
|
@@ -38397,7 +39745,10 @@ async function runRunAction(options) {
|
|
|
38397
39745
|
const dispatcher = createDispatcher({
|
|
38398
39746
|
eventBus,
|
|
38399
39747
|
adapterRegistry: injectedRegistry,
|
|
38400
|
-
config: {
|
|
39748
|
+
config: {
|
|
39749
|
+
routingResolver,
|
|
39750
|
+
...dispatchTimeouts ? { defaultTimeouts: dispatchTimeouts } : {}
|
|
39751
|
+
}
|
|
38401
39752
|
});
|
|
38402
39753
|
eventBus.on("orchestrator:story-phase-complete", (payload) => {
|
|
38403
39754
|
try {
|
|
@@ -39264,4 +40615,4 @@ function registerRunCommand(program, _version = "0.0.0", projectRoot = process.c
|
|
|
39264
40615
|
|
|
39265
40616
|
//#endregion
|
|
39266
40617
|
export { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, GitClient, GrammarLoader, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, getTwinRunsForRun, listGraphRuns, normalizeGraphSummaryToStatus, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runRunAction, runSolutioningPhase, validateStopAfterFromConflict };
|
|
39267
|
-
//# sourceMappingURL=run-
|
|
40618
|
+
//# sourceMappingURL=run-Byzy10gG.js.map
|