agentweaver 0.1.15 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -9
- package/dist/artifact-manifest.js +219 -0
- package/dist/artifacts.js +15 -0
- package/dist/doctor/checks/env-diagnostics.js +25 -0
- package/dist/doctor/checks/flow-readiness.js +15 -18
- package/dist/flow-state.js +75 -15
- package/dist/index.js +391 -175
- package/dist/interactive/blessed-session.js +361 -0
- package/dist/interactive/controller.js +1293 -0
- package/dist/interactive/create-interactive-session.js +5 -0
- package/dist/interactive/ink/index.js +576 -0
- package/dist/interactive/progress.js +245 -0
- package/dist/interactive/selectors.js +14 -0
- package/dist/interactive/session.js +1 -0
- package/dist/interactive/state.js +34 -0
- package/dist/interactive/tree.js +155 -0
- package/dist/interactive/types.js +1 -0
- package/dist/interactive/view-model.js +1 -0
- package/dist/interactive-ui.js +159 -194
- package/dist/pipeline/context.js +1 -0
- package/dist/pipeline/declarative-flow-runner.js +212 -6
- package/dist/pipeline/declarative-flows.js +27 -0
- package/dist/pipeline/execution-routing-config.js +15 -0
- package/dist/pipeline/flow-catalog.js +19 -3
- package/dist/pipeline/flow-run-resume.js +29 -0
- package/dist/pipeline/flow-specs/auto-common.json +89 -360
- package/dist/pipeline/flow-specs/auto-golang.json +58 -363
- package/dist/pipeline/flow-specs/auto-simple.json +141 -0
- package/dist/pipeline/flow-specs/bugz/bug-analyze.json +2 -0
- package/dist/pipeline/flow-specs/bugz/bug-fix.json +1 -0
- package/dist/pipeline/flow-specs/design-review/design-review-loop.json +304 -0
- package/dist/pipeline/flow-specs/design-review.json +10 -0
- package/dist/pipeline/flow-specs/gitlab/gitlab-diff-review.json +11 -0
- package/dist/pipeline/flow-specs/gitlab/gitlab-review.json +2 -0
- package/dist/pipeline/flow-specs/gitlab/mr-description.json +1 -0
- package/dist/pipeline/flow-specs/go/run-go-linter-loop.json +2 -0
- package/dist/pipeline/flow-specs/go/run-go-tests-loop.json +2 -0
- package/dist/pipeline/flow-specs/implement.json +13 -6
- package/dist/pipeline/flow-specs/instant-task.json +177 -0
- package/dist/pipeline/flow-specs/normalize-task-source.json +311 -0
- package/dist/pipeline/flow-specs/plan-revise.json +7 -1
- package/dist/pipeline/flow-specs/plan.json +48 -70
- package/dist/pipeline/flow-specs/review/review-fix.json +24 -4
- package/dist/pipeline/flow-specs/review/review-loop.json +351 -45
- package/dist/pipeline/flow-specs/review/review-project-loop.json +590 -0
- package/dist/pipeline/flow-specs/review/review-project.json +12 -0
- package/dist/pipeline/flow-specs/review/review.json +37 -31
- package/dist/pipeline/flow-specs/task-describe.json +2 -0
- package/dist/pipeline/flow-specs/task-source/jira-fetch.json +70 -0
- package/dist/pipeline/flow-specs/task-source/manual-input.json +216 -0
- package/dist/pipeline/node-registry.js +41 -1
- package/dist/pipeline/node-runner.js +3 -2
- package/dist/pipeline/nodes/build-review-fix-prompt-node.js +5 -1
- package/dist/pipeline/nodes/clear-ready-to-merge-node.js +11 -0
- package/dist/pipeline/nodes/commit-message-form-node.js +8 -0
- package/dist/pipeline/nodes/design-review-verdict-node.js +36 -0
- package/dist/pipeline/nodes/ensure-summary-json-node.js +13 -2
- package/dist/pipeline/nodes/fetch-gitlab-diff-node.js +19 -2
- package/dist/pipeline/nodes/fetch-gitlab-review-node.js +19 -2
- package/dist/pipeline/nodes/flow-run-node.js +226 -7
- package/dist/pipeline/nodes/git-commit-form-node.js +8 -0
- package/dist/pipeline/nodes/gitlab-review-artifacts-node.js +19 -2
- package/dist/pipeline/nodes/jira-fetch-node.js +50 -4
- package/dist/pipeline/nodes/llm-prompt-node.js +32 -12
- package/dist/pipeline/nodes/planning-bundle-node.js +10 -0
- package/dist/pipeline/nodes/review-verdict-node.js +86 -0
- package/dist/pipeline/nodes/select-files-form-node.js +8 -0
- package/dist/pipeline/nodes/structured-summary-node.js +24 -0
- package/dist/pipeline/nodes/user-input-node.js +38 -3
- package/dist/pipeline/nodes/write-selection-file-node.js +20 -4
- package/dist/pipeline/prompt-registry.js +3 -1
- package/dist/pipeline/prompt-runtime.js +4 -1
- package/dist/pipeline/review-iteration.js +26 -0
- package/dist/pipeline/spec-compiler.js +2 -0
- package/dist/pipeline/spec-types.js +3 -0
- package/dist/pipeline/spec-validator.js +14 -0
- package/dist/pipeline/value-resolver.js +74 -1
- package/dist/prompts.js +36 -14
- package/dist/review-severity.js +45 -0
- package/dist/runtime/artifact-registry.js +402 -0
- package/dist/runtime/design-review-input-contract.js +17 -16
- package/dist/runtime/env-loader.js +3 -0
- package/dist/runtime/execution-routing-store.js +134 -0
- package/dist/runtime/execution-routing.js +227 -0
- package/dist/runtime/interactive-execution-routing.js +462 -0
- package/dist/runtime/plan-revise-input-contract.js +35 -32
- package/dist/runtime/planning-bundle.js +123 -0
- package/dist/runtime/ready-to-merge.js +22 -1
- package/dist/runtime/review-input-contract.js +100 -0
- package/dist/structured-artifact-schema-registry.js +9 -0
- package/dist/structured-artifact-schemas.json +140 -1
- package/dist/structured-artifacts.js +77 -6
- package/dist/user-input.js +70 -3
- package/package.json +6 -3
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { TaskRunnerError } from "../errors.js";
|
|
3
|
+
import { ALLOWED_MODELS_BY_EXECUTOR, DEFAULT_LAUNCH_PROFILE, defaultModelForExecutor, isAllowedModelForExecutor, resolveLaunchProfile, } from "../pipeline/launch-profile-config.js";
|
|
4
|
+
import { BUILT_IN_EXECUTION_PRESET_IDS, EXECUTION_ROUTING_GROUPS, } from "../pipeline/execution-routing-config.js";
|
|
5
|
+
export const BUILT_IN_EXECUTION_PRESETS = {
|
|
6
|
+
balanced: {
|
|
7
|
+
id: "balanced",
|
|
8
|
+
label: "Balanced",
|
|
9
|
+
description: "Use Codex for planning and review, OpenCode for implementation-style loops.",
|
|
10
|
+
defaultRoute: { executor: "opencode", model: "default" },
|
|
11
|
+
groupOverrides: {
|
|
12
|
+
planning: { executor: "codex", model: "gpt-5.4" },
|
|
13
|
+
"design-review": { executor: "codex", model: "gpt-5.4" },
|
|
14
|
+
review: { executor: "codex", model: "gpt-5.4" },
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
"quality-first": {
|
|
18
|
+
id: "quality-first",
|
|
19
|
+
label: "Quality-first",
|
|
20
|
+
description: "Run all routing groups on Codex GPT-5.4.",
|
|
21
|
+
defaultRoute: { executor: "codex", model: "gpt-5.4" },
|
|
22
|
+
},
|
|
23
|
+
"cheap-first": {
|
|
24
|
+
id: "cheap-first",
|
|
25
|
+
label: "Cheap-first",
|
|
26
|
+
description: "Prefer lower-cost models while preserving valid executor and model pairs.",
|
|
27
|
+
defaultRoute: { executor: "opencode", model: "opencode/minimax-m2.5-free" },
|
|
28
|
+
groupOverrides: {
|
|
29
|
+
planning: { executor: "codex", model: "gpt-5.4-mini" },
|
|
30
|
+
"design-review": { executor: "codex", model: "gpt-5.4-mini" },
|
|
31
|
+
review: { executor: "codex", model: "gpt-5.4-mini" },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
"codex-only": {
|
|
35
|
+
id: "codex-only",
|
|
36
|
+
label: "Codex-only",
|
|
37
|
+
description: "Run all routing groups on Codex with the default Codex model.",
|
|
38
|
+
defaultRoute: { executor: "codex", model: "default" },
|
|
39
|
+
},
|
|
40
|
+
"opencode-only": {
|
|
41
|
+
id: "opencode-only",
|
|
42
|
+
label: "OpenCode-only",
|
|
43
|
+
description: "Run all routing groups on OpenCode with the default OpenCode model.",
|
|
44
|
+
defaultRoute: { executor: "opencode", model: "default" },
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
function stableRoutingPayload(routing) {
|
|
48
|
+
return JSON.stringify({
|
|
49
|
+
defaultRoute: {
|
|
50
|
+
executor: routing.defaultRoute.executor,
|
|
51
|
+
model: routing.defaultRoute.model,
|
|
52
|
+
},
|
|
53
|
+
groups: Object.fromEntries(EXECUTION_ROUTING_GROUPS.map((group) => [
|
|
54
|
+
group,
|
|
55
|
+
{
|
|
56
|
+
executor: routing.groups[group].executor,
|
|
57
|
+
model: routing.groups[group].model,
|
|
58
|
+
},
|
|
59
|
+
])),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
export function executionRoutingFingerprint(routing) {
|
|
63
|
+
return createHash("sha256").update(stableRoutingPayload({ ...routing, fingerprint: "" })).digest("hex");
|
|
64
|
+
}
|
|
65
|
+
export function validateExecutionRoute(executor, model) {
|
|
66
|
+
const allowedModels = ALLOWED_MODELS_BY_EXECUTOR[executor];
|
|
67
|
+
if (!allowedModels) {
|
|
68
|
+
throw new TaskRunnerError(`Unsupported llm executor '${executor}'.`);
|
|
69
|
+
}
|
|
70
|
+
if (!allowedModels.includes(model)) {
|
|
71
|
+
throw new TaskRunnerError(`Model '${model}' is not allowed for executor '${executor}'.`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function toExecutionRouteSelection(route) {
|
|
75
|
+
return {
|
|
76
|
+
executor: route.executor,
|
|
77
|
+
model: route.model,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export function resolveExecutionRoute(selection, fallback) {
|
|
81
|
+
const launchProfile = resolveLaunchProfile({
|
|
82
|
+
executor: selection.executor,
|
|
83
|
+
model: selection.model,
|
|
84
|
+
}, fallback);
|
|
85
|
+
validateExecutionRoute(launchProfile.executor, launchProfile.model);
|
|
86
|
+
return launchProfile;
|
|
87
|
+
}
|
|
88
|
+
export function resolveExecutionRouting(options) {
|
|
89
|
+
const fallbackDefaultRoute = options.fallbackDefaultRoute ?? DEFAULT_LAUNCH_PROFILE;
|
|
90
|
+
const preset = options.presetId ? BUILT_IN_EXECUTION_PRESETS[options.presetId] : null;
|
|
91
|
+
const defaultRouteSelection = options.defaultRoute ?? preset?.defaultRoute ?? toExecutionRouteSelection(fallbackDefaultRoute);
|
|
92
|
+
const defaultRoute = resolveExecutionRoute(defaultRouteSelection, fallbackDefaultRoute);
|
|
93
|
+
const groups = Object.fromEntries(EXECUTION_ROUTING_GROUPS.map((group) => {
|
|
94
|
+
const override = options.currentRunOverrides?.[group]
|
|
95
|
+
?? options.presetOverrides?.[group]
|
|
96
|
+
?? preset?.groupOverrides?.[group]
|
|
97
|
+
?? { executor: "default", model: "default" };
|
|
98
|
+
return [group, resolveExecutionRoute(override, defaultRoute)];
|
|
99
|
+
}));
|
|
100
|
+
const fingerprint = executionRoutingFingerprint({
|
|
101
|
+
defaultRoute,
|
|
102
|
+
groups,
|
|
103
|
+
});
|
|
104
|
+
return {
|
|
105
|
+
defaultRoute,
|
|
106
|
+
groups,
|
|
107
|
+
fingerprint,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
export function routingGroupLabel(group) {
|
|
111
|
+
switch (group) {
|
|
112
|
+
case "planning":
|
|
113
|
+
return "Planning";
|
|
114
|
+
case "design-review":
|
|
115
|
+
return "Design review";
|
|
116
|
+
case "implementation":
|
|
117
|
+
return "Implementation";
|
|
118
|
+
case "review":
|
|
119
|
+
return "Review";
|
|
120
|
+
case "repair-loop":
|
|
121
|
+
return "Repair loop";
|
|
122
|
+
case "local-fix-loop":
|
|
123
|
+
return "Local fix loop";
|
|
124
|
+
default:
|
|
125
|
+
return group;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
export function describeExecutionRouting(routing, groups = EXECUTION_ROUTING_GROUPS) {
|
|
129
|
+
const lines = [`Default: ${routing.defaultRoute.executor} / ${routing.defaultRoute.model}`];
|
|
130
|
+
for (const group of groups) {
|
|
131
|
+
const route = routing.groups[group];
|
|
132
|
+
lines.push(`${routingGroupLabel(group)}: ${route.executor} / ${route.model}`);
|
|
133
|
+
}
|
|
134
|
+
return lines.join("\n");
|
|
135
|
+
}
|
|
136
|
+
export function executorsForRoutingGroups(routing, groups) {
|
|
137
|
+
const requiredExecutors = [];
|
|
138
|
+
for (const group of groups) {
|
|
139
|
+
const executor = routing.groups[group].executor;
|
|
140
|
+
if (!requiredExecutors.includes(executor)) {
|
|
141
|
+
requiredExecutors.push(executor);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return requiredExecutors;
|
|
145
|
+
}
|
|
146
|
+
export function normalizeEditableExecutionRouting(routes) {
|
|
147
|
+
const normalizedRoutes = {};
|
|
148
|
+
const validationErrors = [];
|
|
149
|
+
for (const group of EXECUTION_ROUTING_GROUPS) {
|
|
150
|
+
const route = routes[group];
|
|
151
|
+
if (isAllowedModelForExecutor(route.executor, route.model)) {
|
|
152
|
+
normalizedRoutes[group] = { ...route };
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
normalizedRoutes[group] = {
|
|
156
|
+
executor: route.executor,
|
|
157
|
+
model: defaultModelForExecutor(route.executor),
|
|
158
|
+
};
|
|
159
|
+
validationErrors.push(`${routingGroupLabel(group)} model '${route.model}' is not allowed for executor '${route.executor}'. Select a ${route.executor} model.`);
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
routes: normalizedRoutes,
|
|
163
|
+
validationErrors,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
export function builtInExecutionPresetList() {
|
|
167
|
+
return BUILT_IN_EXECUTION_PRESET_IDS.map((id) => BUILT_IN_EXECUTION_PRESETS[id]);
|
|
168
|
+
}
|
|
169
|
+
export function selectedExecutionPresetLabel(selection) {
|
|
170
|
+
return selection.label;
|
|
171
|
+
}
|
|
172
|
+
export function cloneResolvedExecutionRouting(routing) {
|
|
173
|
+
return {
|
|
174
|
+
defaultRoute: { ...routing.defaultRoute },
|
|
175
|
+
groups: Object.fromEntries(EXECUTION_ROUTING_GROUPS.map((group) => [group, { ...routing.groups[group] }])),
|
|
176
|
+
fingerprint: routing.fingerprint,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
export function defaultExecutionRouting() {
|
|
180
|
+
return resolveExecutionRouting({
|
|
181
|
+
defaultRoute: {
|
|
182
|
+
executor: DEFAULT_LAUNCH_PROFILE.executor,
|
|
183
|
+
model: DEFAULT_LAUNCH_PROFILE.model,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
export function resolveStoredExecutionRoutingSnapshot(routing) {
|
|
188
|
+
validateExecutionRoute(routing.defaultRoute.executor, routing.defaultRoute.model);
|
|
189
|
+
for (const group of EXECUTION_ROUTING_GROUPS) {
|
|
190
|
+
validateExecutionRoute(routing.groups[group].executor, routing.groups[group].model);
|
|
191
|
+
}
|
|
192
|
+
const fingerprint = executionRoutingFingerprint({
|
|
193
|
+
defaultRoute: routing.defaultRoute,
|
|
194
|
+
groups: routing.groups,
|
|
195
|
+
});
|
|
196
|
+
return {
|
|
197
|
+
defaultRoute: {
|
|
198
|
+
...routing.defaultRoute,
|
|
199
|
+
selectedExecutor: routing.defaultRoute.selectedExecutor ?? routing.defaultRoute.executor,
|
|
200
|
+
selectedModel: routing.defaultRoute.selectedModel ?? routing.defaultRoute.model,
|
|
201
|
+
fingerprint: routing.defaultRoute.fingerprint ?? `${routing.defaultRoute.executor}::${routing.defaultRoute.model}`,
|
|
202
|
+
},
|
|
203
|
+
groups: Object.fromEntries(EXECUTION_ROUTING_GROUPS.map((group) => {
|
|
204
|
+
const route = routing.groups[group];
|
|
205
|
+
return [group, {
|
|
206
|
+
...route,
|
|
207
|
+
selectedExecutor: route.selectedExecutor ?? route.executor,
|
|
208
|
+
selectedModel: route.selectedModel ?? route.model,
|
|
209
|
+
fingerprint: route.fingerprint ?? `${route.executor}::${route.model}`,
|
|
210
|
+
}];
|
|
211
|
+
})),
|
|
212
|
+
fingerprint,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
export function singleLaunchProfileExecutionRouting(launchProfile) {
|
|
216
|
+
return resolveExecutionRouting({
|
|
217
|
+
defaultRoute: toExecutionRouteSelection(launchProfile),
|
|
218
|
+
currentRunOverrides: Object.fromEntries(EXECUTION_ROUTING_GROUPS.map((group) => [group, toExecutionRouteSelection(launchProfile)])),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
export function modelOptionsForExecutor(executor) {
|
|
222
|
+
const defaultModel = defaultModelForExecutor(executor);
|
|
223
|
+
return ALLOWED_MODELS_BY_EXECUTOR[executor].map((model) => ({
|
|
224
|
+
value: model,
|
|
225
|
+
label: model === defaultModel ? `${model} [default]` : model,
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { flowRoutingGroups, flowRoutingKey } from "../pipeline/flow-catalog.js";
|
|
3
|
+
import { loadNamedDeclarativeFlow } from "../pipeline/declarative-flows.js";
|
|
4
|
+
import { EXECUTION_ROUTING_GROUPS, } from "../pipeline/execution-routing-config.js";
|
|
5
|
+
import { DEFAULT_LAUNCH_PROFILE, defaultModelForExecutor, isAllowedModelForExecutor, } from "../pipeline/launch-profile-config.js";
|
|
6
|
+
import { FlowInterruptedError, TaskRunnerError } from "../errors.js";
|
|
7
|
+
import { BUILT_IN_EXECUTION_PRESETS, builtInExecutionPresetList, modelOptionsForExecutor, resolveExecutionRouting, resolveStoredExecutionRoutingSnapshot, routingGroupLabel, } from "./execution-routing.js";
|
|
8
|
+
import { getFlowDefaultExecutionRouting, getLastUsedExecutionRouting, getNamedExecutionPresets, saveFlowDefaultExecutionRouting, saveLastUsedExecutionRouting, saveNamedExecutionPreset, } from "./execution-routing-store.js";
|
|
9
|
+
function executionPresetSelectionForm(flowEntry, flowDefaultAvailable, lastUsedAvailable, namedPresetNames) {
|
|
10
|
+
const options = [];
|
|
11
|
+
if (flowDefaultAvailable) {
|
|
12
|
+
options.push({
|
|
13
|
+
value: "flow-default",
|
|
14
|
+
label: "Flow default",
|
|
15
|
+
description: "Use the saved routing snapshot for this flow.",
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
if (lastUsedAvailable) {
|
|
19
|
+
options.push({
|
|
20
|
+
value: "last-used",
|
|
21
|
+
label: "Last used",
|
|
22
|
+
description: "Reuse the most recent routing started for this flow.",
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
for (const preset of builtInExecutionPresetList()) {
|
|
26
|
+
options.push({
|
|
27
|
+
value: `built-in:${preset.id}`,
|
|
28
|
+
label: preset.label,
|
|
29
|
+
description: preset.description,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
for (const name of namedPresetNames) {
|
|
33
|
+
options.push({
|
|
34
|
+
value: `named:${name}`,
|
|
35
|
+
label: `Named preset: ${name}`,
|
|
36
|
+
description: "Reuse a saved routing snapshot across flows.",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
options.push({
|
|
40
|
+
value: "custom",
|
|
41
|
+
label: "Custom",
|
|
42
|
+
description: "Start from the default route and edit the fallback route plus routing groups manually.",
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
formId: "flow-execution-preset",
|
|
46
|
+
title: "Execution Routing",
|
|
47
|
+
description: `Select an execution strategy for '${flowEntry.id}'.`,
|
|
48
|
+
submitLabel: "Continue",
|
|
49
|
+
fields: [
|
|
50
|
+
{
|
|
51
|
+
id: "preset",
|
|
52
|
+
type: "single-select",
|
|
53
|
+
label: "Preset",
|
|
54
|
+
required: true,
|
|
55
|
+
default: options[0]?.value ?? "custom",
|
|
56
|
+
options,
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function executorFieldOptions() {
|
|
62
|
+
return [
|
|
63
|
+
{ value: "codex", label: "codex" },
|
|
64
|
+
{ value: "opencode", label: "opencode" },
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
function selectedExecutorFromValues(values, fieldId, fallbackExecutor) {
|
|
68
|
+
const candidate = values[fieldId];
|
|
69
|
+
return candidate === "codex" || candidate === "opencode" ? candidate : fallbackExecutor;
|
|
70
|
+
}
|
|
71
|
+
function routingEditorDraftFromRouting(routing) {
|
|
72
|
+
return {
|
|
73
|
+
defaultRoute: {
|
|
74
|
+
executor: routing.defaultRoute.executor,
|
|
75
|
+
model: routing.defaultRoute.model,
|
|
76
|
+
},
|
|
77
|
+
groups: Object.fromEntries(EXECUTION_ROUTING_GROUPS.map((group) => [
|
|
78
|
+
group,
|
|
79
|
+
{
|
|
80
|
+
executor: routing.groups[group].executor,
|
|
81
|
+
model: routing.groups[group].model,
|
|
82
|
+
},
|
|
83
|
+
])),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function advancedRoutingEditorForm(activeGroups, draft, validationMessage) {
|
|
87
|
+
const fields = [
|
|
88
|
+
{
|
|
89
|
+
id: "default_route_executor",
|
|
90
|
+
type: "single-select",
|
|
91
|
+
label: "Default route executor",
|
|
92
|
+
required: true,
|
|
93
|
+
default: draft.defaultRoute.executor,
|
|
94
|
+
options: executorFieldOptions(),
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "default_route_model",
|
|
98
|
+
type: "single-select",
|
|
99
|
+
label: "Default route model",
|
|
100
|
+
help: "Available models follow the selected default executor.",
|
|
101
|
+
required: true,
|
|
102
|
+
default: draft.defaultRoute.model,
|
|
103
|
+
options: modelOptionsForExecutor(draft.defaultRoute.executor),
|
|
104
|
+
optionsFromValues: (values) => modelOptionsForExecutor(selectedExecutorFromValues(values, "default_route_executor", draft.defaultRoute.executor)),
|
|
105
|
+
},
|
|
106
|
+
...activeGroups.flatMap((group) => {
|
|
107
|
+
const route = draft.groups[group];
|
|
108
|
+
return [
|
|
109
|
+
{
|
|
110
|
+
id: `${group}_executor`,
|
|
111
|
+
type: "single-select",
|
|
112
|
+
label: `${routingGroupLabel(group)} executor`,
|
|
113
|
+
required: true,
|
|
114
|
+
default: route.executor,
|
|
115
|
+
options: executorFieldOptions(),
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: `${group}_model`,
|
|
119
|
+
type: "single-select",
|
|
120
|
+
label: `${routingGroupLabel(group)} model`,
|
|
121
|
+
help: `Available models follow the selected ${routingGroupLabel(group)} executor.`,
|
|
122
|
+
required: true,
|
|
123
|
+
default: route.model,
|
|
124
|
+
options: modelOptionsForExecutor(route.executor),
|
|
125
|
+
optionsFromValues: (values) => modelOptionsForExecutor(selectedExecutorFromValues(values, `${group}_executor`, route.executor)),
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
}),
|
|
129
|
+
];
|
|
130
|
+
return {
|
|
131
|
+
formId: "flow-routing-editor",
|
|
132
|
+
title: "Advanced Routing",
|
|
133
|
+
description: validationMessage
|
|
134
|
+
? `${activeGroups.length > 0
|
|
135
|
+
? "Edit the fallback route plus executor and model by routing group."
|
|
136
|
+
: "Edit the fallback executor and model used by this flow."}\n\n${validationMessage}`
|
|
137
|
+
: activeGroups.length > 0
|
|
138
|
+
? "Edit the fallback route plus executor and model by routing group."
|
|
139
|
+
: "Edit the fallback executor and model used by this flow.",
|
|
140
|
+
submitLabel: "Apply",
|
|
141
|
+
fields,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function routingActionForm(previewText) {
|
|
145
|
+
return {
|
|
146
|
+
formId: "flow-routing-action",
|
|
147
|
+
title: "Routing Preview",
|
|
148
|
+
description: "Review the effective routing and choose the next action.",
|
|
149
|
+
preview: previewText,
|
|
150
|
+
submitLabel: "Continue",
|
|
151
|
+
fields: [
|
|
152
|
+
{
|
|
153
|
+
id: "action",
|
|
154
|
+
type: "single-select",
|
|
155
|
+
label: "Action",
|
|
156
|
+
required: true,
|
|
157
|
+
default: "start",
|
|
158
|
+
options: [
|
|
159
|
+
{ value: "start", label: "Start", description: "Run the flow with the previewed routing." },
|
|
160
|
+
{ value: "edit", label: "Edit routing", description: "Open the advanced routing editor." },
|
|
161
|
+
{ value: "cancel", label: "Cancel", description: "Abort launching this flow." },
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function routingPersistenceForm() {
|
|
168
|
+
return {
|
|
169
|
+
formId: "flow-routing-persistence",
|
|
170
|
+
title: "Save Routing",
|
|
171
|
+
description: "Choose how to persist the edited routing.",
|
|
172
|
+
submitLabel: "Continue",
|
|
173
|
+
fields: [
|
|
174
|
+
{
|
|
175
|
+
id: "persistence",
|
|
176
|
+
type: "single-select",
|
|
177
|
+
label: "Save mode",
|
|
178
|
+
required: true,
|
|
179
|
+
default: "current-run",
|
|
180
|
+
options: [
|
|
181
|
+
{ value: "current-run", label: "Current run only", description: "Use the edited routing only for this start." },
|
|
182
|
+
{ value: "flow-default", label: "Flow default", description: "Reuse this routing automatically for this flow." },
|
|
183
|
+
{ value: "named-preset", label: "Named preset", description: "Save this routing as a reusable preset." },
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function presetNameForm() {
|
|
190
|
+
return {
|
|
191
|
+
formId: "flow-routing-preset-name",
|
|
192
|
+
title: "Preset Name",
|
|
193
|
+
description: "Enter a reusable name for the routing preset.",
|
|
194
|
+
submitLabel: "Save",
|
|
195
|
+
fields: [
|
|
196
|
+
{
|
|
197
|
+
id: "name",
|
|
198
|
+
type: "text",
|
|
199
|
+
label: "Preset name",
|
|
200
|
+
required: true,
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function normalizeEditableRoute(label, route) {
|
|
206
|
+
if (isAllowedModelForExecutor(route.executor, route.model)) {
|
|
207
|
+
return {
|
|
208
|
+
route: { ...route },
|
|
209
|
+
validationErrors: [],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
route: {
|
|
214
|
+
executor: route.executor,
|
|
215
|
+
model: defaultModelForExecutor(route.executor),
|
|
216
|
+
},
|
|
217
|
+
validationErrors: [
|
|
218
|
+
`${label} model '${route.model}' is not allowed for executor '${route.executor}'. Select a ${route.executor} model.`,
|
|
219
|
+
],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function normalizeEditableRoutingDraft(draft) {
|
|
223
|
+
const defaultRoute = normalizeEditableRoute("Default route", draft.defaultRoute);
|
|
224
|
+
const groups = {};
|
|
225
|
+
const currentRunOverrides = {};
|
|
226
|
+
const validationErrors = [...defaultRoute.validationErrors];
|
|
227
|
+
for (const group of EXECUTION_ROUTING_GROUPS) {
|
|
228
|
+
const normalizedRoute = normalizeEditableRoute(routingGroupLabel(group), draft.groups[group]);
|
|
229
|
+
groups[group] = normalizedRoute.route;
|
|
230
|
+
if (normalizedRoute.route.executor !== defaultRoute.route.executor
|
|
231
|
+
|| normalizedRoute.route.model !== defaultRoute.route.model) {
|
|
232
|
+
currentRunOverrides[group] = normalizedRoute.route;
|
|
233
|
+
}
|
|
234
|
+
validationErrors.push(...normalizedRoute.validationErrors);
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
draft: {
|
|
238
|
+
defaultRoute: defaultRoute.route,
|
|
239
|
+
groups,
|
|
240
|
+
},
|
|
241
|
+
currentRunOverrides,
|
|
242
|
+
validationErrors,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function normalizedEffectiveStepSignature(row) {
|
|
246
|
+
const normalizedStep = row.step.replace(/review_iteration_\d+/g, "review_iteration_*");
|
|
247
|
+
return [normalizedStep, row.group, row.executor, row.model].join("\u0000");
|
|
248
|
+
}
|
|
249
|
+
function collapseRepeatedEffectiveRoutedStepRows(rows) {
|
|
250
|
+
const seen = new Set();
|
|
251
|
+
const collapsed = [];
|
|
252
|
+
for (const row of rows) {
|
|
253
|
+
const signature = normalizedEffectiveStepSignature(row);
|
|
254
|
+
if (seen.has(signature)) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
seen.add(signature);
|
|
258
|
+
collapsed.push(row);
|
|
259
|
+
}
|
|
260
|
+
return collapsed;
|
|
261
|
+
}
|
|
262
|
+
function truncateTableCell(value, maxWidth) {
|
|
263
|
+
if (value.length <= maxWidth) {
|
|
264
|
+
return value;
|
|
265
|
+
}
|
|
266
|
+
if (maxWidth <= 1) {
|
|
267
|
+
return value.slice(0, maxWidth);
|
|
268
|
+
}
|
|
269
|
+
return `${value.slice(0, maxWidth - 1)}…`;
|
|
270
|
+
}
|
|
271
|
+
function formatAsciiTable(headers, rows, maxWidths) {
|
|
272
|
+
const widths = headers.map((header, index) => Math.min(maxWidths[index] ?? Number.MAX_SAFE_INTEGER, Math.max(header.length, ...rows.map((row) => (row[index] ?? "").length))));
|
|
273
|
+
const formatRow = (row) => `| ${row.map((cell, index) => truncateTableCell(cell ?? "", widths[index] ?? 1).padEnd(widths[index] ?? 1)).join(" | ")} |`;
|
|
274
|
+
const separator = `|-${widths.map((width) => "-".repeat(width)).join("-|-")}-|`;
|
|
275
|
+
return [formatRow(headers), separator, ...rows.map((row) => formatRow(row))];
|
|
276
|
+
}
|
|
277
|
+
function collectEffectiveRoutedStepRows(flow, cwd, routing, prefixSegments = [], ancestry = []) {
|
|
278
|
+
if (ancestry.includes(flow.absolutePath)) {
|
|
279
|
+
return [];
|
|
280
|
+
}
|
|
281
|
+
const nextAncestry = [...ancestry, flow.absolutePath];
|
|
282
|
+
const rows = [];
|
|
283
|
+
for (const phase of flow.phases) {
|
|
284
|
+
for (const step of phase.steps) {
|
|
285
|
+
const stepRef = `${phase.id}.${step.id}`;
|
|
286
|
+
if (step.routingGroup) {
|
|
287
|
+
const route = routing.groups[step.routingGroup];
|
|
288
|
+
rows.push({
|
|
289
|
+
step: [...prefixSegments, stepRef].join(" > "),
|
|
290
|
+
group: routingGroupLabel(step.routingGroup),
|
|
291
|
+
executor: route.executor,
|
|
292
|
+
model: route.model,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
if (step.node !== "flow-run") {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const nestedFlowName = step.params?.fileName;
|
|
299
|
+
if (!nestedFlowName || !("const" in nestedFlowName) || typeof nestedFlowName.const !== "string") {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const nestedFlow = loadNamedDeclarativeFlow(nestedFlowName.const, cwd);
|
|
303
|
+
const nestedFlowLabel = path.basename(nestedFlow.fileName, path.extname(nestedFlow.fileName));
|
|
304
|
+
rows.push(...collectEffectiveRoutedStepRows(nestedFlow, cwd, routing, [...prefixSegments, stepRef, nestedFlowLabel], nextAncestry));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return rows;
|
|
308
|
+
}
|
|
309
|
+
export function describeEffectiveRoutingPreview(flowEntry, routing, cwd) {
|
|
310
|
+
const previewGroups = flowRoutingGroups(flowEntry, cwd);
|
|
311
|
+
const summaryRows = [
|
|
312
|
+
["Default", routing.defaultRoute.executor, routing.defaultRoute.model],
|
|
313
|
+
...previewGroups.map((group) => [
|
|
314
|
+
routingGroupLabel(group),
|
|
315
|
+
routing.groups[group].executor,
|
|
316
|
+
routing.groups[group].model,
|
|
317
|
+
]),
|
|
318
|
+
];
|
|
319
|
+
const lines = [
|
|
320
|
+
"Effective routes:",
|
|
321
|
+
...formatAsciiTable(["Scope", "Executor", "Model"], summaryRows, [18, 12, 36]),
|
|
322
|
+
];
|
|
323
|
+
const routedSteps = collapseRepeatedEffectiveRoutedStepRows(collectEffectiveRoutedStepRows(flowEntry.flow, cwd, routing));
|
|
324
|
+
if (routedSteps.length === 0) {
|
|
325
|
+
return lines.join("\n");
|
|
326
|
+
}
|
|
327
|
+
return [
|
|
328
|
+
...lines,
|
|
329
|
+
"",
|
|
330
|
+
"Routed LLM steps:",
|
|
331
|
+
...formatAsciiTable(["Step", "Group", "Executor", "Model"], routedSteps.map((row) => [row.step, row.group, row.executor, row.model]), [64, 16, 12, 30]),
|
|
332
|
+
].join("\n");
|
|
333
|
+
}
|
|
334
|
+
export async function requestInteractiveExecutionRouting(flowEntry, requestUserInput) {
|
|
335
|
+
const previewGroups = flowRoutingGroups(flowEntry, process.cwd());
|
|
336
|
+
const flowKey = flowRoutingKey(flowEntry);
|
|
337
|
+
const namedPresets = getNamedExecutionPresets();
|
|
338
|
+
const flowDefault = getFlowDefaultExecutionRouting(flowKey);
|
|
339
|
+
const lastUsed = getLastUsedExecutionRouting(flowKey);
|
|
340
|
+
const presetSelection = await requestUserInput(executionPresetSelectionForm(flowEntry, Boolean(flowDefault), Boolean(lastUsed), Object.keys(namedPresets).sort((left, right) => left.localeCompare(right, "en"))));
|
|
341
|
+
const selectedPresetValue = String(presetSelection.values.preset ?? "custom");
|
|
342
|
+
let selectedPreset;
|
|
343
|
+
let routing;
|
|
344
|
+
if (selectedPresetValue === "flow-default") {
|
|
345
|
+
if (!flowDefault) {
|
|
346
|
+
throw new TaskRunnerError("Flow default routing is unavailable.");
|
|
347
|
+
}
|
|
348
|
+
selectedPreset = { kind: "flow-default", label: "Flow default" };
|
|
349
|
+
routing = resolveStoredExecutionRoutingSnapshot(flowDefault.routing);
|
|
350
|
+
}
|
|
351
|
+
else if (selectedPresetValue === "last-used") {
|
|
352
|
+
if (!lastUsed) {
|
|
353
|
+
throw new TaskRunnerError("Last-used routing is unavailable.");
|
|
354
|
+
}
|
|
355
|
+
selectedPreset = { kind: "last-used", label: "Last used" };
|
|
356
|
+
routing = resolveStoredExecutionRoutingSnapshot(lastUsed.routing);
|
|
357
|
+
}
|
|
358
|
+
else if (selectedPresetValue.startsWith("named:")) {
|
|
359
|
+
const presetName = selectedPresetValue.slice("named:".length);
|
|
360
|
+
const namedPreset = namedPresets[presetName];
|
|
361
|
+
if (!namedPreset) {
|
|
362
|
+
throw new TaskRunnerError(`Named preset '${presetName}' is unavailable.`);
|
|
363
|
+
}
|
|
364
|
+
selectedPreset = { kind: "named", presetId: presetName, label: `Named preset: ${presetName}` };
|
|
365
|
+
routing = resolveStoredExecutionRoutingSnapshot(namedPreset.routing);
|
|
366
|
+
}
|
|
367
|
+
else if (selectedPresetValue.startsWith("built-in:")) {
|
|
368
|
+
const presetId = selectedPresetValue.slice("built-in:".length);
|
|
369
|
+
const preset = BUILT_IN_EXECUTION_PRESETS[presetId];
|
|
370
|
+
if (!preset) {
|
|
371
|
+
throw new TaskRunnerError(`Unknown execution preset '${presetId}'.`);
|
|
372
|
+
}
|
|
373
|
+
selectedPreset = { kind: "built-in", presetId, label: preset.label };
|
|
374
|
+
routing = resolveExecutionRouting({ presetId });
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
selectedPreset = { kind: "custom", label: "Custom" };
|
|
378
|
+
routing = resolveExecutionRouting({
|
|
379
|
+
defaultRoute: {
|
|
380
|
+
executor: DEFAULT_LAUNCH_PROFILE.executor,
|
|
381
|
+
model: DEFAULT_LAUNCH_PROFILE.model,
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
let editorDraft = routingEditorDraftFromRouting(routing);
|
|
386
|
+
let editorValidationMessage;
|
|
387
|
+
for (;;) {
|
|
388
|
+
const previewText = `Preset: ${selectedPreset.label}\n${describeEffectiveRoutingPreview(flowEntry, routing, process.cwd())}`;
|
|
389
|
+
const actionResult = await requestUserInput(routingActionForm(previewText));
|
|
390
|
+
const action = String(actionResult.values.action ?? "start");
|
|
391
|
+
if (action === "cancel") {
|
|
392
|
+
throw new FlowInterruptedError("Flow launch cancelled.");
|
|
393
|
+
}
|
|
394
|
+
if (action === "start") {
|
|
395
|
+
saveLastUsedExecutionRouting(flowKey, routing, selectedPreset);
|
|
396
|
+
return { routing, selectedPreset };
|
|
397
|
+
}
|
|
398
|
+
const routingFormResult = await requestUserInput(advancedRoutingEditorForm(previewGroups, editorDraft, editorValidationMessage));
|
|
399
|
+
const requestedDefaultRoute = {
|
|
400
|
+
executor: String(routingFormResult.values.default_route_executor ?? editorDraft.defaultRoute.executor),
|
|
401
|
+
model: String(routingFormResult.values.default_route_model ?? editorDraft.defaultRoute.model),
|
|
402
|
+
};
|
|
403
|
+
const requestedDraft = {
|
|
404
|
+
defaultRoute: requestedDefaultRoute,
|
|
405
|
+
groups: Object.fromEntries(EXECUTION_ROUTING_GROUPS.map((group) => {
|
|
406
|
+
const submittedExecutor = String(routingFormResult.values[`${group}_executor`] ?? editorDraft.groups[group].executor);
|
|
407
|
+
const submittedModel = String(routingFormResult.values[`${group}_model`] ?? editorDraft.groups[group].model);
|
|
408
|
+
const inheritedBeforeEdit = editorDraft.groups[group].executor === editorDraft.defaultRoute.executor
|
|
409
|
+
&& editorDraft.groups[group].model === editorDraft.defaultRoute.model;
|
|
410
|
+
const groupRoute = inheritedBeforeEdit
|
|
411
|
+
&& submittedExecutor === editorDraft.groups[group].executor
|
|
412
|
+
&& submittedModel === editorDraft.groups[group].model
|
|
413
|
+
? requestedDefaultRoute
|
|
414
|
+
: { executor: submittedExecutor, model: submittedModel };
|
|
415
|
+
return [
|
|
416
|
+
group,
|
|
417
|
+
groupRoute,
|
|
418
|
+
];
|
|
419
|
+
})),
|
|
420
|
+
};
|
|
421
|
+
const normalizedDraft = normalizeEditableRoutingDraft(requestedDraft);
|
|
422
|
+
editorDraft = normalizedDraft.draft;
|
|
423
|
+
if (normalizedDraft.validationErrors.length > 0) {
|
|
424
|
+
editorValidationMessage = normalizedDraft.validationErrors.join("\n");
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
try {
|
|
428
|
+
routing = resolveExecutionRouting({
|
|
429
|
+
defaultRoute: {
|
|
430
|
+
executor: normalizedDraft.draft.defaultRoute.executor,
|
|
431
|
+
model: normalizedDraft.draft.defaultRoute.model,
|
|
432
|
+
},
|
|
433
|
+
currentRunOverrides: normalizedDraft.currentRunOverrides,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
if (error instanceof TaskRunnerError) {
|
|
438
|
+
editorValidationMessage = error.message;
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
throw error;
|
|
442
|
+
}
|
|
443
|
+
editorDraft = routingEditorDraftFromRouting(routing);
|
|
444
|
+
editorValidationMessage = undefined;
|
|
445
|
+
selectedPreset = { kind: "custom", label: "Custom" };
|
|
446
|
+
const persistenceResult = await requestUserInput(routingPersistenceForm());
|
|
447
|
+
const persistence = String(persistenceResult.values.persistence ?? "current-run");
|
|
448
|
+
if (persistence === "flow-default") {
|
|
449
|
+
selectedPreset = { kind: "flow-default", label: "Flow default" };
|
|
450
|
+
saveFlowDefaultExecutionRouting(flowKey, routing, selectedPreset);
|
|
451
|
+
}
|
|
452
|
+
else if (persistence === "named-preset") {
|
|
453
|
+
const presetNameResult = await requestUserInput(presetNameForm());
|
|
454
|
+
const presetName = String(presetNameResult.values.name ?? "").trim();
|
|
455
|
+
if (!presetName) {
|
|
456
|
+
throw new TaskRunnerError("Preset name is required.");
|
|
457
|
+
}
|
|
458
|
+
selectedPreset = { kind: "named", presetId: presetName, label: `Named preset: ${presetName}` };
|
|
459
|
+
saveNamedExecutionPreset(presetName, routing, selectedPreset);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|