akm-cli 0.9.0-rc.0 → 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 +339 -2
- 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/prompts/workflow-unit-preamble.md +26 -0
- 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/env-binding.js +95 -0
- package/dist/commands/env/env-cli.js +8 -65
- 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/migration-help.js +7 -4
- 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/commands/workflow-cli.js +276 -12
- package/dist/core/asset/asset-spec.js +58 -1
- 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 +379 -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/json-schema.js +142 -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 +118 -2
- 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/indexer/walk/matchers.js +39 -0
- package/dist/integrations/agent/builder-shared.js +7 -0
- package/dist/integrations/agent/builders.js +5 -50
- 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 +16 -2
- package/dist/integrations/agent/profiles.js +36 -62
- package/dist/integrations/agent/runner-dispatch.js +91 -4
- package/dist/integrations/agent/runner.js +76 -207
- package/dist/integrations/agent/spawn.js +141 -20
- package/dist/integrations/harnesses/aider/agent-builder.js +112 -0
- package/dist/integrations/harnesses/aider/index.js +57 -0
- package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
- package/dist/integrations/harnesses/amazonq/agent-builder.js +152 -0
- package/dist/integrations/harnesses/amazonq/index.js +58 -0
- package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
- package/dist/integrations/harnesses/claude/agent-builder.js +46 -8
- package/dist/integrations/harnesses/claude/index.js +25 -25
- package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
- package/dist/integrations/harnesses/codex/agent-builder.js +136 -0
- package/dist/integrations/harnesses/codex/index.js +62 -0
- package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
- package/dist/integrations/harnesses/copilot/agent-builder.js +121 -0
- package/dist/integrations/harnesses/copilot/index.js +59 -0
- package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
- package/dist/integrations/harnesses/gemini/agent-builder.js +120 -0
- package/dist/integrations/harnesses/gemini/index.js +59 -0
- package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
- package/dist/integrations/harnesses/index.js +27 -28
- package/dist/integrations/harnesses/opencode/agent-builder.js +2 -3
- package/dist/integrations/harnesses/opencode/index.js +15 -22
- package/dist/integrations/harnesses/opencode-sdk/harness.js +60 -0
- package/dist/integrations/harnesses/opencode-sdk/index.js +8 -32
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
- package/dist/integrations/harnesses/openhands/agent-builder.js +125 -0
- package/dist/integrations/harnesses/openhands/index.js +57 -0
- package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
- package/dist/integrations/harnesses/pi/agent-builder.js +103 -0
- package/dist/integrations/harnesses/pi/index.js +57 -0
- package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
- package/dist/integrations/harnesses/types.js +8 -32
- package/dist/integrations/lockfile.js +32 -21
- package/dist/integrations/session-logs/index.js +24 -11
- 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/renderers.js +3 -2
- package/dist/output/shapes/helpers.js +2 -1
- package/dist/output/shapes/passthrough.js +6 -0
- package/dist/output/text/helpers.js +215 -2
- package/dist/output/text/workflow.js +3 -1
- package/dist/schemas/akm-config.json +16638 -0
- package/dist/schemas/akm-task.json +87 -0
- package/dist/schemas/akm-workflow.json +372 -0
- package/dist/scripts/migrate-storage.js +10944 -8801
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9247 -350
- 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 +190 -1
- 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/text-import-hook.mjs +1 -1
- package/dist/wiki/wiki.js +9 -8
- package/dist/workflows/authoring/authoring.js +123 -10
- package/dist/workflows/authoring/workflow-program-template.yaml +31 -0
- package/dist/workflows/cli.js +4 -0
- package/dist/workflows/concurrency-policy.js +15 -0
- package/dist/workflows/db.js +200 -13
- package/dist/workflows/exec/brief.js +478 -0
- package/dist/workflows/exec/frozen-judge.js +47 -0
- package/dist/workflows/exec/native-executor.js +1034 -0
- package/dist/workflows/exec/param-secrets.js +115 -0
- package/dist/workflows/exec/report.js +1355 -0
- package/dist/workflows/exec/run-workflow.js +609 -0
- package/dist/workflows/exec/scheduler.js +71 -0
- package/dist/workflows/exec/step-work.js +1212 -0
- package/dist/workflows/exec/unit-writer.js +23 -0
- package/dist/workflows/exec/watch.js +116 -0
- package/dist/workflows/exec/worktree.js +171 -0
- package/dist/workflows/ir/compile.js +375 -0
- package/dist/workflows/ir/freeze.js +243 -0
- package/dist/workflows/ir/params.js +54 -0
- package/dist/workflows/ir/plan-hash.js +68 -0
- package/dist/workflows/ir/schema.js +545 -0
- package/dist/workflows/parser.js +10 -1
- package/dist/workflows/program/expressions.js +369 -0
- package/dist/workflows/program/parser.js +869 -0
- package/dist/workflows/program/project.js +104 -0
- package/dist/workflows/program/schema.js +54 -0
- package/dist/workflows/renderer.js +82 -5
- package/dist/workflows/resource-limits.js +20 -0
- package/dist/workflows/runtime/agent-identity.js +59 -14
- package/dist/workflows/runtime/plan-classifier.js +187 -0
- package/dist/workflows/runtime/runs.js +246 -69
- package/dist/workflows/runtime/unit-checkin.js +45 -0
- package/dist/workflows/runtime/workflow-asset-loader.js +42 -1
- package/dist/workflows/validate-summary.js +24 -3
- package/dist/workflows/validator.js +26 -1
- package/docs/data-and-telemetry.md +4 -3
- 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/release-notes/0.9.0-beta.60.md +19 -0
- 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
|
@@ -0,0 +1,375 @@
|
|
|
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
|
+
/**
|
|
5
|
+
* Frontends -> unresolved workflow plan compilers.
|
|
6
|
+
*
|
|
7
|
+
* Two frontends, one source-plan shape. Engine resolution in `freeze.ts`
|
|
8
|
+
* lowers this shape into the only executable format, workflow IR v3.
|
|
9
|
+
*
|
|
10
|
+
* - {@link compileWorkflowProgram} — YAML orchestration programs
|
|
11
|
+
* (`program/parser.ts`). Pure and deterministic; performs FULL expression
|
|
12
|
+
* validation (closed `${{ … }}` grammar, earlier-step references,
|
|
13
|
+
* whole-value contexts, `item`/`item_index` scoping) and MERGES the
|
|
14
|
+
* Returns accumulated `WorkflowError`s rather than throwing.
|
|
15
|
+
* - {@link compileWorkflowPlan} — classic LINEAR markdown workflows
|
|
16
|
+
* (`parser.ts`), the stable CLI contract: one unit node per step with the
|
|
17
|
+
* fail-fast default. The P1
|
|
18
|
+
* markdown orchestration grammar is gone — this path is linear-only.
|
|
19
|
+
*
|
|
20
|
+
* Node-id convention (stable, unique within a plan):
|
|
21
|
+
* step root → `<stepId>` (agent) or `<stepId>.map` (map)
|
|
22
|
+
* map unit → `<stepId>.unit` (template instantiated per item)
|
|
23
|
+
* gate → `<stepId>.gate`
|
|
24
|
+
*/
|
|
25
|
+
import { formatReference, listReferences, parseTemplate } from "../program/expressions.js";
|
|
26
|
+
/**
|
|
27
|
+
* Compile a parsed YAML program into a frozen-plan-ready graph. Assumes the
|
|
28
|
+
* program came out of `parseWorkflowProgram` ok (structure already valid);
|
|
29
|
+
* this pass owns the expression-language rules the parser deliberately does
|
|
30
|
+
* not check:
|
|
31
|
+
*
|
|
32
|
+
* - every `${{ … }}` in instructions / `map.over` / `route.input` parses
|
|
33
|
+
* against the CLOSED grammar;
|
|
34
|
+
* - `steps.<id>` references name an EARLIER step (a producer that has
|
|
35
|
+
* already run when the reference resolves);
|
|
36
|
+
* - `map.over` and `route.input` are single whole-value references — a bare
|
|
37
|
+
* `${{ … }}` with no surrounding text;
|
|
38
|
+
* - `item` / `item_index` appear only inside a map unit's instructions.
|
|
39
|
+
*/
|
|
40
|
+
export function compileWorkflowProgram(program) {
|
|
41
|
+
const errors = [];
|
|
42
|
+
const allStepIds = new Set(program.steps.map((s) => s.id));
|
|
43
|
+
const earlierStepIds = new Set();
|
|
44
|
+
const steps = [];
|
|
45
|
+
program.steps.forEach((step, index) => {
|
|
46
|
+
const check = { allStepIds, earlierStepIds, errors };
|
|
47
|
+
if (step.unit) {
|
|
48
|
+
checkTemplateExpressions(step.unit.instructions, {
|
|
49
|
+
...check,
|
|
50
|
+
line: step.unit.source.start,
|
|
51
|
+
label: `Step "${step.id}" instructions`,
|
|
52
|
+
inMapUnit: false,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (step.map) {
|
|
56
|
+
checkWholeValueExpression(step.map.over, {
|
|
57
|
+
...check,
|
|
58
|
+
line: step.source.start,
|
|
59
|
+
label: `Step "${step.id}" map.over`,
|
|
60
|
+
});
|
|
61
|
+
checkTemplateExpressions(step.map.unit.instructions, {
|
|
62
|
+
...check,
|
|
63
|
+
line: step.map.unit.source.start,
|
|
64
|
+
label: `Step "${step.id}" instructions`,
|
|
65
|
+
inMapUnit: true,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (step.route) {
|
|
69
|
+
checkWholeValueExpression(step.route.input, {
|
|
70
|
+
...check,
|
|
71
|
+
line: step.source.start,
|
|
72
|
+
label: `Step "${step.id}" route.input`,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
steps.push(compileProgramStep(step, index, program.defaults));
|
|
76
|
+
earlierStepIds.add(step.id);
|
|
77
|
+
});
|
|
78
|
+
if (errors.length > 0)
|
|
79
|
+
return { ok: false, errors };
|
|
80
|
+
const paramNames = program.params ? Object.keys(program.params) : [];
|
|
81
|
+
return {
|
|
82
|
+
ok: true,
|
|
83
|
+
// Non-fatal advisories (redesign addendum). Warnings NEVER change the plan
|
|
84
|
+
// or its hash — they are computed alongside the frozen plan and surfaced by
|
|
85
|
+
// `workflow validate` / `workflow start`, never persisted onto the run row.
|
|
86
|
+
warnings: collectProgramWarnings(program),
|
|
87
|
+
plan: {
|
|
88
|
+
title: program.name,
|
|
89
|
+
...(paramNames.length > 0 ? { params: paramNames } : {}),
|
|
90
|
+
// Reviewer #12: freeze the per-param schemas into the plan so `--params`
|
|
91
|
+
// can be validated at start and re-asserted at brief/report against the
|
|
92
|
+
// exact schemas the run was created with (the plan hash covers them).
|
|
93
|
+
...(program.params && paramNames.length > 0 ? { paramSchemas: program.params } : {}),
|
|
94
|
+
// Budget ceilings (addendum R2): frozen onto the plan so enforcement is
|
|
95
|
+
// a pure function of (frozen plan, journal) — never the live asset.
|
|
96
|
+
...(program.budget
|
|
97
|
+
? {
|
|
98
|
+
budget: {
|
|
99
|
+
...(program.budget.maxTokens !== undefined ? { maxTokens: program.budget.maxTokens } : {}),
|
|
100
|
+
...(program.budget.maxUnits !== undefined ? { maxUnits: program.budget.maxUnits } : {}),
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
: {}),
|
|
104
|
+
steps,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function compileProgramStep(step, index, defaults) {
|
|
109
|
+
const gate = {
|
|
110
|
+
kind: "gate",
|
|
111
|
+
id: `${step.id}.gate`,
|
|
112
|
+
stepId: step.id,
|
|
113
|
+
criteria: step.gate?.criteria ?? [],
|
|
114
|
+
// TODO(R2): maxLoops execution (bounded evaluator-optimizer) is engine
|
|
115
|
+
// rework scope; carried through the frozen plan now.
|
|
116
|
+
...(step.gate?.maxLoops !== undefined ? { maxLoops: step.gate.maxLoops } : {}),
|
|
117
|
+
// Reviewer #18: a required gate rides the frozen plan so BOTH surfaces
|
|
118
|
+
// (engine + report) enforce it identically.
|
|
119
|
+
...(step.gate?.required !== undefined ? { required: step.gate.required } : {}),
|
|
120
|
+
};
|
|
121
|
+
let root;
|
|
122
|
+
if (step.unit) {
|
|
123
|
+
root = compileProgramUnit(step.unit, step.id, defaults);
|
|
124
|
+
}
|
|
125
|
+
else if (step.map) {
|
|
126
|
+
root = {
|
|
127
|
+
kind: "map",
|
|
128
|
+
id: `${step.id}.map`,
|
|
129
|
+
over: step.map.over,
|
|
130
|
+
template: compileProgramUnit(step.map.unit, `${step.id}.unit`, defaults),
|
|
131
|
+
...(step.map.concurrency !== undefined ? { concurrency: step.map.concurrency } : {}),
|
|
132
|
+
reducer: step.map.reducer ?? "collect",
|
|
133
|
+
source: step.source,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
stepId: step.id,
|
|
138
|
+
title: step.title ?? step.id,
|
|
139
|
+
sequenceIndex: index,
|
|
140
|
+
...(root ? { root } : {}),
|
|
141
|
+
...(step.route
|
|
142
|
+
? {
|
|
143
|
+
route: {
|
|
144
|
+
input: step.route.input,
|
|
145
|
+
when: Object.fromEntries(step.route.branches.map((b) => [b.match, b.stepId])),
|
|
146
|
+
...(step.route.defaultStepId !== undefined ? { defaultStepId: step.route.defaultStepId } : {}),
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
: {}),
|
|
150
|
+
// TODO(R2): validating the reducer result against this schema (typed step
|
|
151
|
+
// artifacts) is engine-rework scope; the frozen plan carries it now.
|
|
152
|
+
...(step.output !== undefined ? { outputSchema: step.output } : {}),
|
|
153
|
+
gate,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Lower one source unit into the unresolved structural plan. Engine/model/time
|
|
158
|
+
* settings remain on the parsed source until the single freeze boundary.
|
|
159
|
+
*/
|
|
160
|
+
function compileProgramUnit(unit, id, defaults) {
|
|
161
|
+
return {
|
|
162
|
+
kind: "unit",
|
|
163
|
+
id,
|
|
164
|
+
instructions: unit.instructions,
|
|
165
|
+
// YAML program instructions are `${{ … }}` templates (validated above);
|
|
166
|
+
// the executor resolves them per unit.
|
|
167
|
+
templating: "expressions",
|
|
168
|
+
...(unit.output !== undefined ? { schema: unit.output } : {}),
|
|
169
|
+
// TODO(R2): retry dispatch is engine-rework scope; carried through now.
|
|
170
|
+
...(unit.retry ? { retry: { max: unit.retry.max, on: [...unit.retry.on] } } : {}),
|
|
171
|
+
onError: unit.onError ?? defaults?.onError ?? "fail",
|
|
172
|
+
...(unit.env ? { env: [...unit.env] } : {}),
|
|
173
|
+
...(unit.isolation !== undefined ? { isolation: unit.isolation } : {}),
|
|
174
|
+
source: unit.source,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/** Validate every `${{ … }}` in a free-text template (instructions). */
|
|
178
|
+
function checkTemplateExpressions(text, check) {
|
|
179
|
+
const parsed = parseTemplate(text);
|
|
180
|
+
if (!parsed.ok) {
|
|
181
|
+
for (const err of parsed.errors) {
|
|
182
|
+
check.errors.push({ line: check.line, message: `${check.label}: ${err.message}` });
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
for (const ref of listReferences(parsed.segments)) {
|
|
187
|
+
checkReference(ref, check, check.inMapUnit);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Validate a whole-value field (`map.over`, `route.input`): the text must be
|
|
192
|
+
* exactly one `${{ … }}` reference with no surrounding literal text, so the
|
|
193
|
+
* engine can resolve it to a RAW value (array/object), never a string splice.
|
|
194
|
+
*/
|
|
195
|
+
function checkWholeValueExpression(text, check) {
|
|
196
|
+
const parsed = parseTemplate(text);
|
|
197
|
+
if (!parsed.ok) {
|
|
198
|
+
for (const err of parsed.errors) {
|
|
199
|
+
check.errors.push({ line: check.line, message: `${check.label}: ${err.message}` });
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const [first] = parsed.segments;
|
|
204
|
+
if (parsed.segments.length !== 1 || first?.kind !== "reference") {
|
|
205
|
+
check.errors.push({
|
|
206
|
+
line: check.line,
|
|
207
|
+
message: `${check.label} must be a single whole-value \${{ … }} reference with no surrounding text ` +
|
|
208
|
+
`(e.g. "\${{ steps.discover.output.files }}"), got ${JSON.stringify(text)}.`,
|
|
209
|
+
});
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// `item`/`item_index` never exist where a whole-value field resolves (the
|
|
213
|
+
// item list itself, or a spine route input), so inMapUnit is always false.
|
|
214
|
+
checkReference(first.expr, check, false);
|
|
215
|
+
}
|
|
216
|
+
function checkReference(ref, check, inMapUnit) {
|
|
217
|
+
switch (ref.kind) {
|
|
218
|
+
case "item":
|
|
219
|
+
case "itemIndex": {
|
|
220
|
+
if (!inMapUnit) {
|
|
221
|
+
check.errors.push({
|
|
222
|
+
line: check.line,
|
|
223
|
+
message: `${check.label}: "\${{ ${formatReference(ref)} }}" is only valid inside a map unit's instructions.`,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
case "stepOutput": {
|
|
229
|
+
if (!check.earlierStepIds.has(ref.stepId)) {
|
|
230
|
+
const why = check.allStepIds.has(ref.stepId)
|
|
231
|
+
? `step "${ref.stepId}" does not come before this step — references must name an earlier step (a producer that has already run)`
|
|
232
|
+
: `"${ref.stepId}" is not a step in this workflow`;
|
|
233
|
+
check.errors.push({
|
|
234
|
+
line: check.line,
|
|
235
|
+
message: `${check.label}: "\${{ ${formatReference(ref)} }}" cannot be resolved — ${why}.`,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
case "param": {
|
|
241
|
+
// Param presence is a RUN-SCOPE concern, never a compile-time one. A
|
|
242
|
+
// declared `params:` block is NOT a closed set of legal references: the
|
|
243
|
+
// runtime resolves any param SUPPLIED at start (`resolveReference`), and
|
|
244
|
+
// `validateWorkflowParams` documents that undeclared params are permitted
|
|
245
|
+
// — so `${{ params.mode }}` with `mode` passed via `--params` runs fine
|
|
246
|
+
// even when only `files` is declared. At compile time an undeclared
|
|
247
|
+
// reference is indistinguishable from that legitimate start-supplied
|
|
248
|
+
// extra, so treating the block as closed would reject a runtime-supported
|
|
249
|
+
// authoring pattern and put the two layers in disagreement. A genuine typo
|
|
250
|
+
// (`params.changed_file` for `changed_files`) surfaces at run time with a
|
|
251
|
+
// precise "is not defined in the run's params" error instead. As a lint-time
|
|
252
|
+
// heads-up short of rejection, `collectProgramWarnings` (below) emits a
|
|
253
|
+
// non-fatal WARNING for an undeclared reference when a `params:` block is
|
|
254
|
+
// declared — never an error, so the runtime-supported pattern still compiles.
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// ── Non-fatal warnings ───────────────────────────────────────────────────────
|
|
260
|
+
/**
|
|
261
|
+
* Collect the program's non-fatal WARNINGS — advisories that never fail
|
|
262
|
+
* compilation, never change the frozen plan or its hash, and are surfaced by
|
|
263
|
+
* `workflow validate` (human + JSON) and as `warn()` lines at `workflow start`.
|
|
264
|
+
*
|
|
265
|
+
* Two promised-but-previously-missing warnings (redesign addendum):
|
|
266
|
+
*
|
|
267
|
+
* A. A unit/map step with NO step-level `output:` schema carries its units'
|
|
268
|
+
* raw results as an untyped artifact — permitted, but the addendum says
|
|
269
|
+
* "the validator warns". Anchored on the STEP's `output` (the reducer /
|
|
270
|
+
* step-artifact schema); a per-unit `output:` types the unit result but
|
|
271
|
+
* leaves the step artifact untyped.
|
|
272
|
+
* B. A `${{ params.<name> }}` reference to an UNDECLARED param, but ONLY when
|
|
273
|
+
* the program declares a `params:` block. Compile-time REJECTION was tried
|
|
274
|
+
* and reverted (see the `case "param"` note above): the runtime legitimately
|
|
275
|
+
* resolves any param supplied at start, declared or not. The agreed middle
|
|
276
|
+
* ground is a warning — a likely typo (`changed_file` for `changed_files`)
|
|
277
|
+
* surfaces at lint time, while a genuinely start-supplied extra still runs.
|
|
278
|
+
* With no `params:` block there is nothing to compare against, so B is silent.
|
|
279
|
+
*
|
|
280
|
+
* Pure and deterministic; the returned order is document order per step
|
|
281
|
+
* (warning A, then each undeclared-param reference in field/document order).
|
|
282
|
+
* Only called on an OK compile, so every template here already parsed cleanly.
|
|
283
|
+
*/
|
|
284
|
+
export function collectProgramWarnings(program) {
|
|
285
|
+
const warnings = [];
|
|
286
|
+
const declaredParams = program.params ? new Set(Object.keys(program.params)) : undefined;
|
|
287
|
+
for (const step of program.steps) {
|
|
288
|
+
// Warning A — a unit/map step with no step-level output schema.
|
|
289
|
+
if ((step.unit || step.map) && step.output === undefined) {
|
|
290
|
+
warnings.push({
|
|
291
|
+
line: step.source.start,
|
|
292
|
+
message: `Step "${step.id}" declares no \`output:\` schema — its unit results are carried as an untyped ` +
|
|
293
|
+
`artifact (permitted). Add an \`output:\` JSON Schema to type and validate the step artifact.`,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
// Warning B — references to a param the declared `params:` block omits.
|
|
297
|
+
if (declaredParams)
|
|
298
|
+
collectUndeclaredParamWarnings(step, declaredParams, warnings);
|
|
299
|
+
}
|
|
300
|
+
return warnings;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Push a warning for every `${{ params.<name> }}` reference in `step` whose
|
|
304
|
+
* name is not in the declared param set. Walks the same template-bearing
|
|
305
|
+
* fields the compiler validates (unit / map.over + map unit / route.input) so
|
|
306
|
+
* the step + field context in the message matches the error labels.
|
|
307
|
+
*/
|
|
308
|
+
function collectUndeclaredParamWarnings(step, declared, warnings) {
|
|
309
|
+
const declaredList = [...declared].join(", ");
|
|
310
|
+
const scan = (text, line, label) => {
|
|
311
|
+
const parsed = parseTemplate(text);
|
|
312
|
+
if (!parsed.ok)
|
|
313
|
+
return; // OK compile guarantees this parses; defensive only.
|
|
314
|
+
for (const ref of listReferences(parsed.segments)) {
|
|
315
|
+
if (ref.kind !== "param" || declared.has(ref.name))
|
|
316
|
+
continue;
|
|
317
|
+
warnings.push({
|
|
318
|
+
line,
|
|
319
|
+
message: `${label}: "\${{ ${formatReference(ref)} }}" references a param not declared in \`params:\` ` +
|
|
320
|
+
`(declared: ${declaredList || "none"}) — likely a typo. An undeclared param supplied at start still ` +
|
|
321
|
+
`resolves at run time.`,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
if (step.unit)
|
|
326
|
+
scan(step.unit.instructions, step.unit.source.start, `Step "${step.id}" instructions`);
|
|
327
|
+
if (step.map) {
|
|
328
|
+
scan(step.map.over, step.source.start, `Step "${step.id}" map.over`);
|
|
329
|
+
scan(step.map.unit.instructions, step.map.unit.source.start, `Step "${step.id}" instructions`);
|
|
330
|
+
}
|
|
331
|
+
if (step.route)
|
|
332
|
+
scan(step.route.input, step.source.start, `Step "${step.id}" route.input`);
|
|
333
|
+
}
|
|
334
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
335
|
+
// Frontend B — classic markdown workflow
|
|
336
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
337
|
+
/**
|
|
338
|
+
* Compile a markdown `WorkflowDocument` to an unresolved structural plan. Pure
|
|
339
|
+
* and deterministic: the same document always compiles to the same plan.
|
|
340
|
+
* Linear workflows produce one fail-fast unit per step guarded by its gate.
|
|
341
|
+
*/
|
|
342
|
+
export function compileWorkflowPlan(document) {
|
|
343
|
+
const params = document.parameters?.map((p) => p.name);
|
|
344
|
+
return {
|
|
345
|
+
title: document.title,
|
|
346
|
+
...(params && params.length > 0 ? { params } : {}),
|
|
347
|
+
steps: document.steps.map(compileMarkdownStep),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function compileMarkdownStep(step) {
|
|
351
|
+
const gate = {
|
|
352
|
+
kind: "gate",
|
|
353
|
+
id: `${step.id}.gate`,
|
|
354
|
+
stepId: step.id,
|
|
355
|
+
criteria: step.completionCriteria?.map((c) => c.text) ?? [],
|
|
356
|
+
};
|
|
357
|
+
return {
|
|
358
|
+
stepId: step.id,
|
|
359
|
+
title: step.title,
|
|
360
|
+
sequenceIndex: step.sequenceIndex,
|
|
361
|
+
root: {
|
|
362
|
+
kind: "unit",
|
|
363
|
+
id: step.id,
|
|
364
|
+
instructions: step.instructions.text,
|
|
365
|
+
// Stable contract: markdown instructions are opaque data, passed to the
|
|
366
|
+
// agent byte-exact. A literal `${{ … }}` (GitHub Actions syntax, docs of
|
|
367
|
+
// the YAML format) is content here, never expression grammar.
|
|
368
|
+
templating: "verbatim",
|
|
369
|
+
// Markdown has no failure-policy surface; the fail-fast default applies.
|
|
370
|
+
onError: "fail",
|
|
371
|
+
source: step.instructions.source,
|
|
372
|
+
},
|
|
373
|
+
gate,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
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 path from "node:path";
|
|
5
|
+
import { deepMergeConfig } from "../../core/config/deep-merge.js";
|
|
6
|
+
import { ConfigError, UsageError } from "../../core/errors.js";
|
|
7
|
+
import { DEFAULT_AGENT_TIMEOUT_MS, DEFAULT_LLM_TIMEOUT_MS } from "../../integrations/agent/config.js";
|
|
8
|
+
import { resolveLlmEngineUse, } from "../../integrations/agent/engine-resolution.js";
|
|
9
|
+
import { resolveLlmModel, resolveModel } from "../../integrations/agent/model-aliases.js";
|
|
10
|
+
import { getBuiltinAgentProfile } from "../../integrations/agent/profiles.js";
|
|
11
|
+
import { HARNESS_BY_ID } from "../../integrations/harnesses/index.js";
|
|
12
|
+
import { workflowMaxConcurrency } from "../concurrency-policy.js";
|
|
13
|
+
import { compileWorkflowPlan, compileWorkflowProgram, } from "./compile.js";
|
|
14
|
+
import { decodeWorkflowPlanV3, WORKFLOW_IR_VERSION } from "./schema.js";
|
|
15
|
+
/**
|
|
16
|
+
* The only source-to-runtime boundary. Source compilation remains pure; engine
|
|
17
|
+
* selection and every dispatch-significant setting are resolved here once.
|
|
18
|
+
*/
|
|
19
|
+
export function compileResolveFreezeWorkflow(asset, config) {
|
|
20
|
+
const preliminary = asset.program ? compileProgram(asset) : compileMarkdown(asset);
|
|
21
|
+
const engines = {};
|
|
22
|
+
const maxConcurrency = frozenConcurrency(config);
|
|
23
|
+
const programDefaults = asset.program?.defaults;
|
|
24
|
+
const freezeInvocation = (unit) => {
|
|
25
|
+
const layers = [...(programDefaults ? [programDefaults] : []), ...(unit ? [unit] : [])];
|
|
26
|
+
const name = selectedEngine(config, layers);
|
|
27
|
+
if (!name)
|
|
28
|
+
throw new ConfigError("No workflow engine is selected. Set defaults.engine or workflow defaults.engine.", "INVALID_CONFIG_FILE");
|
|
29
|
+
const engine = engineDefinition(config, name);
|
|
30
|
+
addSnapshot(config, name, engines);
|
|
31
|
+
const model = exactModel(config, name, engine, layers);
|
|
32
|
+
const timeoutMs = effectiveTimeout(config, engine, layers);
|
|
33
|
+
const llm = engine.kind === "llm" ? mergedLlmOverrides(layers) : undefined;
|
|
34
|
+
if (engine.kind !== "llm" && llm !== undefined) {
|
|
35
|
+
throw new ConfigError(`Workflow engine "${name}" is an agent engine and cannot receive llm overrides.`, "INVALID_CONFIG_FILE");
|
|
36
|
+
}
|
|
37
|
+
return { engine: name, model, timeoutMs, ...(llm ? { llm } : {}) };
|
|
38
|
+
};
|
|
39
|
+
const freezeUnit = (node, unit) => ({
|
|
40
|
+
kind: "unit",
|
|
41
|
+
id: node.id,
|
|
42
|
+
instructions: node.instructions,
|
|
43
|
+
templating: node.templating ?? "verbatim",
|
|
44
|
+
invocation: freezeInvocation(unit),
|
|
45
|
+
...(node.schema ? { schema: node.schema } : {}),
|
|
46
|
+
...(node.retry ? { retry: node.retry } : {}),
|
|
47
|
+
onError: node.onError,
|
|
48
|
+
...(node.env ? { env: node.env } : {}),
|
|
49
|
+
isolation: node.isolation ?? "none",
|
|
50
|
+
...(node.source ? { source: node.source } : {}),
|
|
51
|
+
});
|
|
52
|
+
const steps = preliminary.steps.map((step, index) => {
|
|
53
|
+
const sourceStep = asset.program?.steps[index];
|
|
54
|
+
const sourceUnit = sourceStep?.unit ?? sourceStep?.map?.unit;
|
|
55
|
+
const root = step.root
|
|
56
|
+
? step.root.kind === "map"
|
|
57
|
+
? {
|
|
58
|
+
kind: "map",
|
|
59
|
+
id: step.root.id,
|
|
60
|
+
over: step.root.over,
|
|
61
|
+
template: freezeUnit(step.root.template, sourceUnit),
|
|
62
|
+
concurrency: step.root.concurrency ?? 1,
|
|
63
|
+
reducer: step.root.reducer,
|
|
64
|
+
...(step.root.source ? { source: step.root.source } : {}),
|
|
65
|
+
}
|
|
66
|
+
: freezeUnit(step.root, sourceUnit)
|
|
67
|
+
: undefined;
|
|
68
|
+
const criteria = step.gate.criteria;
|
|
69
|
+
const judge = criteria.length === 0 ? null : freezeGateJudge(config, engines, step.gate.required === true);
|
|
70
|
+
const gate = {
|
|
71
|
+
kind: "gate",
|
|
72
|
+
id: `${step.stepId}.gate`,
|
|
73
|
+
stepId: step.stepId,
|
|
74
|
+
criteria,
|
|
75
|
+
maxLoops: step.gate.maxLoops ?? 1,
|
|
76
|
+
required: step.gate.required === true,
|
|
77
|
+
judge,
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
stepId: step.stepId,
|
|
81
|
+
title: step.title,
|
|
82
|
+
sequenceIndex: step.sequenceIndex,
|
|
83
|
+
...(root ? { root } : {}),
|
|
84
|
+
...(step.route ? { route: step.route } : {}),
|
|
85
|
+
...(step.outputSchema ? { outputSchema: step.outputSchema } : {}),
|
|
86
|
+
gate,
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
const plan = decodeWorkflowPlanV3({
|
|
90
|
+
irVersion: WORKFLOW_IR_VERSION,
|
|
91
|
+
title: preliminary.title,
|
|
92
|
+
...(preliminary.params ? { params: preliminary.params } : {}),
|
|
93
|
+
...(preliminary.paramSchemas ? { paramSchemas: preliminary.paramSchemas } : {}),
|
|
94
|
+
...(preliminary.budget ? { budget: preliminary.budget } : {}),
|
|
95
|
+
execution: { maxConcurrency, engines },
|
|
96
|
+
steps,
|
|
97
|
+
});
|
|
98
|
+
return {
|
|
99
|
+
warnings: asset.program ? preliminary.warnings : [],
|
|
100
|
+
plan,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function compileProgram(asset) {
|
|
104
|
+
if (!asset.program)
|
|
105
|
+
throw new UsageError(`Workflow asset ${asset.ref} has no YAML program.`);
|
|
106
|
+
const compiled = compileWorkflowProgram(asset.program);
|
|
107
|
+
if (!compiled.ok)
|
|
108
|
+
throw new UsageError(compiled.errors.map((error) => `${asset.path}:${error.line}: ${error.message}`).join("\n"));
|
|
109
|
+
return { ...compiled.plan, warnings: compiled.warnings };
|
|
110
|
+
}
|
|
111
|
+
function compileMarkdown(asset) {
|
|
112
|
+
if (!asset.document)
|
|
113
|
+
throw new UsageError(`Workflow asset ${asset.ref} has no source document.`);
|
|
114
|
+
return { ...compileWorkflowPlan(asset.document), warnings: [] };
|
|
115
|
+
}
|
|
116
|
+
function selectedEngine(config, layers) {
|
|
117
|
+
for (let index = layers.length - 1; index >= 0; index--)
|
|
118
|
+
if (layers[index]?.engine !== undefined)
|
|
119
|
+
return layers[index]?.engine;
|
|
120
|
+
return config.defaults?.engine;
|
|
121
|
+
}
|
|
122
|
+
function engineDefinition(config, name) {
|
|
123
|
+
const engine = config.engines?.[name];
|
|
124
|
+
if (!engine)
|
|
125
|
+
throw new ConfigError(`Engine "${name}" is not configured.`, "INVALID_CONFIG_FILE");
|
|
126
|
+
return engine;
|
|
127
|
+
}
|
|
128
|
+
function exactModel(config, name, engine, layers) {
|
|
129
|
+
let selected;
|
|
130
|
+
for (const layer of layers)
|
|
131
|
+
if (layer.model !== undefined)
|
|
132
|
+
selected = layer.model;
|
|
133
|
+
selected ??= engine.model;
|
|
134
|
+
if (!selected) {
|
|
135
|
+
if (engine.kind === "llm")
|
|
136
|
+
throw new ConfigError(`LLM engine "${name}" has no model.`, "INVALID_CONFIG_FILE");
|
|
137
|
+
if (engine.platform === "opencode-sdk") {
|
|
138
|
+
const fallbackName = engine.llmEngine ?? config.defaults?.llmEngine;
|
|
139
|
+
if (fallbackName) {
|
|
140
|
+
const fallback = engineDefinition(config, fallbackName);
|
|
141
|
+
if (fallback.kind !== "llm") {
|
|
142
|
+
throw new ConfigError(`SDK engine "${name}" fallback "${fallbackName}" is not an LLM engine.`, "INVALID_CONFIG_FILE");
|
|
143
|
+
}
|
|
144
|
+
return exactModel(config, fallbackName, fallback, []);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
if (engine.kind === "llm")
|
|
150
|
+
return resolveLlmModel(selected, name, config.modelAliases);
|
|
151
|
+
return resolveModel(selected, engine.platform, engine.modelAliases, config.modelAliases);
|
|
152
|
+
}
|
|
153
|
+
function effectiveTimeout(config, engine, layers) {
|
|
154
|
+
for (let index = layers.length - 1; index >= 0; index--) {
|
|
155
|
+
if (Object.hasOwn(layers[index] ?? {}, "timeoutMs"))
|
|
156
|
+
return layers[index]?.timeoutMs ?? null;
|
|
157
|
+
}
|
|
158
|
+
if (Object.hasOwn(engine, "timeoutMs"))
|
|
159
|
+
return engine.timeoutMs ?? null;
|
|
160
|
+
if (engine.kind === "llm")
|
|
161
|
+
return DEFAULT_LLM_TIMEOUT_MS;
|
|
162
|
+
if (engine.platform === "opencode-sdk") {
|
|
163
|
+
const fallbackName = engine.llmEngine ?? config.defaults?.llmEngine;
|
|
164
|
+
if (fallbackName) {
|
|
165
|
+
const fallback = engineDefinition(config, fallbackName);
|
|
166
|
+
if (fallback.kind === "llm") {
|
|
167
|
+
return Object.hasOwn(fallback, "timeoutMs") ? (fallback.timeoutMs ?? null) : DEFAULT_LLM_TIMEOUT_MS;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return DEFAULT_AGENT_TIMEOUT_MS;
|
|
172
|
+
}
|
|
173
|
+
function mergedLlmOverrides(layers) {
|
|
174
|
+
let merged;
|
|
175
|
+
for (const layer of layers)
|
|
176
|
+
if (layer.llm)
|
|
177
|
+
merged = deepMergeConfig(merged ?? {}, layer.llm);
|
|
178
|
+
return merged;
|
|
179
|
+
}
|
|
180
|
+
function addSnapshot(config, name, target) {
|
|
181
|
+
if (target[name])
|
|
182
|
+
return;
|
|
183
|
+
const engine = engineDefinition(config, name);
|
|
184
|
+
if (engine.kind === "llm") {
|
|
185
|
+
const resolved = resolveLlmEngineUse(config, [{ engine: name }]);
|
|
186
|
+
const snapshot = {
|
|
187
|
+
name,
|
|
188
|
+
kind: "llm",
|
|
189
|
+
endpoint: engine.endpoint,
|
|
190
|
+
model: exactModel(config, name, engine, []),
|
|
191
|
+
concurrency: engine.concurrency ?? 1,
|
|
192
|
+
...(engine.provider ? { provider: engine.provider } : {}),
|
|
193
|
+
...(resolved.credential ? { credential: resolved.credential } : {}),
|
|
194
|
+
...(engine.temperature !== undefined ? { temperature: engine.temperature } : {}),
|
|
195
|
+
...(engine.maxTokens !== undefined ? { maxTokens: engine.maxTokens } : {}),
|
|
196
|
+
...(engine.supportsJsonSchema !== undefined ? { supportsJsonSchema: engine.supportsJsonSchema } : {}),
|
|
197
|
+
...(engine.extraParams ? { extraParams: engine.extraParams } : {}),
|
|
198
|
+
...(engine.contextLength !== undefined ? { contextLength: engine.contextLength } : {}),
|
|
199
|
+
...(engine.enableThinking !== undefined ? { enableThinking: engine.enableThinking } : {}),
|
|
200
|
+
};
|
|
201
|
+
target[name] = snapshot;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const harness = HARNESS_BY_ID.get(engine.platform);
|
|
205
|
+
if (!harness?.capabilities.agentDispatch)
|
|
206
|
+
throw new ConfigError(`Engine "${name}" cannot dispatch platform ${engine.platform}.`, "INVALID_CONFIG_FILE");
|
|
207
|
+
const sdk = engine.platform === "opencode-sdk";
|
|
208
|
+
const builtin = getBuiltinAgentProfile(engine.platform);
|
|
209
|
+
const fallback = sdk ? (engine.llmEngine ?? config.defaults?.llmEngine ?? null) : null;
|
|
210
|
+
if (fallback)
|
|
211
|
+
addSnapshot(config, fallback, target);
|
|
212
|
+
const snapshot = {
|
|
213
|
+
name,
|
|
214
|
+
kind: "agent",
|
|
215
|
+
runnerKind: sdk ? "sdk" : "agent",
|
|
216
|
+
platform: engine.platform,
|
|
217
|
+
bin: engine.bin ?? builtin?.bin ?? (sdk ? "opencode" : engine.platform),
|
|
218
|
+
args: [...(engine.args ?? builtin?.args ?? [])],
|
|
219
|
+
workspace: engine.workspace ? path.resolve(engine.workspace) : null,
|
|
220
|
+
envPassthrough: [...(builtin?.envPassthrough ?? [])],
|
|
221
|
+
commandBuilder: engine.platform,
|
|
222
|
+
fallbackLlmEngine: fallback,
|
|
223
|
+
};
|
|
224
|
+
target[name] = snapshot;
|
|
225
|
+
}
|
|
226
|
+
function freezeGateJudge(config, engines, required) {
|
|
227
|
+
const resolved = resolveLlmEngineUse(config, [], { optional: true });
|
|
228
|
+
if (!resolved) {
|
|
229
|
+
if (required)
|
|
230
|
+
throw new ConfigError("A required workflow gate needs defaults.llmEngine at start.", "LLM_NOT_CONFIGURED");
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
addSnapshot(config, resolved.engine, engines);
|
|
234
|
+
return {
|
|
235
|
+
engine: resolved.engine,
|
|
236
|
+
model: exactModel(config, resolved.engine, engineDefinition(config, resolved.engine), []),
|
|
237
|
+
timeoutMs: resolved.timeoutMs,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function frozenConcurrency(config) {
|
|
241
|
+
const configured = config.workflow?.maxConcurrency;
|
|
242
|
+
return workflowMaxConcurrency(typeof configured === "number" && Number.isFinite(configured) ? configured : undefined);
|
|
243
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
/**
|
|
5
|
+
* Run-parameter validation against the frozen plan's param schemas (reviewer
|
|
6
|
+
* #12). A program can declare `params.files: { type: array }`; supplying
|
|
7
|
+
* `--params '{"files":"not-an-array"}'` must be rejected at start rather than
|
|
8
|
+
* silently flowing into a unit prompt. The schemas are frozen into the plan
|
|
9
|
+
* ({@link WorkflowPlanGraph.paramSchemas}, program path only) so validation is
|
|
10
|
+
* a pure function of the frozen plan and the supplied params — no live-asset
|
|
11
|
+
* re-read.
|
|
12
|
+
*
|
|
13
|
+
* Uses the same bounded {@link validateJsonSchemaSubset} the engine applies to
|
|
14
|
+
* unit output: undeclared params are permitted (the schema map only constrains
|
|
15
|
+
* the params it names), and a workflow with no declared schemas (every Markdown
|
|
16
|
+
* workflow, and programs without a `params:` block) validates trivially.
|
|
17
|
+
*
|
|
18
|
+
* Pure module: no IO, no engine imports.
|
|
19
|
+
*/
|
|
20
|
+
import { UsageError } from "../../core/errors.js";
|
|
21
|
+
import { validateJsonSchemaSubset } from "../../core/json-schema.js";
|
|
22
|
+
/**
|
|
23
|
+
* Validate a run's supplied params against the plan's frozen param schemas.
|
|
24
|
+
* Returns a flat list of human-readable, path-prefixed error strings (empty =
|
|
25
|
+
* valid). Params the plan does not declare a schema for are not constrained.
|
|
26
|
+
*/
|
|
27
|
+
export function validateWorkflowParams(plan, params) {
|
|
28
|
+
const schemas = plan.paramSchemas;
|
|
29
|
+
if (!schemas || Object.keys(schemas).length === 0)
|
|
30
|
+
return [];
|
|
31
|
+
// Validate the params object as a whole against a synthetic object schema
|
|
32
|
+
// whose `properties` are the declared param schemas. Missing declared params
|
|
33
|
+
// are NOT required (params may be optional / defaulted downstream); only a
|
|
34
|
+
// PRESENT param that violates its declared schema is an error.
|
|
35
|
+
// Re-root the validator's `$` JSON-pointer prefix to `params` for messages
|
|
36
|
+
// that read naturally in a start/CLI error (e.g. `params.files: expected …`).
|
|
37
|
+
return validateJsonSchemaSubset(params, { type: "object", properties: schemas }).map((e) => e.replace(/^\$/, "params"));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Brief/report integrity assert (reviewer #12): the journaled `params_json`
|
|
41
|
+
* row must STILL satisfy the frozen param schemas. `startWorkflowRun` already
|
|
42
|
+
* validated the params it stored, so a violation here means the row was edited
|
|
43
|
+
* after the run started — loud corruption, exactly like the frozen-plan hash
|
|
44
|
+
* mismatch and the tampered-params replay-divergence path. Refuse to describe
|
|
45
|
+
* or drive the run rather than resolve prompts from schema-violating params.
|
|
46
|
+
*/
|
|
47
|
+
export function assertRunParamsSatisfyPlan(runId, plan, params) {
|
|
48
|
+
const errors = validateWorkflowParams(plan, params);
|
|
49
|
+
if (errors.length === 0)
|
|
50
|
+
return;
|
|
51
|
+
throw new UsageError(`Workflow run ${runId} failed the frozen param-schema integrity check: the journaled params row no longer ` +
|
|
52
|
+
`satisfies the workflow's declared parameter schemas (edited after the run started). Refusing to execute it. ` +
|
|
53
|
+
`Start a new run.\n${errors.map((e) => ` - ${e}`).join("\n")}`);
|
|
54
|
+
}
|