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
|
@@ -1,4 +1,545 @@
|
|
|
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
|
-
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { UsageError } from "../../core/errors.js";
|
|
6
|
+
import { formatExtraParamsIssue, validateExtraParams } from "../../core/extra-params.js";
|
|
7
|
+
import { HARNESS_BY_ID } from "../../integrations/harnesses/index.js";
|
|
8
|
+
import { listReferences, parseTemplate } from "../program/expressions.js";
|
|
9
|
+
import { PROGRAM_PARAM_NAME_PATTERN, PROGRAM_RETRY_REASONS, PROGRAM_STEP_ID_PATTERN } from "../program/schema.js";
|
|
10
|
+
import { jsonBytes, WORKFLOW_MAX_ENGINES, WORKFLOW_MAX_EXTRA_PARAMS_BYTES, WORKFLOW_MAX_INSTRUCTION_BYTES, WORKFLOW_MAX_JSON_DEPTH, WORKFLOW_MAX_MAP_EXPANSION, WORKFLOW_MAX_PARAMS, WORKFLOW_MAX_PLAN_BYTES, WORKFLOW_MAX_ROUTE_BRANCHES, WORKFLOW_MAX_SCHEMA_BYTES, WORKFLOW_MAX_STEPS, } from "../resource-limits.js";
|
|
11
|
+
/** The only executable persisted workflow plan format. */
|
|
12
|
+
export const WORKFLOW_IR_VERSION = 3;
|
|
13
|
+
export const WORKFLOW_MAX_CONCURRENCY = 64;
|
|
14
|
+
export const WORKFLOW_MAX_GATE_LOOPS = 100;
|
|
15
|
+
export const WORKFLOW_MAX_RETRIES = 100;
|
|
16
|
+
export const WORKFLOW_MAX_UNITS = WORKFLOW_MAX_MAP_EXPANSION;
|
|
17
|
+
export const WORKFLOW_MAX_TIMEOUT_MS = 2 ** 31 - 1;
|
|
18
|
+
const MAX_LIST_ITEMS = 1024;
|
|
19
|
+
const MAX_STRING_LENGTH = 1_000_000;
|
|
20
|
+
const ENGINE_NAME_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
21
|
+
/** Strictly decode persisted v3 data before it can drive a workflow. */
|
|
22
|
+
export function decodeWorkflowPlanV3(input, hooks = {}) {
|
|
23
|
+
if (!isRecord(input) || input.irVersion !== WORKFLOW_IR_VERSION)
|
|
24
|
+
fail("irVersion must be 3");
|
|
25
|
+
assertJson(input);
|
|
26
|
+
if (jsonBytes(input) > WORKFLOW_MAX_PLAN_BYTES)
|
|
27
|
+
fail("plan exceeds the 2 MiB resource limit");
|
|
28
|
+
const plan = input;
|
|
29
|
+
assertKeys(input, ["irVersion", "title", "params", "paramSchemas", "budget", "execution", "steps"], "plan");
|
|
30
|
+
assertString(plan.title, "title");
|
|
31
|
+
validateParams(plan.params, plan.paramSchemas);
|
|
32
|
+
validateBudget(plan.budget);
|
|
33
|
+
if (!isRecord(plan.execution) ||
|
|
34
|
+
!Number.isInteger(plan.execution.maxConcurrency) ||
|
|
35
|
+
plan.execution.maxConcurrency < 1 ||
|
|
36
|
+
plan.execution.maxConcurrency > WORKFLOW_MAX_CONCURRENCY) {
|
|
37
|
+
fail("execution.maxConcurrency must be an integer from 1 through 64");
|
|
38
|
+
}
|
|
39
|
+
assertKeys(plan.execution, ["maxConcurrency", "engines"], "execution");
|
|
40
|
+
if (!isRecord(plan.execution.engines))
|
|
41
|
+
fail("execution.engines must be an object");
|
|
42
|
+
const engines = plan.execution.engines;
|
|
43
|
+
if (Object.keys(engines).length > WORKFLOW_MAX_ENGINES)
|
|
44
|
+
fail(`execution.engines exceeds ${WORKFLOW_MAX_ENGINES} entries`);
|
|
45
|
+
const references = new Set();
|
|
46
|
+
for (const [key, engine] of Object.entries(engines))
|
|
47
|
+
validateEngine(key, engine, references, hooks);
|
|
48
|
+
if (!Array.isArray(plan.steps) || plan.steps.length === 0 || plan.steps.length > WORKFLOW_MAX_STEPS)
|
|
49
|
+
fail("steps must contain 1 through 256 entries");
|
|
50
|
+
const stepIds = new Set();
|
|
51
|
+
const nodeIds = new Set();
|
|
52
|
+
for (let index = 0; index < plan.steps.length; index++) {
|
|
53
|
+
const step = plan.steps[index];
|
|
54
|
+
if (!isRecord(step) ||
|
|
55
|
+
typeof step.stepId !== "string" ||
|
|
56
|
+
!PROGRAM_STEP_ID_PATTERN.test(step.stepId) ||
|
|
57
|
+
stepIds.has(step.stepId))
|
|
58
|
+
fail("step ids must be unique non-empty strings");
|
|
59
|
+
stepIds.add(step.stepId);
|
|
60
|
+
if (step.sequenceIndex !== index)
|
|
61
|
+
fail("step sequence indices must be contiguous and unique");
|
|
62
|
+
assertString(step.title, `step ${step.stepId} title`);
|
|
63
|
+
if (!!step.root === !!step.route)
|
|
64
|
+
fail(`step ${step.stepId} must contain exactly one of root or route`);
|
|
65
|
+
assertKeys(step, ["stepId", "title", "sequenceIndex", "dependsOn", "root", "route", "outputSchema", "gate"], `step ${step.stepId}`);
|
|
66
|
+
validateStringArray(step.dependsOn, `step ${step.stepId} dependsOn`, WORKFLOW_MAX_STEPS, true);
|
|
67
|
+
if (step.outputSchema !== undefined)
|
|
68
|
+
validateSchema(step.outputSchema, `step ${step.stepId} outputSchema`);
|
|
69
|
+
if (step.root)
|
|
70
|
+
validateNode(step.root, step.stepId, references, nodeIds, hooks);
|
|
71
|
+
if (step.route)
|
|
72
|
+
validateRoute(step.route, step.stepId);
|
|
73
|
+
validateGate(step.gate, step.stepId, references, nodeIds, hooks);
|
|
74
|
+
}
|
|
75
|
+
const stepIndex = new Map(plan.steps.map((step, index) => [step.stepId, index]));
|
|
76
|
+
for (const [index, step] of plan.steps.entries()) {
|
|
77
|
+
for (const target of step.route
|
|
78
|
+
? [...Object.values(step.route.when), ...(step.route.defaultStepId ? [step.route.defaultStepId] : [])]
|
|
79
|
+
: []) {
|
|
80
|
+
const targetIndex = stepIndex.get(target);
|
|
81
|
+
if (targetIndex === undefined)
|
|
82
|
+
fail(`route target ${target} does not name a step`);
|
|
83
|
+
if (targetIndex <= index)
|
|
84
|
+
fail(`route target ${target} must come after step ${step.stepId}`);
|
|
85
|
+
}
|
|
86
|
+
const dependencies = new Set();
|
|
87
|
+
for (const dependency of step.dependsOn ?? []) {
|
|
88
|
+
const dependencyIndex = stepIndex.get(dependency);
|
|
89
|
+
if (dependencyIndex === undefined || dependencyIndex >= index || dependencies.has(dependency))
|
|
90
|
+
fail(`step ${step.stepId} has an invalid dependency`);
|
|
91
|
+
dependencies.add(dependency);
|
|
92
|
+
}
|
|
93
|
+
validateStepExpressions(step, index, stepIndex);
|
|
94
|
+
}
|
|
95
|
+
for (const name of references) {
|
|
96
|
+
if (!engines[name])
|
|
97
|
+
fail(`engine reference ${name} is not in execution.engines`);
|
|
98
|
+
}
|
|
99
|
+
for (const name of Object.keys(engines))
|
|
100
|
+
if (!references.has(name))
|
|
101
|
+
fail(`engine ${name} is not referenced`);
|
|
102
|
+
for (const engine of Object.values(engines)) {
|
|
103
|
+
if (engine.kind !== "agent")
|
|
104
|
+
continue;
|
|
105
|
+
if (engine.fallbackLlmEngine !== null && engines[engine.fallbackLlmEngine]?.kind !== "llm")
|
|
106
|
+
fail(`SDK engine ${engine.name} fallback must name an LLM engine`);
|
|
107
|
+
}
|
|
108
|
+
for (const step of plan.steps) {
|
|
109
|
+
if (step.root)
|
|
110
|
+
assertUnitEngineCompatibility(step.root, engines);
|
|
111
|
+
if (step.gate.judge) {
|
|
112
|
+
const judge = engines[step.gate.judge.engine];
|
|
113
|
+
if (!judge || judge.kind !== "llm")
|
|
114
|
+
fail(`gate ${step.gate.id} must reference an LLM engine`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return plan;
|
|
118
|
+
}
|
|
119
|
+
function validateEngine(key, engine, references, hooks) {
|
|
120
|
+
if (!ENGINE_NAME_PATTERN.test(key) ||
|
|
121
|
+
key.length > 63 ||
|
|
122
|
+
!isRecord(engine) ||
|
|
123
|
+
engine.name !== key ||
|
|
124
|
+
(engine.kind !== "llm" && engine.kind !== "agent"))
|
|
125
|
+
fail("catalog keys must equal a valid snapshot name");
|
|
126
|
+
if (engine.kind === "llm") {
|
|
127
|
+
assertKeys(engine, [
|
|
128
|
+
"name",
|
|
129
|
+
"kind",
|
|
130
|
+
"provider",
|
|
131
|
+
"endpoint",
|
|
132
|
+
"model",
|
|
133
|
+
"credential",
|
|
134
|
+
"temperature",
|
|
135
|
+
"maxTokens",
|
|
136
|
+
"concurrency",
|
|
137
|
+
"supportsJsonSchema",
|
|
138
|
+
"extraParams",
|
|
139
|
+
"contextLength",
|
|
140
|
+
"enableThinking",
|
|
141
|
+
], `LLM engine ${key}`);
|
|
142
|
+
if (typeof engine.endpoint !== "string" ||
|
|
143
|
+
!engine.endpoint ||
|
|
144
|
+
typeof engine.model !== "string" ||
|
|
145
|
+
!engine.model ||
|
|
146
|
+
!Number.isInteger(engine.concurrency) ||
|
|
147
|
+
engine.concurrency < 1 ||
|
|
148
|
+
engine.concurrency > WORKFLOW_MAX_CONCURRENCY)
|
|
149
|
+
fail(`LLM engine ${key} is invalid`);
|
|
150
|
+
if (engine.provider !== undefined)
|
|
151
|
+
assertString(engine.provider, `LLM engine ${key} provider`);
|
|
152
|
+
validateEndpoint(engine.endpoint, key);
|
|
153
|
+
validateOptionalFiniteNumber(engine.temperature, `LLM engine ${key} temperature`);
|
|
154
|
+
validateOptionalPositiveInteger(engine.maxTokens, `LLM engine ${key} maxTokens`);
|
|
155
|
+
validateOptionalPositiveInteger(engine.contextLength, `LLM engine ${key} contextLength`);
|
|
156
|
+
if (engine.supportsJsonSchema !== undefined && typeof engine.supportsJsonSchema !== "boolean")
|
|
157
|
+
fail(`LLM engine ${key} supportsJsonSchema must be boolean`);
|
|
158
|
+
if (engine.enableThinking !== undefined && typeof engine.enableThinking !== "boolean")
|
|
159
|
+
fail(`LLM engine ${key} enableThinking must be boolean`);
|
|
160
|
+
if (engine.extraParams !== undefined) {
|
|
161
|
+
const issue = validateExtraParams(engine.extraParams)[0];
|
|
162
|
+
if (issue)
|
|
163
|
+
fail(formatExtraParamsIssue(`LLM engine ${key} extraParams`, issue));
|
|
164
|
+
}
|
|
165
|
+
validateExtraParamsValue(engine.extraParams, `LLM engine ${key} extraParams`, hooks);
|
|
166
|
+
if (engine.credential !== undefined) {
|
|
167
|
+
if (!isRecord(engine.credential) ||
|
|
168
|
+
!Array.isArray(engine.credential.names) ||
|
|
169
|
+
engine.credential.names.length === 0 ||
|
|
170
|
+
engine.credential.names.length > 32 ||
|
|
171
|
+
!engine.credential.names.every((n) => typeof n === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(n)) ||
|
|
172
|
+
new Set(engine.credential.names).size !== engine.credential.names.length ||
|
|
173
|
+
typeof engine.credential.required !== "boolean") {
|
|
174
|
+
fail(`LLM engine ${key} has an invalid credential descriptor`);
|
|
175
|
+
}
|
|
176
|
+
assertKeys(engine.credential, ["names", "required"], `LLM engine ${key} credential`);
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
assertKeys(engine, [
|
|
181
|
+
"name",
|
|
182
|
+
"kind",
|
|
183
|
+
"runnerKind",
|
|
184
|
+
"platform",
|
|
185
|
+
"bin",
|
|
186
|
+
"args",
|
|
187
|
+
"workspace",
|
|
188
|
+
"envPassthrough",
|
|
189
|
+
"commandBuilder",
|
|
190
|
+
"fallbackLlmEngine",
|
|
191
|
+
], `agent engine ${key}`);
|
|
192
|
+
if ((engine.runnerKind !== "agent" && engine.runnerKind !== "sdk") ||
|
|
193
|
+
typeof engine.platform !== "string" ||
|
|
194
|
+
!engine.platform ||
|
|
195
|
+
typeof engine.bin !== "string" ||
|
|
196
|
+
!engine.bin ||
|
|
197
|
+
!Array.isArray(engine.args) ||
|
|
198
|
+
engine.args.length > MAX_LIST_ITEMS ||
|
|
199
|
+
!engine.args.every((arg) => typeof arg === "string" && arg.length <= MAX_STRING_LENGTH) ||
|
|
200
|
+
!(typeof engine.workspace === "string" || engine.workspace === null) ||
|
|
201
|
+
!Array.isArray(engine.envPassthrough) ||
|
|
202
|
+
engine.envPassthrough.length > MAX_LIST_ITEMS ||
|
|
203
|
+
!engine.envPassthrough.every((name) => typeof name === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) ||
|
|
204
|
+
new Set(engine.envPassthrough).size !== engine.envPassthrough.length ||
|
|
205
|
+
typeof engine.commandBuilder !== "string" ||
|
|
206
|
+
engine.commandBuilder !== engine.platform ||
|
|
207
|
+
(engine.workspace !== null && !path.isAbsolute(engine.workspace)) ||
|
|
208
|
+
!HARNESS_BY_ID.get(engine.platform)?.capabilities.agentDispatch)
|
|
209
|
+
fail(`agent engine ${key} is invalid`);
|
|
210
|
+
if ((engine.platform === "opencode-sdk") !== (engine.runnerKind === "sdk"))
|
|
211
|
+
fail(`agent engine ${key} has incompatible platform and runnerKind`);
|
|
212
|
+
if (engine.runnerKind === "agent" && engine.fallbackLlmEngine !== null)
|
|
213
|
+
fail(`agent engine ${key} cannot have an SDK fallback`);
|
|
214
|
+
if (engine.fallbackLlmEngine !== null) {
|
|
215
|
+
if (typeof engine.fallbackLlmEngine !== "string" || !engine.fallbackLlmEngine)
|
|
216
|
+
fail(`agent engine ${key} has an invalid fallback`);
|
|
217
|
+
references.add(engine.fallbackLlmEngine);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function validateNode(node, stepId, references, nodeIds, hooks) {
|
|
221
|
+
if (!isRecord(node) ||
|
|
222
|
+
(node.kind !== "unit" && node.kind !== "map") ||
|
|
223
|
+
typeof node.id !== "string" ||
|
|
224
|
+
!node.id ||
|
|
225
|
+
nodeIds.has(node.id))
|
|
226
|
+
fail("node ids must be unique non-empty strings");
|
|
227
|
+
nodeIds.add(node.id);
|
|
228
|
+
if (node.kind === "map") {
|
|
229
|
+
assertKeys(node, ["kind", "id", "over", "template", "concurrency", "reducer", "source"], `map ${node.id}`);
|
|
230
|
+
if (typeof node.over !== "string" ||
|
|
231
|
+
!node.over ||
|
|
232
|
+
!Number.isInteger(node.concurrency) ||
|
|
233
|
+
node.concurrency < 1 ||
|
|
234
|
+
node.concurrency > WORKFLOW_MAX_CONCURRENCY ||
|
|
235
|
+
(node.reducer !== "collect" && node.reducer !== "vote"))
|
|
236
|
+
fail(`map ${node.id} is invalid`);
|
|
237
|
+
if (node.id !== `${stepId}.map`)
|
|
238
|
+
fail(`map ${node.id} does not match step ${stepId}`);
|
|
239
|
+
validateSource(node.source, `map ${node.id} source`);
|
|
240
|
+
validateNode(node.template, stepId, references, nodeIds, hooks);
|
|
241
|
+
if (!isRecord(node.template) || node.template.kind !== "unit")
|
|
242
|
+
fail(`map ${node.id} template must be a unit`);
|
|
243
|
+
if (node.template.id !== `${stepId}.unit`)
|
|
244
|
+
fail(`map ${node.id} template id is invalid`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
assertKeys(node, [
|
|
248
|
+
"kind",
|
|
249
|
+
"id",
|
|
250
|
+
"instructions",
|
|
251
|
+
"templating",
|
|
252
|
+
"invocation",
|
|
253
|
+
"schema",
|
|
254
|
+
"retry",
|
|
255
|
+
"onError",
|
|
256
|
+
"env",
|
|
257
|
+
"isolation",
|
|
258
|
+
"source",
|
|
259
|
+
], `unit ${node.id}`);
|
|
260
|
+
if (typeof node.instructions !== "string" ||
|
|
261
|
+
!node.instructions ||
|
|
262
|
+
(node.templating !== "expressions" && node.templating !== "verbatim") ||
|
|
263
|
+
(node.onError !== "fail" && node.onError !== "continue") ||
|
|
264
|
+
(node.isolation !== "none" && node.isolation !== "worktree"))
|
|
265
|
+
fail(`unit ${node.id} is invalid`);
|
|
266
|
+
if (node.id !== stepId && node.id !== `${stepId}.unit`)
|
|
267
|
+
fail(`unit ${node.id} does not belong to step ${stepId}`);
|
|
268
|
+
if (Buffer.byteLength(node.instructions, "utf8") > WORKFLOW_MAX_INSTRUCTION_BYTES)
|
|
269
|
+
fail(`unit ${node.id} instructions exceed the 256 KiB resource limit`);
|
|
270
|
+
if (node.schema !== undefined)
|
|
271
|
+
validateSchema(node.schema, `unit ${node.id} schema`);
|
|
272
|
+
validateRetry(node.retry, node.id);
|
|
273
|
+
validateStringArray(node.env, `unit ${node.id} env`, MAX_LIST_ITEMS, true);
|
|
274
|
+
validateSource(node.source, `unit ${node.id} source`);
|
|
275
|
+
validateInvocation(node.invocation, references, hooks);
|
|
276
|
+
}
|
|
277
|
+
function validateGate(gate, stepId, references, nodeIds, hooks) {
|
|
278
|
+
if (!isRecord(gate) ||
|
|
279
|
+
gate.kind !== "gate" ||
|
|
280
|
+
gate.id !== `${stepId}.gate` ||
|
|
281
|
+
gate.stepId !== stepId ||
|
|
282
|
+
!Array.isArray(gate.criteria) ||
|
|
283
|
+
gate.criteria.length > MAX_LIST_ITEMS ||
|
|
284
|
+
!gate.criteria.every((x) => typeof x === "string" && x.length > 0 && x.length <= MAX_STRING_LENGTH) ||
|
|
285
|
+
!Number.isInteger(gate.maxLoops) ||
|
|
286
|
+
gate.maxLoops < 1 ||
|
|
287
|
+
gate.maxLoops > WORKFLOW_MAX_GATE_LOOPS ||
|
|
288
|
+
typeof gate.required !== "boolean")
|
|
289
|
+
fail(`gate for step ${stepId} is invalid`);
|
|
290
|
+
if (nodeIds.has(gate.id))
|
|
291
|
+
fail(`gate id ${gate.id} collides with a node`);
|
|
292
|
+
assertKeys(gate, ["kind", "id", "stepId", "criteria", "maxLoops", "required", "judge"], `gate ${stepId}`);
|
|
293
|
+
nodeIds.add(gate.id);
|
|
294
|
+
if (gate.criteria.length === 0 && gate.judge !== null)
|
|
295
|
+
fail(`gate ${gate.id} without criteria cannot have a judge`);
|
|
296
|
+
if (gate.required && gate.criteria.length === 0)
|
|
297
|
+
fail(`gate ${gate.id} cannot be required without criteria`);
|
|
298
|
+
if (gate.judge !== null)
|
|
299
|
+
validateInvocation(gate.judge, references, hooks);
|
|
300
|
+
}
|
|
301
|
+
function validateInvocation(invocation, references, hooks) {
|
|
302
|
+
if (!isRecord(invocation) ||
|
|
303
|
+
typeof invocation.engine !== "string" ||
|
|
304
|
+
!invocation.engine ||
|
|
305
|
+
!((typeof invocation.model === "string" && invocation.model.length > 0) || invocation.model === null) ||
|
|
306
|
+
!(invocation.timeoutMs === null ||
|
|
307
|
+
(Number.isSafeInteger(invocation.timeoutMs) &&
|
|
308
|
+
invocation.timeoutMs >= 1 &&
|
|
309
|
+
invocation.timeoutMs <= WORKFLOW_MAX_TIMEOUT_MS)))
|
|
310
|
+
fail("invocation is invalid");
|
|
311
|
+
references.add(invocation.engine);
|
|
312
|
+
assertKeys(invocation, ["engine", "model", "timeoutMs", "llm"], "invocation");
|
|
313
|
+
validateLlmOverrides(invocation.llm, hooks);
|
|
314
|
+
}
|
|
315
|
+
function validateRoute(route, stepId) {
|
|
316
|
+
if (!isRecord(route) ||
|
|
317
|
+
typeof route.input !== "string" ||
|
|
318
|
+
!route.input ||
|
|
319
|
+
!isRecord(route.when) ||
|
|
320
|
+
Object.keys(route.when).length === 0 ||
|
|
321
|
+
Object.keys(route.when).length > WORKFLOW_MAX_ROUTE_BRANCHES ||
|
|
322
|
+
!Object.keys(route.when).every((match) => match.length > 0 && match.length <= MAX_STRING_LENGTH) ||
|
|
323
|
+
!Object.values(route.when).every((target) => typeof target === "string" && target))
|
|
324
|
+
fail(`route for step ${stepId} is invalid`);
|
|
325
|
+
assertKeys(route, ["input", "when", "defaultStepId"], `route ${stepId}`);
|
|
326
|
+
if (route.defaultStepId !== undefined && (typeof route.defaultStepId !== "string" || !route.defaultStepId))
|
|
327
|
+
fail(`route for step ${stepId} has an invalid default target`);
|
|
328
|
+
}
|
|
329
|
+
function assertUnitEngineCompatibility(node, engines) {
|
|
330
|
+
const unit = node.kind === "map" ? node.template : node;
|
|
331
|
+
const engine = unit.invocation ? engines[unit.invocation.engine] : undefined;
|
|
332
|
+
if (!engine)
|
|
333
|
+
return;
|
|
334
|
+
if (engine.kind === "llm" && unit.invocation?.model === null)
|
|
335
|
+
fail(`LLM unit ${unit.id} has no exact model`);
|
|
336
|
+
if (engine.kind === "agent" && unit.invocation?.llm !== undefined)
|
|
337
|
+
fail(`agent unit ${unit.id} cannot carry LLM invocation settings`);
|
|
338
|
+
if (engine.kind === "llm" && ((unit.env?.length ?? 0) > 0 || unit.isolation === "worktree")) {
|
|
339
|
+
fail(`LLM unit ${unit.id} cannot use env injection or worktree isolation`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function assertKeys(value, allowed, label) {
|
|
343
|
+
for (const key of Object.keys(value))
|
|
344
|
+
if (!allowed.includes(key))
|
|
345
|
+
fail(`${label} contains unknown key ${key}`);
|
|
346
|
+
}
|
|
347
|
+
function assertJson(value, depth = 0) {
|
|
348
|
+
if (depth > WORKFLOW_MAX_JSON_DEPTH)
|
|
349
|
+
fail("plan exceeds JSON depth limit of 64");
|
|
350
|
+
if (value === null || typeof value === "boolean")
|
|
351
|
+
return;
|
|
352
|
+
if (typeof value === "string") {
|
|
353
|
+
if (value.length > MAX_STRING_LENGTH)
|
|
354
|
+
fail("plan contains an oversized string");
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (typeof value === "number") {
|
|
358
|
+
if (!Number.isFinite(value))
|
|
359
|
+
fail("plan contains a non-finite number");
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (Array.isArray(value)) {
|
|
363
|
+
for (const item of value)
|
|
364
|
+
assertJson(item, depth + 1);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (isRecord(value)) {
|
|
368
|
+
for (const item of Object.values(value))
|
|
369
|
+
assertJson(item, depth + 1);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
fail("plan contains a non-JSON value");
|
|
373
|
+
}
|
|
374
|
+
function validateParams(params, schemas) {
|
|
375
|
+
validateStringArray(params, "params", WORKFLOW_MAX_PARAMS, true);
|
|
376
|
+
if (params?.some((name) => !PROGRAM_PARAM_NAME_PATTERN.test(name)))
|
|
377
|
+
fail("params contains an invalid name");
|
|
378
|
+
if (schemas !== undefined) {
|
|
379
|
+
if (!isRecord(schemas) || Object.keys(schemas).length > WORKFLOW_MAX_PARAMS)
|
|
380
|
+
fail("paramSchemas must be a bounded object");
|
|
381
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
382
|
+
if (!PROGRAM_PARAM_NAME_PATTERN.test(name))
|
|
383
|
+
fail(`paramSchemas.${name} is invalid`);
|
|
384
|
+
validateSchema(schema, `paramSchemas.${name}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const names = params ?? [];
|
|
388
|
+
if (schemas && (names.length !== Object.keys(schemas).length || names.some((name) => !Object.hasOwn(schemas, name))))
|
|
389
|
+
fail("params and paramSchemas must name the same parameters");
|
|
390
|
+
if (!schemas && names.length > 0) {
|
|
391
|
+
// Markdown workflows have named parameters but no schemas, which is valid.
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function validateBudget(budget) {
|
|
396
|
+
if (budget === undefined)
|
|
397
|
+
return;
|
|
398
|
+
if (!isRecord(budget))
|
|
399
|
+
fail("budget must be an object");
|
|
400
|
+
assertKeys(budget, ["maxTokens", "maxUnits"], "budget");
|
|
401
|
+
if (budget.maxTokens === undefined && budget.maxUnits === undefined)
|
|
402
|
+
fail("budget must declare a ceiling");
|
|
403
|
+
validateOptionalPositiveInteger(budget.maxTokens, "budget.maxTokens");
|
|
404
|
+
const maxUnits = budget.maxUnits;
|
|
405
|
+
if (maxUnits !== undefined &&
|
|
406
|
+
(!Number.isSafeInteger(maxUnits) || maxUnits < 1 || maxUnits > WORKFLOW_MAX_UNITS))
|
|
407
|
+
fail(`budget.maxUnits must be an integer from 1 through ${WORKFLOW_MAX_UNITS}`);
|
|
408
|
+
}
|
|
409
|
+
function validateRetry(retry, nodeId) {
|
|
410
|
+
if (retry === undefined)
|
|
411
|
+
return;
|
|
412
|
+
if (!isRecord(retry))
|
|
413
|
+
fail(`unit ${nodeId} retry must be an object`);
|
|
414
|
+
assertKeys(retry, ["max", "on"], `unit ${nodeId} retry`);
|
|
415
|
+
if (!Number.isSafeInteger(retry.max) || retry.max < 0 || retry.max > WORKFLOW_MAX_RETRIES)
|
|
416
|
+
fail(`unit ${nodeId} retry.max is invalid`);
|
|
417
|
+
validateStringArray(retry.on, `unit ${nodeId} retry.on`, PROGRAM_RETRY_REASONS.length, true);
|
|
418
|
+
if (retry.on === undefined ||
|
|
419
|
+
retry.on.length === 0 ||
|
|
420
|
+
retry.on.some((reason) => !PROGRAM_RETRY_REASONS.includes(reason)))
|
|
421
|
+
fail(`unit ${nodeId} retry.on is invalid`);
|
|
422
|
+
}
|
|
423
|
+
function validateLlmOverrides(value, hooks) {
|
|
424
|
+
if (value === undefined)
|
|
425
|
+
return;
|
|
426
|
+
if (!isRecord(value))
|
|
427
|
+
fail("invocation.llm must be an object");
|
|
428
|
+
assertKeys(value, ["temperature", "maxTokens", "supportsJsonSchema", "extraParams", "contextLength", "enableThinking"], "invocation.llm");
|
|
429
|
+
validateOptionalFiniteNumber(value.temperature, "invocation.llm.temperature");
|
|
430
|
+
validateOptionalPositiveInteger(value.maxTokens, "invocation.llm.maxTokens");
|
|
431
|
+
validateOptionalPositiveInteger(value.contextLength, "invocation.llm.contextLength");
|
|
432
|
+
if (value.supportsJsonSchema !== undefined && typeof value.supportsJsonSchema !== "boolean")
|
|
433
|
+
fail("invocation.llm.supportsJsonSchema must be boolean");
|
|
434
|
+
if (value.enableThinking !== undefined && typeof value.enableThinking !== "boolean")
|
|
435
|
+
fail("invocation.llm.enableThinking must be boolean");
|
|
436
|
+
if (value.extraParams !== undefined) {
|
|
437
|
+
const issue = validateExtraParams(value.extraParams)[0];
|
|
438
|
+
if (issue)
|
|
439
|
+
fail(formatExtraParamsIssue("invocation.llm.extraParams", issue));
|
|
440
|
+
}
|
|
441
|
+
validateExtraParamsValue(value.extraParams, "invocation.llm.extraParams", hooks);
|
|
442
|
+
}
|
|
443
|
+
function validateSchema(value, label) {
|
|
444
|
+
if (!isRecord(value))
|
|
445
|
+
fail(`${label} must be an object`);
|
|
446
|
+
if (jsonBytes(value) > WORKFLOW_MAX_SCHEMA_BYTES)
|
|
447
|
+
fail(`${label} exceeds the 256 KiB resource limit`);
|
|
448
|
+
}
|
|
449
|
+
function validateExtraParamsValue(value, label, hooks) {
|
|
450
|
+
if (value === undefined)
|
|
451
|
+
return;
|
|
452
|
+
if (!isRecord(value))
|
|
453
|
+
fail(`${label} must be an object`);
|
|
454
|
+
if (jsonBytes(value) > WORKFLOW_MAX_EXTRA_PARAMS_BYTES)
|
|
455
|
+
fail(`${label} exceeds the 64 KiB resource limit`);
|
|
456
|
+
const issue = validateExtraParams(value)[0];
|
|
457
|
+
if (issue)
|
|
458
|
+
fail(formatExtraParamsIssue(label, issue));
|
|
459
|
+
const policyError = hooks.validateExtraParams?.(value, label);
|
|
460
|
+
if (policyError)
|
|
461
|
+
fail(`${label}: ${policyError}`);
|
|
462
|
+
}
|
|
463
|
+
function validateStepExpressions(step, index, steps) {
|
|
464
|
+
const validateReferences = (text, label, itemAllowed, wholeValue) => {
|
|
465
|
+
const parsed = parseTemplate(text);
|
|
466
|
+
if (!parsed.ok)
|
|
467
|
+
fail(`${label} contains an invalid expression`);
|
|
468
|
+
if (wholeValue && (parsed.segments.length !== 1 || parsed.segments[0]?.kind !== "reference"))
|
|
469
|
+
fail(`${label} must be one whole-value expression`);
|
|
470
|
+
for (const reference of listReferences(parsed.segments)) {
|
|
471
|
+
if ((reference.kind === "item" || reference.kind === "itemIndex") && !itemAllowed)
|
|
472
|
+
fail(`${label} uses item outside a map unit`);
|
|
473
|
+
if (reference.kind === "stepOutput") {
|
|
474
|
+
const referenced = steps.get(reference.stepId);
|
|
475
|
+
if (referenced === undefined || referenced >= index)
|
|
476
|
+
fail(`${label} references a non-earlier step`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
if (step.root) {
|
|
481
|
+
const unit = step.root.kind === "map" ? step.root.template : step.root;
|
|
482
|
+
if (step.root.kind === "map")
|
|
483
|
+
validateReferences(step.root.over, `map ${step.root.id} over`, false, true);
|
|
484
|
+
if (unit.templating === "expressions")
|
|
485
|
+
validateReferences(unit.instructions, `unit ${unit.id} instructions`, step.root.kind === "map", false);
|
|
486
|
+
}
|
|
487
|
+
if (step.route)
|
|
488
|
+
validateReferences(step.route.input, `route ${step.stepId} input`, false, true);
|
|
489
|
+
}
|
|
490
|
+
function validateSource(value, label) {
|
|
491
|
+
if (value === undefined)
|
|
492
|
+
return;
|
|
493
|
+
if (!isRecord(value))
|
|
494
|
+
fail(`${label} must be an object`);
|
|
495
|
+
assertKeys(value, ["path", "start", "end"], label);
|
|
496
|
+
if (typeof value.path !== "string" ||
|
|
497
|
+
!value.path ||
|
|
498
|
+
!Number.isSafeInteger(value.start) ||
|
|
499
|
+
!Number.isSafeInteger(value.end) ||
|
|
500
|
+
value.start < 1 ||
|
|
501
|
+
value.end < value.start)
|
|
502
|
+
fail(`${label} is invalid`);
|
|
503
|
+
}
|
|
504
|
+
function validateStringArray(value, label, max, unique) {
|
|
505
|
+
if (value === undefined)
|
|
506
|
+
return;
|
|
507
|
+
if (!Array.isArray(value) ||
|
|
508
|
+
value.length > max ||
|
|
509
|
+
!value.every((item) => typeof item === "string" && item.length > 0 && item.length <= MAX_STRING_LENGTH) ||
|
|
510
|
+
(unique && new Set(value).size !== value.length))
|
|
511
|
+
fail(`${label} is invalid`);
|
|
512
|
+
}
|
|
513
|
+
function validateOptionalFiniteNumber(value, label) {
|
|
514
|
+
if (value !== undefined && (typeof value !== "number" || !Number.isFinite(value)))
|
|
515
|
+
fail(`${label} must be finite`);
|
|
516
|
+
}
|
|
517
|
+
function validateOptionalPositiveInteger(value, label) {
|
|
518
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value < 1))
|
|
519
|
+
fail(`${label} must be a positive safe integer`);
|
|
520
|
+
}
|
|
521
|
+
function validateEndpoint(value, engine) {
|
|
522
|
+
try {
|
|
523
|
+
const url = new URL(value);
|
|
524
|
+
if ((url.protocol !== "http:" && url.protocol !== "https:") ||
|
|
525
|
+
url.username ||
|
|
526
|
+
url.password ||
|
|
527
|
+
url.search ||
|
|
528
|
+
url.hash ||
|
|
529
|
+
!url.pathname.endsWith("/chat/completions"))
|
|
530
|
+
fail(`LLM engine ${engine} endpoint is not canonical`);
|
|
531
|
+
}
|
|
532
|
+
catch {
|
|
533
|
+
fail(`LLM engine ${engine} endpoint is invalid`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
function assertString(value, label) {
|
|
537
|
+
if (typeof value !== "string" || value.length === 0 || value.length > MAX_STRING_LENGTH)
|
|
538
|
+
fail(`${label} must be a non-empty bounded string`);
|
|
539
|
+
}
|
|
540
|
+
function isRecord(value) {
|
|
541
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
542
|
+
}
|
|
543
|
+
function fail(message) {
|
|
544
|
+
throw new UsageError(`Invalid frozen workflow plan: ${message}.`);
|
|
545
|
+
}
|
package/dist/workflows/parser.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { parse as yamlParse } from "yaml";
|
|
14
14
|
import { parseFrontmatterBlock } from "../core/asset/frontmatter.js";
|
|
15
15
|
import { parseMarkdownToc } from "../core/asset/markdown.js";
|
|
16
|
+
import { utf8Bytes, WORKFLOW_MAX_SOURCE_BYTES } from "./resource-limits.js";
|
|
16
17
|
import { WORKFLOW_SCHEMA_VERSION, } from "./schema.js";
|
|
17
18
|
import { runSemanticChecks } from "./validator.js";
|
|
18
19
|
const WORKFLOW_TITLE_PREFIX = "Workflow:";
|
|
@@ -48,6 +49,12 @@ function stripFencedCodeBlocks(body) {
|
|
|
48
49
|
return stripped.join("\n");
|
|
49
50
|
}
|
|
50
51
|
export function parseWorkflow(markdown, source) {
|
|
52
|
+
if (utf8Bytes(markdown) > WORKFLOW_MAX_SOURCE_BYTES) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
errors: [{ line: 1, message: "Workflow source exceeds the 1 MiB resource limit." }],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
51
58
|
const errors = [];
|
|
52
59
|
const path = source.path;
|
|
53
60
|
const lines = markdown.split(/\r?\n/);
|