akm-cli 0.9.0-rc.1 → 0.9.0-rc.2
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/CHANGELOG.md +223 -9
- package/SECURITY.md +23 -24
- package/dist/assets/help/help-improve.md +10 -10
- package/dist/assets/hints/cli-hints-full.md +44 -10
- package/dist/assets/hints/cli-hints-short.md +6 -2
- package/dist/assets/{profiles → improve-strategies}/default.json +1 -0
- package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +1 -1
- package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +2 -3
- package/dist/assets/{profiles → improve-strategies}/reflect-distill.json +3 -4
- package/dist/assets/stash-skeleton/README.md +28 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +12 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +11 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +9 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +7 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +7 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +98 -0
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +63 -0
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +127 -0
- package/dist/assets/tasks/core/backup.yml +1 -0
- package/dist/assets/tasks/core/extract.yml +1 -0
- package/dist/assets/tasks/core/improve.yml +1 -0
- package/dist/assets/tasks/core/index-refresh.yml +1 -0
- package/dist/assets/tasks/core/sync.yml +1 -0
- package/dist/assets/tasks/core/version-check.yml +1 -0
- package/dist/assets/tasks/graph-refresh-weekly.yml +4 -4
- package/dist/assets/templates/html/health.html +5 -1
- package/dist/cli/config-migrate.js +31 -138
- package/dist/cli/config-validate.js +10 -8
- package/dist/cli.js +48 -14
- package/dist/commands/agent/agent-dispatch.js +17 -16
- package/dist/commands/agent/agent-support.js +0 -24
- package/dist/commands/agent/contribute-cli.js +5 -15
- package/dist/commands/backup-cli.js +54 -0
- package/dist/commands/config-cli.js +45 -159
- package/dist/commands/env/secret.js +8 -5
- package/dist/commands/health/checks.js +130 -83
- package/dist/commands/health/html-report.js +4 -0
- package/dist/commands/health/improve-metrics.js +30 -32
- package/dist/commands/health/llm-usage.js +19 -19
- package/dist/commands/health/md-report.js +4 -0
- package/dist/commands/health/metrics.js +2 -1
- package/dist/commands/health/surfaces.js +5 -4
- package/dist/commands/health.js +1 -1
- package/dist/commands/improve/consolidate/chunking.js +2 -2
- package/dist/commands/improve/consolidate.js +28 -25
- package/dist/commands/improve/distill/promote-memory.js +5 -12
- package/dist/commands/improve/distill/quality-gate.js +5 -7
- package/dist/commands/improve/distill.js +16 -5
- package/dist/commands/improve/eligibility.js +22 -12
- package/dist/commands/improve/extract-cli.js +47 -19
- package/dist/commands/improve/extract.js +110 -62
- package/dist/commands/improve/improve-cli.js +38 -16
- package/dist/commands/improve/improve-result-file.js +30 -24
- package/dist/commands/improve/improve-strategies.js +137 -0
- package/dist/commands/improve/improve.js +60 -30
- package/dist/commands/improve/locks.js +66 -45
- package/dist/commands/improve/loop-stages.js +75 -33
- package/dist/commands/improve/memory/memory-belief.js +79 -7
- package/dist/commands/improve/memory/memory-contradiction-detect.js +12 -4
- package/dist/commands/improve/preparation.js +71 -73
- package/dist/commands/improve/procedural.js +3 -2
- package/dist/commands/improve/recombine.js +2 -1
- package/dist/commands/improve/reflect.js +119 -214
- package/dist/commands/improve/shared.js +11 -5
- package/dist/commands/lint/base-linter.js +152 -42
- package/dist/commands/mv-cli.js +809 -0
- package/dist/commands/proposal/proposal-cli.js +18 -8
- package/dist/commands/proposal/propose.js +64 -69
- package/dist/commands/read/knowledge.js +436 -4
- package/dist/commands/read/remember-cli.js +39 -2
- package/dist/commands/read/search-cli.js +6 -1
- package/dist/commands/registry-cli.js +29 -14
- package/dist/commands/remember.js +2 -0
- package/dist/commands/sources/init.js +13 -14
- package/dist/commands/sources/schema-repair.js +2 -4
- package/dist/commands/sources/source-add.js +62 -73
- package/dist/commands/sources/source-manage.js +50 -46
- package/dist/commands/sources/stash-cli.js +41 -4
- package/dist/commands/tasks/default-tasks.js +12 -12
- package/dist/commands/tasks/tasks-cli.js +7 -3
- package/dist/commands/tasks/tasks.js +113 -18
- package/dist/commands/wiki-cli.js +9 -10
- package/dist/core/asset/frontmatter.js +12 -2
- package/dist/core/common.js +5 -3
- package/dist/core/config/config-io.js +28 -17
- package/dist/core/config/config-schema.js +358 -66
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config-version.js +29 -0
- package/dist/core/config/config-walker.js +98 -27
- package/dist/core/config/config.js +132 -266
- package/dist/core/config/deep-merge.js +41 -0
- package/dist/core/config/engine-semantics.js +32 -0
- package/dist/core/errors.js +2 -2
- package/dist/core/extra-params.js +61 -0
- package/dist/core/file-lock.js +201 -56
- package/dist/core/improve-result.js +178 -0
- package/dist/core/maintenance-barrier.js +119 -0
- package/dist/core/migration-backup.js +416 -0
- package/dist/core/paths.js +3 -0
- package/dist/core/redaction.js +358 -0
- package/dist/core/state/migrations.js +17 -2
- package/dist/core/state-db.js +44 -1
- package/dist/indexer/db/db.js +116 -1
- package/dist/indexer/graph/graph-extraction.js +28 -16
- package/dist/indexer/index-writer-lock.js +31 -24
- package/dist/indexer/index-written-assets.js +15 -6
- package/dist/indexer/indexer.js +47 -2
- package/dist/indexer/passes/memory-inference.js +10 -6
- package/dist/indexer/passes/metadata.js +250 -0
- package/dist/indexer/search/db-search.js +111 -44
- package/dist/indexer/search/fts-query.js +41 -0
- package/dist/indexer/search/ranking-contributors.js +48 -0
- package/dist/indexer/search/ranking.js +36 -23
- package/dist/indexer/search/search-fields.js +11 -1
- package/dist/integrations/agent/builder-shared.js +7 -0
- package/dist/integrations/agent/builders.js +5 -52
- package/dist/integrations/agent/config.js +3 -143
- package/dist/integrations/agent/detect.js +17 -2
- package/dist/integrations/agent/engine-resolution.js +202 -0
- package/dist/integrations/agent/index.js +1 -2
- package/dist/integrations/agent/model-aliases.js +7 -2
- package/dist/integrations/agent/profiles.js +6 -99
- package/dist/integrations/agent/runner-dispatch.js +76 -13
- package/dist/integrations/agent/runner.js +76 -207
- package/dist/integrations/agent/spawn.js +4 -6
- package/dist/integrations/harnesses/aider/agent-builder.js +2 -3
- package/dist/integrations/harnesses/aider/index.js +0 -1
- package/dist/integrations/harnesses/amazonq/agent-builder.js +2 -3
- package/dist/integrations/harnesses/amazonq/index.js +0 -1
- package/dist/integrations/harnesses/claude/agent-builder.js +2 -3
- package/dist/integrations/harnesses/claude/index.js +0 -2
- package/dist/integrations/harnesses/codex/agent-builder.js +2 -3
- package/dist/integrations/harnesses/codex/index.js +0 -1
- package/dist/integrations/harnesses/copilot/agent-builder.js +2 -3
- package/dist/integrations/harnesses/copilot/index.js +0 -1
- package/dist/integrations/harnesses/gemini/agent-builder.js +2 -3
- package/dist/integrations/harnesses/gemini/index.js +0 -1
- package/dist/integrations/harnesses/index.js +1 -24
- package/dist/integrations/harnesses/opencode/agent-builder.js +2 -3
- package/dist/integrations/harnesses/opencode/index.js +0 -6
- package/dist/integrations/harnesses/opencode-sdk/harness.js +1 -6
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +274 -191
- package/dist/integrations/harnesses/openhands/agent-builder.js +2 -3
- package/dist/integrations/harnesses/openhands/index.js +0 -1
- package/dist/integrations/harnesses/pi/agent-builder.js +2 -3
- package/dist/integrations/harnesses/pi/index.js +0 -1
- package/dist/integrations/harnesses/types.js +1 -32
- package/dist/integrations/lockfile.js +32 -21
- package/dist/llm/client.js +48 -14
- package/dist/llm/feature-gate.js +15 -47
- package/dist/llm/graph-extract.js +1 -1
- package/dist/llm/index-passes.js +8 -42
- package/dist/llm/memory-infer-impl.js +1 -1
- package/dist/llm/usage-persist.js +4 -0
- package/dist/llm/usage-telemetry.js +35 -5
- package/dist/output/shapes/helpers.js +2 -1
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/helpers.js +3 -1
- package/dist/schemas/akm-config.json +11013 -8600
- package/dist/schemas/akm-task.json +87 -0
- package/dist/schemas/akm-workflow.json +57 -13
- package/dist/scripts/migrate-storage.js +8591 -509
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +8623 -484
- package/dist/setup/detected-engines.js +142 -0
- package/dist/setup/engine-config.js +89 -0
- package/dist/setup/setup.js +236 -132
- package/dist/setup/steps/connection.js +61 -32
- package/dist/setup/steps/platforms.js +4 -4
- package/dist/setup/steps.js +3 -2
- package/dist/storage/database.js +13 -1
- package/dist/storage/engines/sqlite-migrations.js +1 -0
- package/dist/storage/repositories/improve-runs-repository.js +5 -5
- package/dist/storage/repositories/task-history-repository.js +78 -0
- package/dist/storage/repositories/workflow-runs-repository.js +9 -8
- package/dist/tasks/parser.js +138 -52
- package/dist/tasks/runner.js +71 -75
- package/dist/tasks/schema.js +1 -1
- package/dist/tasks/validator.js +11 -6
- package/dist/wiki/wiki.js +9 -8
- package/dist/workflows/authoring/workflow-program-template.yaml +3 -3
- package/dist/workflows/concurrency-policy.js +15 -0
- package/dist/workflows/db.js +65 -13
- package/dist/workflows/exec/brief.js +18 -24
- package/dist/workflows/exec/frozen-judge.js +47 -0
- package/dist/workflows/exec/native-executor.js +124 -65
- package/dist/workflows/exec/report.js +93 -33
- package/dist/workflows/exec/run-workflow.js +38 -25
- package/dist/workflows/exec/scheduler.js +12 -41
- package/dist/workflows/exec/step-work.js +91 -35
- package/dist/workflows/ir/compile.js +13 -26
- package/dist/workflows/ir/freeze.js +243 -0
- package/dist/workflows/ir/plan-hash.js +40 -5
- package/dist/workflows/ir/schema.js +542 -1
- package/dist/workflows/parser.js +7 -0
- package/dist/workflows/program/parser.js +132 -23
- package/dist/workflows/program/project.js +3 -4
- package/dist/workflows/program/schema.js +2 -2
- package/dist/workflows/resource-limits.js +20 -0
- package/dist/workflows/runtime/plan-classifier.js +187 -0
- package/dist/workflows/runtime/runs.js +54 -47
- package/dist/workflows/runtime/workflow-asset-loader.js +0 -22
- package/dist/workflows/validator.js +25 -0
- package/docs/data-and-telemetry.md +2 -2
- package/docs/migration/release-notes/0.6.0.md +1 -1
- package/docs/migration/release-notes/0.7.0.md +5 -4
- package/docs/migration/v0.8-to-v0.9.md +401 -0
- package/package.json +4 -2
- package/schemas/akm-config.json +16638 -0
- package/schemas/akm-task.json +87 -0
- package/schemas/akm-workflow.json +372 -0
- package/dist/commands/improve/improve-profiles.js +0 -168
- package/dist/core/config/config-migration.js +0 -602
- package/dist/core/deep-merge.js +0 -38
- package/dist/llm/call-ai.js +0 -62
- package/dist/setup/legacy-config.js +0 -106
- package/docs/README.md +0 -104
- /package/dist/assets/{profiles → improve-strategies}/catchup.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/consolidate.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/frequent.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/memory-focus.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/quick.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/recombine-only.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/synthesize.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/thorough.json +0 -0
|
@@ -24,15 +24,17 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { createRequire } from "node:module";
|
|
26
26
|
import { isMap, isScalar, LineCounter, parseDocument } from "yaml";
|
|
27
|
-
import {
|
|
27
|
+
import { formatExtraParamsIssue, validateExtraParams } from "../../core/extra-params.js";
|
|
28
|
+
import { jsonBytes, utf8Bytes, WORKFLOW_MAX_EXTRA_PARAMS_BYTES, WORKFLOW_MAX_INSTRUCTION_BYTES, WORKFLOW_MAX_MAP_EXPANSION, WORKFLOW_MAX_PARAMS, WORKFLOW_MAX_ROUTE_BRANCHES, WORKFLOW_MAX_SCHEMA_BYTES, WORKFLOW_MAX_SOURCE_BYTES, WORKFLOW_MAX_STEPS, } from "../resource-limits.js";
|
|
29
|
+
import { PROGRAM_ISOLATION_KINDS, PROGRAM_ON_ERROR, PROGRAM_PARAM_NAME_PATTERN, PROGRAM_REDUCERS, PROGRAM_RETRY_REASONS, PROGRAM_STEP_ID_PATTERN, WORKFLOW_PROGRAM_VERSION, } from "./schema.js";
|
|
28
30
|
const TOP_LEVEL_KEYS = ["version", "name", "description", "params", "defaults", "budget", "steps"];
|
|
29
|
-
const DEFAULTS_KEYS = ["
|
|
31
|
+
const DEFAULTS_KEYS = ["engine", "model", "timeout", "on_error", "llm"];
|
|
30
32
|
const BUDGET_KEYS = ["max_tokens", "max_units"];
|
|
31
33
|
const STEP_KEYS = ["id", "title", "unit", "map", "route", "output", "gate"];
|
|
32
34
|
const UNIT_KEYS = [
|
|
33
|
-
"
|
|
34
|
-
"profile",
|
|
35
|
+
"engine",
|
|
35
36
|
"model",
|
|
37
|
+
"llm",
|
|
36
38
|
"timeout",
|
|
37
39
|
"retry",
|
|
38
40
|
"on_error",
|
|
@@ -51,13 +53,19 @@ const TIMEOUT_HINT = `Use "<n>ms", "<n>s", "<n>m" (e.g. "10m"), or "none"`;
|
|
|
51
53
|
/**
|
|
52
54
|
* Cheap structural probe for the indexer matcher (mirrors `looksLikeWorkflow`
|
|
53
55
|
* in ../parser.ts). Returns true if the text has the unmistakable top-level
|
|
54
|
-
* shape of a YAML workflow program: `version:
|
|
56
|
+
* shape of a YAML workflow program: `version: 2` and a `steps:` key, both at
|
|
55
57
|
* column 0. Used so the matcher and parser cannot drift.
|
|
56
58
|
*/
|
|
57
59
|
export function looksLikeWorkflowProgram(yamlText) {
|
|
58
|
-
return /^version[ \t]*:[ \t]*['"]?1['"]?[ \t]*(#.*)?$/m.test(yamlText) && /^steps[ \t]*:/m.test(yamlText);
|
|
60
|
+
return /^version[ \t]*:[ \t]*['"]?(?:1|2)['"]?[ \t]*(#.*)?$/m.test(yamlText) && /^steps[ \t]*:/m.test(yamlText);
|
|
59
61
|
}
|
|
60
62
|
export function parseWorkflowProgram(yamlText, source) {
|
|
63
|
+
if (utf8Bytes(yamlText) > WORKFLOW_MAX_SOURCE_BYTES) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
errors: [{ line: 1, message: "Workflow source exceeds the 1 MiB resource limit." }],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
61
69
|
const errors = [];
|
|
62
70
|
const lineCounter = new LineCounter();
|
|
63
71
|
let doc;
|
|
@@ -116,14 +124,19 @@ export function parseWorkflowProgram(yamlText, source) {
|
|
|
116
124
|
return {
|
|
117
125
|
ok: false,
|
|
118
126
|
errors: [
|
|
119
|
-
{ line: 1, message: `A workflow program must be a YAML mapping with "version:
|
|
127
|
+
{ line: 1, message: `A workflow program must be a YAML mapping with "version: 2", "name", and "steps".` },
|
|
120
128
|
],
|
|
121
129
|
};
|
|
122
130
|
}
|
|
123
131
|
checkUnknownKeys(ctx, root, [], TOP_LEVEL_KEYS, "top-level");
|
|
124
|
-
if (root.version !==
|
|
125
|
-
|
|
126
|
-
|
|
132
|
+
if (root.version !== WORKFLOW_PROGRAM_VERSION) {
|
|
133
|
+
if (root.version === 1) {
|
|
134
|
+
ctx.err(["version"], `Workflow version 1 retired; version 2 is required. Replace runner/profile selectors with engine.`);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
const got = root.version === undefined ? "it is missing" : `got ${JSON.stringify(root.version)}`;
|
|
138
|
+
ctx.err(["version"], `"version: 2" is required at the top level (${got}). Only the number 2 is a valid version.`);
|
|
139
|
+
}
|
|
127
140
|
}
|
|
128
141
|
let name = "";
|
|
129
142
|
if (typeof root.name === "string" && root.name.trim() !== "") {
|
|
@@ -150,7 +163,7 @@ export function parseWorkflowProgram(yamlText, source) {
|
|
|
150
163
|
if (errors.length > 0)
|
|
151
164
|
return { ok: false, errors };
|
|
152
165
|
const program = {
|
|
153
|
-
version:
|
|
166
|
+
version: WORKFLOW_PROGRAM_VERSION,
|
|
154
167
|
name,
|
|
155
168
|
...(description !== undefined ? { description } : {}),
|
|
156
169
|
...(params !== undefined ? { params } : {}),
|
|
@@ -171,6 +184,9 @@ function parseParams(ctx, raw) {
|
|
|
171
184
|
ctx.err(["params"], `"params" must be a mapping of param name to a JSON Schema object (e.g. changed_files: { type: array }).`);
|
|
172
185
|
return undefined;
|
|
173
186
|
}
|
|
187
|
+
if (Object.keys(raw).length > WORKFLOW_MAX_PARAMS) {
|
|
188
|
+
ctx.err(["params"], `"params" must contain at most ${WORKFLOW_MAX_PARAMS} entries.`);
|
|
189
|
+
}
|
|
174
190
|
const params = {};
|
|
175
191
|
for (const [paramName, value] of Object.entries(raw)) {
|
|
176
192
|
if (!PROGRAM_PARAM_NAME_PATTERN.test(paramName)) {
|
|
@@ -195,9 +211,12 @@ function parseDefaults(ctx, raw) {
|
|
|
195
211
|
}
|
|
196
212
|
checkUnknownKeys(ctx, raw, path, DEFAULTS_KEYS, `"defaults"`);
|
|
197
213
|
const defaults = {};
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
214
|
+
if (raw.engine !== undefined) {
|
|
215
|
+
if (typeof raw.engine === "string" && raw.engine.trim() !== "")
|
|
216
|
+
defaults.engine = raw.engine.trim();
|
|
217
|
+
else
|
|
218
|
+
ctx.err([...path, "engine"], `"defaults.engine" must be a non-empty engine name.`);
|
|
219
|
+
}
|
|
201
220
|
if (raw.model !== undefined) {
|
|
202
221
|
if (typeof raw.model === "string" && raw.model.trim() !== "")
|
|
203
222
|
defaults.model = raw.model.trim();
|
|
@@ -210,6 +229,9 @@ function parseDefaults(ctx, raw) {
|
|
|
210
229
|
const onError = parseEnumField(ctx, raw.on_error, [...path, "on_error"], `"defaults.on_error"`, PROGRAM_ON_ERROR);
|
|
211
230
|
if (onError !== undefined)
|
|
212
231
|
defaults.onError = onError;
|
|
232
|
+
const llm = parseLlmOverrides(ctx, raw.llm, [...path, "llm"], `"defaults.llm"`);
|
|
233
|
+
if (llm !== undefined)
|
|
234
|
+
defaults.llm = llm;
|
|
213
235
|
return Object.keys(defaults).length > 0 ? defaults : undefined;
|
|
214
236
|
}
|
|
215
237
|
function parseBudget(ctx, raw) {
|
|
@@ -231,11 +253,14 @@ function parseBudget(ctx, raw) {
|
|
|
231
253
|
}
|
|
232
254
|
}
|
|
233
255
|
if (raw.max_units !== undefined) {
|
|
234
|
-
if (typeof raw.max_units === "number" &&
|
|
256
|
+
if (typeof raw.max_units === "number" &&
|
|
257
|
+
Number.isInteger(raw.max_units) &&
|
|
258
|
+
raw.max_units >= 1 &&
|
|
259
|
+
raw.max_units <= WORKFLOW_MAX_MAP_EXPANSION) {
|
|
235
260
|
budget.maxUnits = raw.max_units;
|
|
236
261
|
}
|
|
237
262
|
else {
|
|
238
|
-
ctx.err([...path, "max_units"], `"budget.max_units" must be an integer
|
|
263
|
+
ctx.err([...path, "max_units"], `"budget.max_units" must be an integer from 1 through ${WORKFLOW_MAX_MAP_EXPANSION}.`);
|
|
239
264
|
}
|
|
240
265
|
}
|
|
241
266
|
return Object.keys(budget).length > 0 ? budget : undefined;
|
|
@@ -245,6 +270,9 @@ function parseSteps(ctx, raw) {
|
|
|
245
270
|
ctx.err(["steps"], `"steps" is required and must be a list with at least one step.`);
|
|
246
271
|
return [];
|
|
247
272
|
}
|
|
273
|
+
if (raw.length > WORKFLOW_MAX_STEPS) {
|
|
274
|
+
ctx.err(["steps"], `"steps" must contain at most ${WORKFLOW_MAX_STEPS} entries.`);
|
|
275
|
+
}
|
|
248
276
|
// First pass: collect ids so route targets can be checked against ALL steps
|
|
249
277
|
// (including ones that fail their own validation).
|
|
250
278
|
const idIndex = new Map();
|
|
@@ -359,18 +387,18 @@ function parseUnit(ctx, raw, path, stepLabel) {
|
|
|
359
387
|
if (typeof raw.instructions === "string" && raw.instructions.trim() !== "") {
|
|
360
388
|
unit.instructions = raw.instructions;
|
|
361
389
|
ctx.checkTemplates(raw.instructions, [...path, "instructions"], `${stepLabel} "instructions"`);
|
|
390
|
+
if (utf8Bytes(raw.instructions) > WORKFLOW_MAX_INSTRUCTION_BYTES) {
|
|
391
|
+
ctx.err([...path, "instructions"], `${stepLabel} "instructions" exceeds the 256 KiB resource limit.`);
|
|
392
|
+
}
|
|
362
393
|
}
|
|
363
394
|
else {
|
|
364
395
|
ctx.err([...path, "instructions"], `${stepLabel} "unit" requires non-empty string "instructions".`);
|
|
365
396
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
if (raw.profile !== undefined) {
|
|
370
|
-
if (typeof raw.profile === "string" && raw.profile.trim() !== "")
|
|
371
|
-
unit.profile = raw.profile.trim();
|
|
397
|
+
if (raw.engine !== undefined) {
|
|
398
|
+
if (typeof raw.engine === "string" && raw.engine.trim() !== "")
|
|
399
|
+
unit.engine = raw.engine.trim();
|
|
372
400
|
else
|
|
373
|
-
ctx.err([...path, "
|
|
401
|
+
ctx.err([...path, "engine"], `${stepLabel} "engine" must be a non-empty engine name.`);
|
|
374
402
|
}
|
|
375
403
|
if (raw.model !== undefined) {
|
|
376
404
|
if (typeof raw.model === "string" && raw.model.trim() !== "")
|
|
@@ -378,6 +406,9 @@ function parseUnit(ctx, raw, path, stepLabel) {
|
|
|
378
406
|
else
|
|
379
407
|
ctx.err([...path, "model"], `${stepLabel} "model" must be a non-empty string (a model alias or exact id).`);
|
|
380
408
|
}
|
|
409
|
+
const llm = parseLlmOverrides(ctx, raw.llm, [...path, "llm"], `${stepLabel} "llm"`);
|
|
410
|
+
if (llm !== undefined)
|
|
411
|
+
unit.llm = llm;
|
|
381
412
|
const timeoutMs = parseTimeoutField(ctx, raw.timeout, [...path, "timeout"], `${stepLabel} "timeout"`);
|
|
382
413
|
if (timeoutMs !== undefined)
|
|
383
414
|
unit.timeoutMs = timeoutMs;
|
|
@@ -461,6 +492,9 @@ function parseRoute(ctx, raw, path, stepLabel, stepIndex, routeChecks) {
|
|
|
461
492
|
ctx.err(whenPath, `${stepLabel} "route" requires "when": a mapping of match value to target step id (e.g. when: { pass: ship }).`);
|
|
462
493
|
}
|
|
463
494
|
else if (isMap(whenNode)) {
|
|
495
|
+
if (whenNode.items.length > WORKFLOW_MAX_ROUTE_BRANCHES) {
|
|
496
|
+
ctx.err(whenPath, `${stepLabel} "when" must contain at most ${WORKFLOW_MAX_ROUTE_BRANCHES} branches.`);
|
|
497
|
+
}
|
|
464
498
|
// Walk the AST pairs (not the JS object) so duplicate matches that only
|
|
465
499
|
// collide after stringification ("true" vs true) are still caught.
|
|
466
500
|
const seenMatches = new Map();
|
|
@@ -501,6 +535,9 @@ function parseRoute(ctx, raw, path, stepLabel, stepIndex, routeChecks) {
|
|
|
501
535
|
ctx.err(whenPath, `${stepLabel} "when: ${match}" must map to a step id string.`);
|
|
502
536
|
}
|
|
503
537
|
}
|
|
538
|
+
if (Object.keys(raw.when).length > WORKFLOW_MAX_ROUTE_BRANCHES) {
|
|
539
|
+
ctx.err(whenPath, `${stepLabel} "when" must contain at most ${WORKFLOW_MAX_ROUTE_BRANCHES} branches.`);
|
|
540
|
+
}
|
|
504
541
|
if (Object.keys(raw.when).length === 0) {
|
|
505
542
|
ctx.err(whenPath, `${stepLabel} "when" must contain at least one match → step-id entry.`);
|
|
506
543
|
}
|
|
@@ -634,6 +671,75 @@ function parseEnumField(ctx, raw, path, label, allowed) {
|
|
|
634
671
|
ctx.err(path, `${label} must be one of: ${allowed.join(" | ")} (got ${JSON.stringify(raw)}).`);
|
|
635
672
|
return undefined;
|
|
636
673
|
}
|
|
674
|
+
/** Parse only invocation tuning. Connection identity belongs to a named engine. */
|
|
675
|
+
function parseLlmOverrides(ctx, raw, path, label) {
|
|
676
|
+
if (raw === undefined)
|
|
677
|
+
return undefined;
|
|
678
|
+
if (!isPlainRecord(raw)) {
|
|
679
|
+
ctx.err(path, `${label} must be a mapping of LLM invocation overrides.`);
|
|
680
|
+
return undefined;
|
|
681
|
+
}
|
|
682
|
+
const keys = [
|
|
683
|
+
"temperature",
|
|
684
|
+
"max_tokens",
|
|
685
|
+
"supports_json_schema",
|
|
686
|
+
"extra_params",
|
|
687
|
+
"context_length",
|
|
688
|
+
"enable_thinking",
|
|
689
|
+
];
|
|
690
|
+
checkUnknownKeys(ctx, raw, path, keys, label);
|
|
691
|
+
const result = {};
|
|
692
|
+
if (raw.temperature !== undefined) {
|
|
693
|
+
if (typeof raw.temperature === "number" && Number.isFinite(raw.temperature))
|
|
694
|
+
result.temperature = raw.temperature;
|
|
695
|
+
else
|
|
696
|
+
ctx.err([...path, "temperature"], `${label}.temperature must be a finite number.`);
|
|
697
|
+
}
|
|
698
|
+
if (raw.max_tokens !== undefined) {
|
|
699
|
+
if (typeof raw.max_tokens === "number" && Number.isInteger(raw.max_tokens) && raw.max_tokens > 0) {
|
|
700
|
+
result.maxTokens = raw.max_tokens;
|
|
701
|
+
}
|
|
702
|
+
else
|
|
703
|
+
ctx.err([...path, "max_tokens"], `${label}.max_tokens must be a positive integer.`);
|
|
704
|
+
}
|
|
705
|
+
if (raw.supports_json_schema !== undefined) {
|
|
706
|
+
if (typeof raw.supports_json_schema === "boolean")
|
|
707
|
+
result.supportsJsonSchema = raw.supports_json_schema;
|
|
708
|
+
else
|
|
709
|
+
ctx.err([...path, "supports_json_schema"], `${label}.supports_json_schema must be a boolean.`);
|
|
710
|
+
}
|
|
711
|
+
if (raw.extra_params !== undefined) {
|
|
712
|
+
if (!isPlainRecord(raw.extra_params)) {
|
|
713
|
+
ctx.err([...path, "extra_params"], `${label}.extra_params must be a JSON object.`);
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
const issues = validateExtraParams(raw.extra_params);
|
|
717
|
+
for (const issue of issues) {
|
|
718
|
+
ctx.err([...path, "extra_params", ...issue.path], `${formatExtraParamsIssue(`${label}.extra_params`, issue)}.`);
|
|
719
|
+
}
|
|
720
|
+
if (jsonBytes(raw.extra_params) > WORKFLOW_MAX_EXTRA_PARAMS_BYTES) {
|
|
721
|
+
ctx.err([...path, "extra_params"], `${label}.extra_params exceeds the 64 KiB resource limit.`);
|
|
722
|
+
}
|
|
723
|
+
if (issues.length === 0 && jsonBytes(raw.extra_params) <= WORKFLOW_MAX_EXTRA_PARAMS_BYTES) {
|
|
724
|
+
result.extraParams = raw.extra_params;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (raw.context_length !== undefined) {
|
|
729
|
+
if (typeof raw.context_length === "number" && Number.isInteger(raw.context_length) && raw.context_length > 0) {
|
|
730
|
+
result.contextLength = raw.context_length;
|
|
731
|
+
}
|
|
732
|
+
else
|
|
733
|
+
ctx.err([...path, "context_length"], `${label}.context_length must be a positive integer.`);
|
|
734
|
+
}
|
|
735
|
+
if (raw.enable_thinking !== undefined) {
|
|
736
|
+
if (typeof raw.enable_thinking === "boolean")
|
|
737
|
+
result.enableThinking = raw.enable_thinking;
|
|
738
|
+
else
|
|
739
|
+
ctx.err([...path, "enable_thinking"], `${label}.enable_thinking must be a boolean.`);
|
|
740
|
+
}
|
|
741
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
742
|
+
}
|
|
637
743
|
function parseSchemaObject(ctx, raw, path, label) {
|
|
638
744
|
if (raw === undefined)
|
|
639
745
|
return undefined;
|
|
@@ -641,6 +747,9 @@ function parseSchemaObject(ctx, raw, path, label) {
|
|
|
641
747
|
ctx.err(path, `${label} must be a JSON Schema object (e.g. { type: object, properties: { … } }).`);
|
|
642
748
|
return undefined;
|
|
643
749
|
}
|
|
750
|
+
if (jsonBytes(raw) > WORKFLOW_MAX_SCHEMA_BYTES) {
|
|
751
|
+
ctx.err(path, `${label} exceeds the 256 KiB resource limit.`);
|
|
752
|
+
}
|
|
644
753
|
return raw;
|
|
645
754
|
}
|
|
646
755
|
function checkUnknownKeys(ctx, obj, path, allowed, label) {
|
|
@@ -65,19 +65,18 @@ export function projectProgramParameters(program) {
|
|
|
65
65
|
/**
|
|
66
66
|
* Compact, show-facing orchestration summary for one program step, reusing
|
|
67
67
|
* the existing `WorkflowStepOrchestrationSummary` shape. Field mapping:
|
|
68
|
-
* `
|
|
68
|
+
* `engine`/`model`/`timeoutMs` merge the run-level `defaults` exactly like
|
|
69
69
|
* the compiler does (per-unit wins), `fanOut.over` carries the raw `${{ … }}`
|
|
70
70
|
* expression, and `route` carries the explicit input + branch table.
|
|
71
71
|
* Returns undefined when the step declares nothing worth summarizing.
|
|
72
72
|
*/
|
|
73
73
|
export function summarizeProgramStepOrchestration(step, defaults) {
|
|
74
74
|
const unit = step.unit ?? step.map?.unit;
|
|
75
|
-
const
|
|
75
|
+
const engine = unit?.engine ?? defaults?.engine;
|
|
76
76
|
const model = unit?.model ?? defaults?.model;
|
|
77
77
|
const timeoutMs = unit?.timeoutMs !== undefined ? unit.timeoutMs : defaults?.timeoutMs;
|
|
78
78
|
const summary = {
|
|
79
|
-
...(
|
|
80
|
-
...(unit?.profile !== undefined ? { profile: unit.profile } : {}),
|
|
79
|
+
...(engine !== undefined ? { engine } : {}),
|
|
81
80
|
...(model !== undefined ? { model } : {}),
|
|
82
81
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
83
82
|
...(step.map
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
export const WORKFLOW_PROGRAM_VERSION =
|
|
5
|
-
/**
|
|
4
|
+
export const WORKFLOW_PROGRAM_VERSION = 2;
|
|
5
|
+
/** @deprecated source v2 rejects runner; retained for TypeScript migration only. */
|
|
6
6
|
export const PROGRAM_RUNNER_KINDS = ["llm", "agent", "sdk", "inherit"];
|
|
7
7
|
/** How a map step folds its per-item unit results into the step artifact. */
|
|
8
8
|
export const PROGRAM_REDUCERS = ["collect", "vote"];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
export const WORKFLOW_MAX_PLAN_BYTES = 2 * 1024 * 1024;
|
|
5
|
+
export const WORKFLOW_MAX_SOURCE_BYTES = 1024 * 1024;
|
|
6
|
+
export const WORKFLOW_MAX_STEPS = 256;
|
|
7
|
+
export const WORKFLOW_MAX_ENGINES = 64;
|
|
8
|
+
export const WORKFLOW_MAX_PARAMS = 128;
|
|
9
|
+
export const WORKFLOW_MAX_ROUTE_BRANCHES = 256;
|
|
10
|
+
export const WORKFLOW_MAX_INSTRUCTION_BYTES = 256 * 1024;
|
|
11
|
+
export const WORKFLOW_MAX_SCHEMA_BYTES = 256 * 1024;
|
|
12
|
+
export const WORKFLOW_MAX_EXTRA_PARAMS_BYTES = 64 * 1024;
|
|
13
|
+
export const WORKFLOW_MAX_JSON_DEPTH = 64;
|
|
14
|
+
export const WORKFLOW_MAX_MAP_EXPANSION = 10_000;
|
|
15
|
+
export function utf8Bytes(value) {
|
|
16
|
+
return Buffer.byteLength(value, "utf8");
|
|
17
|
+
}
|
|
18
|
+
export function jsonBytes(value) {
|
|
19
|
+
return utf8Bytes(JSON.stringify(value));
|
|
20
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { UsageError } from "../../core/errors.js";
|
|
5
|
+
import { decodeCanonicalPlan } from "../ir/plan-hash.js";
|
|
6
|
+
import { WORKFLOW_IR_VERSION } from "../ir/schema.js";
|
|
7
|
+
/** One policy authority for executable versus inspection-only historical runs. */
|
|
8
|
+
export function classifyWorkflowRunPlan(row) {
|
|
9
|
+
const runId = row.id ?? "(unknown)";
|
|
10
|
+
if (!row.plan_json) {
|
|
11
|
+
if (row.plan_ir_version === WORKFLOW_IR_VERSION) {
|
|
12
|
+
return {
|
|
13
|
+
support: "corrupt-plan",
|
|
14
|
+
irVersion: row.plan_ir_version,
|
|
15
|
+
error: `Workflow run ${runId} declares workflow IR version ${row.plan_ir_version} but has no frozen plan.`,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
support: row.plan_ir_version === null || row.plan_ir_version === undefined ? "missing-plan" : "unsupported-version",
|
|
20
|
+
irVersion: row.plan_ir_version ?? null,
|
|
21
|
+
error: row.plan_ir_version === null || row.plan_ir_version === undefined
|
|
22
|
+
? `Workflow run ${runId} has no executable workflow IR plan.`
|
|
23
|
+
: `Workflow run ${runId} uses unsupported workflow IR version ${String(row.plan_ir_version)} and has no frozen plan.`,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
let raw;
|
|
27
|
+
try {
|
|
28
|
+
raw = JSON.parse(row.plan_json);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
if (row.plan_ir_version !== WORKFLOW_IR_VERSION) {
|
|
32
|
+
return {
|
|
33
|
+
support: row.plan_ir_version === null || row.plan_ir_version === undefined ? "missing-plan" : "unsupported-version",
|
|
34
|
+
irVersion: row.plan_ir_version ?? null,
|
|
35
|
+
error: `Workflow run ${runId} has malformed historical frozen plan JSON that cannot be executed.`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
support: "corrupt-plan",
|
|
40
|
+
irVersion: row.plan_ir_version ?? null,
|
|
41
|
+
error: `Workflow run ${runId} has corrupt frozen plan JSON.`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const decodedVersion = typeof raw === "object" && raw !== null ? raw.irVersion : null;
|
|
45
|
+
if (!Number.isSafeInteger(decodedVersion) || decodedVersion < 1) {
|
|
46
|
+
if (row.plan_ir_version !== WORKFLOW_IR_VERSION) {
|
|
47
|
+
return {
|
|
48
|
+
support: row.plan_ir_version === null || row.plan_ir_version === undefined ? "missing-plan" : "unsupported-version",
|
|
49
|
+
irVersion: row.plan_ir_version ?? null,
|
|
50
|
+
error: `Workflow run ${runId} has historical frozen plan data with no supported IR version.`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
support: "corrupt-plan",
|
|
55
|
+
irVersion: row.plan_ir_version ?? null,
|
|
56
|
+
error: `Workflow run ${runId} has a missing or invalid frozen plan IR version.`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (row.plan_ir_version !== null && row.plan_ir_version !== undefined && row.plan_ir_version !== decodedVersion) {
|
|
60
|
+
if (row.plan_ir_version !== WORKFLOW_IR_VERSION && decodedVersion !== WORKFLOW_IR_VERSION) {
|
|
61
|
+
return {
|
|
62
|
+
support: "unsupported-version",
|
|
63
|
+
irVersion: decodedVersion,
|
|
64
|
+
error: `Workflow run ${runId} uses unsupported workflow IR version ${String(decodedVersion)} with mismatched historical metadata.`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
support: "corrupt-plan",
|
|
69
|
+
irVersion: decodedVersion,
|
|
70
|
+
error: `Workflow run ${runId} has mismatched stored plan IR version (${String(row.plan_ir_version)} != ${String(decodedVersion)}).`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (decodedVersion !== WORKFLOW_IR_VERSION) {
|
|
74
|
+
return {
|
|
75
|
+
support: "unsupported-version",
|
|
76
|
+
irVersion: decodedVersion,
|
|
77
|
+
error: `Workflow run ${runId} uses unsupported workflow IR version ${String(decodedVersion)}.`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (row.plan_ir_version !== WORKFLOW_IR_VERSION) {
|
|
81
|
+
return {
|
|
82
|
+
support: "corrupt-plan",
|
|
83
|
+
irVersion: row.plan_ir_version ?? null,
|
|
84
|
+
error: `Workflow run ${runId} has no stored plan IR version.`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
return {
|
|
89
|
+
support: "supported",
|
|
90
|
+
irVersion: WORKFLOW_IR_VERSION,
|
|
91
|
+
plan: decodeCanonicalPlan(runId, row.plan_json, row.plan_hash),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (cause) {
|
|
95
|
+
return { support: "corrupt-plan", irVersion: 3, error: cause instanceof Error ? cause.message : String(cause) };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Reject an execution mutation while preserving inspection and abandon access. */
|
|
99
|
+
export function requireExecutableWorkflowPlan(row) {
|
|
100
|
+
const classified = classifyWorkflowRunPlan(row);
|
|
101
|
+
if (classified.support === "supported")
|
|
102
|
+
return classified.plan;
|
|
103
|
+
if (classified.support === "missing-plan" || classified.support === "unsupported-version") {
|
|
104
|
+
throw new UsageError(`${classified.error} This historical run is inspection-only; abandon it with \`akm workflow abandon ${row.id ?? "<run>"}\` and start a new run.`, "WORKFLOW_IR_VERSION_UNSUPPORTED");
|
|
105
|
+
}
|
|
106
|
+
throw new UsageError(classified.error, "INVALID_JSON_ARGUMENT");
|
|
107
|
+
}
|
|
108
|
+
/** Abandon is the sole mutation allowed for a historical run; corrupt v3 data is never mutated. */
|
|
109
|
+
export function requireAbandonableWorkflowPlan(row) {
|
|
110
|
+
const classified = classifyWorkflowRunPlan(row);
|
|
111
|
+
if (classified.support === "corrupt-plan")
|
|
112
|
+
throw new UsageError(classified.error, "INVALID_JSON_ARGUMENT");
|
|
113
|
+
}
|
|
114
|
+
/** Project persisted spine rows from the decoded plan, never from the mutable source asset. */
|
|
115
|
+
export function frozenStepRows(plan) {
|
|
116
|
+
return plan.steps.map((step) => ({
|
|
117
|
+
stepId: step.stepId,
|
|
118
|
+
stepTitle: step.title,
|
|
119
|
+
instructions: step.root
|
|
120
|
+
? step.root.kind === "map"
|
|
121
|
+
? step.root.template.instructions
|
|
122
|
+
: step.root.instructions
|
|
123
|
+
: routeInstructions(step.route),
|
|
124
|
+
completionJson: step.gate.criteria.length > 0 ? JSON.stringify(step.gate.criteria) : null,
|
|
125
|
+
sequenceIndex: step.sequenceIndex,
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
/** Verify the durable spine still agrees with the decoded/hash-verified plan before any mutation. */
|
|
129
|
+
export function assertWorkflowSpineMatchesPlan(plan, run, rows) {
|
|
130
|
+
const expected = frozenStepRows(plan);
|
|
131
|
+
if (rows.length !== expected.length)
|
|
132
|
+
corruptSpine(run.id, "step count differs from the frozen plan");
|
|
133
|
+
for (let index = 0; index < expected.length; index++) {
|
|
134
|
+
const actual = rows[index];
|
|
135
|
+
const planned = expected[index];
|
|
136
|
+
if (!actual ||
|
|
137
|
+
!planned ||
|
|
138
|
+
actual.step_id !== planned.stepId ||
|
|
139
|
+
actual.step_title !== planned.stepTitle ||
|
|
140
|
+
actual.instructions !== planned.instructions ||
|
|
141
|
+
actual.completion_json !== planned.completionJson ||
|
|
142
|
+
actual.sequence_index !== planned.sequenceIndex) {
|
|
143
|
+
const fields = [
|
|
144
|
+
actual.step_id !== planned.stepId ? "step_id" : "",
|
|
145
|
+
actual.step_title !== planned.stepTitle ? "step_title" : "",
|
|
146
|
+
actual.instructions !== planned.instructions ? "instructions" : "",
|
|
147
|
+
actual.completion_json !== planned.completionJson ? "completion_json" : "",
|
|
148
|
+
actual.sequence_index !== planned.sequenceIndex ? "sequence_index" : "",
|
|
149
|
+
].filter(Boolean);
|
|
150
|
+
corruptSpine(run.id, `step row ${index} differs from the frozen plan (${fields.join(", ") || "missing row"})`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (run.current_step_id !== null && !expected.some((step) => step.stepId === run.current_step_id))
|
|
154
|
+
corruptSpine(run.id, `current step ${run.current_step_id} is not in the frozen plan`);
|
|
155
|
+
const current = run.current_step_id ? rows.find((row) => row.step_id === run.current_step_id) : undefined;
|
|
156
|
+
if (run.status === "active") {
|
|
157
|
+
const firstPending = rows.find((row) => row.status === "pending");
|
|
158
|
+
if (!current || current.status !== "pending" || firstPending?.step_id !== current.step_id)
|
|
159
|
+
corruptSpine(run.id, "active status/current step does not match the first pending plan step");
|
|
160
|
+
}
|
|
161
|
+
else if (run.status === "blocked") {
|
|
162
|
+
if (!current || current.status !== "blocked")
|
|
163
|
+
corruptSpine(run.id, `${run.status} status does not match the current plan step`);
|
|
164
|
+
}
|
|
165
|
+
else if (run.status === "failed") {
|
|
166
|
+
// `workflow abandon` marks the run failed while intentionally leaving its
|
|
167
|
+
// current step pending so `resume` can reopen the same work.
|
|
168
|
+
if (!current || (current.status !== "failed" && current.status !== "pending"))
|
|
169
|
+
corruptSpine(run.id, `${run.status} status does not match the current plan step`);
|
|
170
|
+
}
|
|
171
|
+
else if (run.status === "completed") {
|
|
172
|
+
if (run.current_step_id !== null ||
|
|
173
|
+
rows.some((row) => row.status === "pending" || row.status === "blocked" || row.status === "failed"))
|
|
174
|
+
corruptSpine(run.id, "completed status disagrees with the plan spine");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function routeInstructions(route) {
|
|
178
|
+
const branches = Object.entries(route.when)
|
|
179
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
180
|
+
.map(([match, stepId]) => `"${match}" -> ${stepId}`);
|
|
181
|
+
if (route.defaultStepId !== undefined)
|
|
182
|
+
branches.push(`default -> ${route.defaultStepId}`);
|
|
183
|
+
return `Route on ${route.input}: ${branches.join(", ")}.`;
|
|
184
|
+
}
|
|
185
|
+
function corruptSpine(runId, detail) {
|
|
186
|
+
throw new UsageError(`Workflow run ${runId} has a corrupt durable step spine: ${detail}. Refusing to mutate state that disagrees with its frozen plan.`, "INVALID_JSON_ARGUMENT");
|
|
187
|
+
}
|