akm-cli 0.9.2-alpha.4 → 0.9.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 +493 -0
- package/STABILITY.md +23 -5
- package/dist/assets/hints/cli-hints-full.md +12 -7
- package/dist/assets/tasks/core/extract.yml +3 -5
- package/dist/assets/tasks/core/improve.yml +3 -5
- package/dist/assets/tasks/core/index-refresh.yml +3 -5
- package/dist/assets/tasks/core/sync.yml +3 -5
- package/dist/assets/tasks/core/version-check.yml +3 -5
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -6
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +3 -5
- package/dist/cli/unknown-flags.js +12 -1
- package/dist/cli.js +8 -1
- package/dist/commands/command/command-execution.js +23 -2
- package/dist/commands/health/improve-metrics.js +38 -0
- package/dist/commands/health/windows.js +8 -4
- package/dist/commands/health.js +8 -4
- package/dist/commands/lint/index.js +1 -1
- package/dist/commands/migrate-cli.js +130 -24
- package/dist/commands/proposal/validators/proposal-validators.js +7 -2
- package/dist/commands/tasks/explain.js +304 -0
- package/dist/commands/tasks/tasks-cli.js +185 -3
- package/dist/commands/tasks/tasks.js +265 -52
- package/dist/commands/workflow/plan.js +159 -0
- package/dist/commands/workflow-cli.js +94 -2
- package/dist/core/activation-policy.js +2 -12
- package/dist/core/adapter/adapters/akm-lint.js +7 -4
- package/dist/core/adapter/adapters/akm-metadata.js +26 -14
- package/dist/core/adapter/adapters/akm-task-adapter.js +13 -10
- package/dist/core/errors.js +45 -0
- package/dist/core/json-schema.js +15 -5
- package/dist/core/state/migrations.js +57 -0
- package/dist/core/state-db.js +16 -14
- package/dist/core/subprocess.js +47 -13
- package/dist/execution/guarded-source.js +44 -0
- package/dist/execution/input-contract.js +250 -0
- package/dist/execution/target-ref.js +63 -0
- package/dist/indexer/usage/usage-events.js +14 -3
- package/dist/integrations/agent/execution-lowering.js +12 -1
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +12 -3
- package/dist/output/text/workflow-format.js +192 -10
- package/dist/output/text/workflow.js +2 -1
- package/dist/runtime.js +1 -0
- package/dist/scripts/akm-migrate-node.js +11838 -10118
- package/dist/scripts/akm-migrate.js +11828 -10117
- package/dist/setup/steps/tasks.js +34 -17
- package/dist/storage/repositories/task-history-repository.js +5 -1
- package/dist/storage/repositories/workflow-runs-repository.js +144 -6
- package/dist/tasks/backends/launchd.js +31 -84
- package/dist/tasks/embedded.js +13 -7
- package/dist/tasks/model/invocation.js +4 -0
- package/dist/tasks/prepare/prepare-script-target.js +9 -0
- package/dist/tasks/prepare/prepare-support.js +154 -0
- package/dist/tasks/prepare/prepare.js +117 -0
- package/dist/tasks/prepare/prepared-execution.js +4 -0
- package/dist/tasks/prepare/script-capture.js +80 -0
- package/dist/tasks/run/attempt-lifecycle.js +165 -0
- package/dist/tasks/run/load-task.js +117 -0
- package/dist/tasks/run/provenance.js +20 -0
- package/dist/tasks/run/run-command-task.js +92 -0
- package/dist/tasks/run/run-native-task.js +222 -0
- package/dist/tasks/run/run-task.js +99 -0
- package/dist/tasks/run/run-workflow-task.js +222 -0
- package/dist/tasks/run/task-history.js +134 -0
- package/dist/tasks/run/task-log.js +179 -0
- package/dist/tasks/run/task-result.js +19 -0
- package/dist/tasks/scheduler-binding.js +66 -2
- package/dist/tasks/scheduler-invocation.js +63 -3
- package/dist/tasks/scheduler-sync.js +77 -14
- package/dist/tasks/source/bounded-document.js +455 -0
- package/dist/tasks/source/parse-task-source.js +59 -0
- package/dist/tasks/source/project-v4.js +62 -0
- package/dist/tasks/source/task-input-diagnostics.js +36 -0
- package/dist/tasks/source/task-source-v4.js +626 -0
- package/dist/tasks/source-v3.js +10 -733
- package/dist/tasks/task-run-reserved-flags.js +79 -0
- package/dist/workflows/authoring/authoring.js +17 -8
- package/dist/workflows/exec/child-invocation.js +34 -0
- package/dist/workflows/exec/child-workflow.js +370 -0
- package/dist/workflows/exec/exec-unit.js +50 -170
- package/dist/workflows/exec/frozen-judge.js +19 -2
- package/dist/workflows/exec/native-executor.js +49 -27
- package/dist/workflows/exec/param-secrets.js +12 -0
- package/dist/workflows/exec/run-workflow.js +48 -59
- package/dist/workflows/exec/step-work.js +222 -80
- package/dist/workflows/exec/unit-dispatch.js +72 -0
- package/dist/workflows/freeze/child-output-references.js +94 -0
- package/dist/workflows/freeze/environment.js +174 -0
- package/dist/workflows/freeze/identity.js +22 -0
- package/dist/workflows/freeze/resolve-steps.js +78 -0
- package/dist/workflows/freeze/source-freeze.js +57 -0
- package/dist/workflows/freeze/step-values.js +68 -0
- package/dist/workflows/freeze/targets/child-workflow.js +206 -0
- package/dist/workflows/freeze/targets/command.js +81 -0
- package/dist/workflows/freeze/targets/script.js +57 -0
- package/dist/workflows/freeze/targets/shell.js +31 -0
- package/dist/workflows/freeze/targets/task.js +179 -0
- package/dist/workflows/freeze/task-bindings.js +180 -0
- package/dist/workflows/ir/compile.js +59 -11
- package/dist/workflows/ir/environment-v4.js +3 -3
- package/dist/workflows/ir/freeze-v4.js +41 -7
- package/dist/workflows/ir/params.js +58 -131
- package/dist/workflows/ir/plan-hash.js +3 -3
- package/dist/workflows/ir/schema-v4.js +246 -17
- package/dist/workflows/parser.js +74 -2
- package/dist/workflows/program/schema.js +5 -2
- package/dist/workflows/resource-limits.js +20 -0
- package/dist/workflows/runtime/plan-classifier.js +24 -7
- package/dist/workflows/runtime/run-outputs.js +103 -0
- package/dist/workflows/runtime/runs.js +114 -9
- package/dist/workflows/runtime/workflow-asset-loader.js +14 -6
- package/dist/workflows/source-files.js +5 -5
- package/dist/workflows/source-ir/compare.js +17 -0
- package/dist/workflows/source-ir/compile.js +7 -3
- package/dist/workflows/source-ir/github-yaml.js +64 -17
- package/dist/workflows/source-ir/schema.js +69 -21
- package/dist/workflows/source-ir/semantics.js +7 -25
- package/dist/workflows/source-ir/triggers.js +79 -0
- package/dist/workflows/source-ir/uses.js +33 -7
- package/docs/migration/README.md +1 -1
- package/docs/migration/release-notes/0.9.2.md +87 -11
- package/docs/migration/release-notes/README.md +3 -2
- package/docs/migration/v0.8-to-v0.9.md +13 -11
- package/docs/migration/v0.9.0-troubleshooting.md +20 -13
- package/docs/migration/v0.9.1-to-v0.9.2.md +598 -49
- package/docs/reference/README.md +1 -1
- package/docs/reference/cli.md +140 -46
- package/docs/reference/configuration.md +6 -5
- package/docs/reference/supported-formats.md +9 -5
- package/docs/reference/tasks.md +338 -75
- package/docs/reference/workflow-schema.md +290 -16
- package/docs/reference/workflows.md +57 -7
- package/package.json +1 -1
- package/schemas/akm-task.json +173 -118
- package/schemas/akm-workflow.json +28 -0
- package/dist/tasks/runner.js +0 -941
- package/dist/tasks/runtime-v3.js +0 -281
- package/dist/workflows/ir/source-freeze-v4.js +0 -506
- package/dist/workflows/source-ir/ordering.js +0 -38
|
@@ -4,44 +4,21 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* Shared step semantics — the ONE implementation of a step's orchestration
|
|
6
6
|
* decisions, consumed by the engine loop (`run-workflow.ts` +
|
|
7
|
-
* `native-executor.ts`) on both the fresh-execution and the resume/replay
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* `native-executor.ts`) on both the fresh-execution and the resume/replay
|
|
8
|
+
* path, so a first run and a resumed run of the same frozen plan produce
|
|
9
|
+
* byte-identical unit graphs. `computeStepWorkList` and its reducer/gate/route
|
|
10
|
+
* helpers are PURE (no clock, no IO, no journal read); the gate-evaluation
|
|
11
|
+
* journaling functions are the one deliberate exception. This module never
|
|
12
|
+
* dispatches a unit and never writes step rows.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* {@link computeStepWorkList} — given the frozen step plan and a
|
|
17
|
-
* {@link WorkListInput} (params, prior step outputs, gate-loop number + its
|
|
18
|
-
* recovered feedback) — is a pure function: same inputs ⇒ same unit ids, input
|
|
19
|
-
* hashes, and fully-resolved prompts. It takes NO clock, NO IO, and NO journal
|
|
20
|
-
* (journal-derived state, i.e. the recovered gate feedback, is passed in). This
|
|
21
|
-
* is the load-bearing guarantee that a resumed run recomputes exactly the units
|
|
22
|
-
* the original run dispatched, so journaled rows can be reused instead of
|
|
23
|
-
* re-executed. So are the reducer/artifact helpers
|
|
24
|
-
* ({@link buildEvidence}, {@link projectStepOutput}, {@link validateStepArtifact},
|
|
25
|
-
* {@link buildArtifactSummary}), the gate-feedback recovery
|
|
26
|
-
* ({@link recoverGateFeedback} / {@link activeGateLoop}), and route evaluation
|
|
27
|
-
* ({@link evaluateRoute} and its bookkeeping).
|
|
28
|
-
*
|
|
29
|
-
* ## What does IO here
|
|
30
|
-
*
|
|
31
|
-
* The gate-evaluation journaling ({@link journalGateEvaluationStart} /
|
|
32
|
-
* {@link journalGateEvaluationFinish}) writes `workflow_run_units` rows through
|
|
33
|
-
* the serialized writer queue — an engine-driven judge call is an LLM call and
|
|
34
|
-
* is journaled like a unit. It lives here (not in the engine loop) so every
|
|
35
|
-
* caller journals gate evaluations through the identical writer.
|
|
36
|
-
*
|
|
37
|
-
* This module NEVER dispatches a unit and NEVER writes step rows: dispatch is
|
|
38
|
-
* the executor's job (`native-executor.ts`), advancing the gated spine is the
|
|
39
|
-
* engine loop's job (`run-workflow.ts` via `completeWorkflowStep`).
|
|
14
|
+
* See docs/architecture/decisions/0002-unit-reuse-and-input-hash-scope.md for
|
|
15
|
+
* the full purity-contract design history.
|
|
40
16
|
*/
|
|
41
17
|
import { createHash, randomUUID } from "node:crypto";
|
|
42
18
|
import unitPreambleTemplate from "../../assets/prompts/workflow-unit-preamble.md" with { type: "text" };
|
|
43
19
|
import { UsageError } from "../../core/errors.js";
|
|
44
20
|
import { validateJsonSchemaSubset } from "../../core/json-schema.js";
|
|
21
|
+
import { canonicalInputJson, validateInputs } from "../../execution/input-contract.js";
|
|
45
22
|
import { withWorkflowRunsRepo, } from "../../storage/repositories/workflow-runs-repository.js";
|
|
46
23
|
import { canonicalJson as canonicalJsonString } from "../ir/plan-hash.js";
|
|
47
24
|
import { parseReference, resolveReferenceString, } from "../program/expressions.js";
|
|
@@ -157,6 +134,54 @@ function truncatedReferenceTarget(reference, scope, resolved) {
|
|
|
157
134
|
}
|
|
158
135
|
return isTruncatedEvidence(current) ? current : undefined;
|
|
159
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Pre-attempt resolution of a task-composing step's frozen `inputBindings`
|
|
139
|
+
* (spec docs/plans/specs/p2b-input-bindings.md §3.6, B-31..B-34): a
|
|
140
|
+
* `{kind:"literal"}` passes through unchanged — its schema was already
|
|
141
|
+
* checked at FREEZE (`freezeTaskInputBindings`,
|
|
142
|
+
* `src/workflows/freeze/task-bindings.ts`), so it is never re-validated. A
|
|
143
|
+
* `{kind:"reference"}` resolves via the SAME {@link resolveStepReference}
|
|
144
|
+
* every other whole-value position uses, then validates the resolved value
|
|
145
|
+
* against the binding's own frozen `schema` — a mismatch (or a reference that
|
|
146
|
+
* fails to resolve at all) fails the WHOLE step here, before
|
|
147
|
+
* `reserveUnitAttempt` is ever called by the native executor. Absent
|
|
148
|
+
* `bindings` (the overwhelmingly common case — no `with:` on this step's
|
|
149
|
+
* target) resolves trivially to `{}` with no scope access at all.
|
|
150
|
+
*/
|
|
151
|
+
function resolveTaskInputBindings(bindings, stepId, scope) {
|
|
152
|
+
if (!bindings || bindings.length === 0)
|
|
153
|
+
return { ok: true, values: {} };
|
|
154
|
+
const values = {};
|
|
155
|
+
for (const binding of bindings) {
|
|
156
|
+
if (binding.kind === "literal") {
|
|
157
|
+
values[binding.name] = binding.value;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const resolved = resolveStepReference(binding.from, scope);
|
|
161
|
+
if (!resolved.ok) {
|
|
162
|
+
return {
|
|
163
|
+
ok: false,
|
|
164
|
+
error: `Step "${stepId}" input "${binding.name}" reference ${binding.from} failed to resolve: ` +
|
|
165
|
+
resolved.error.message,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const errors = validateInputs({ [binding.name]: { schema: binding.schema, required: false } }, { [binding.name]: resolved.value },
|
|
169
|
+
// Fixed neutral namespace (matches `checkScheduleEntryRunnable`'s
|
|
170
|
+
// `pathRoot: "inputs"` and the `contractViolation` diagnostics' own
|
|
171
|
+
// `$`-strip) — NOT `binding.name`, which would double the input name
|
|
172
|
+
// (`count.count: ...`) since the outer message below already names it.
|
|
173
|
+
{ pathRoot: "inputs" });
|
|
174
|
+
if (errors.length > 0) {
|
|
175
|
+
return {
|
|
176
|
+
ok: false,
|
|
177
|
+
error: `Step "${stepId}" input "${binding.name}" reference ${binding.from} resolved to a value violating its ` +
|
|
178
|
+
`declared schema: ${errors.join("; ")}`,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
values[binding.name] = resolved.value;
|
|
182
|
+
}
|
|
183
|
+
return { ok: true, values };
|
|
184
|
+
}
|
|
160
185
|
export function computeStepWorkList(plan, input) {
|
|
161
186
|
const root = plan.root;
|
|
162
187
|
// Route-only steps (YAML `route:`) carry no execution subgraph.
|
|
@@ -187,6 +212,19 @@ export function computeStepWorkList(plan, input) {
|
|
|
187
212
|
}
|
|
188
213
|
resolvedInputs.push({ reference, value: resolved.value });
|
|
189
214
|
}
|
|
215
|
+
// P2b Lane A2 — pre-attempt resolution of a task-composing step's frozen
|
|
216
|
+
// `inputBindings` (spec §3.6, B-31..B-34): a `{kind:"literal"}` passes
|
|
217
|
+
// through unchanged (its schema was already checked at freeze, B-34); a
|
|
218
|
+
// `{kind:"reference"}` resolves against this SAME scope, then its resolved
|
|
219
|
+
// value is validated against the binding's own frozen `schema` — a
|
|
220
|
+
// mismatch fails the WHOLE step here, before `reserveUnitAttempt` is ever
|
|
221
|
+
// reached (B-32). This runs for every target kind (command/shell/script);
|
|
222
|
+
// Lane B's delivery consumes the result via `StepWorkUnitContext.taskInputs`
|
|
223
|
+
// / `taskInputsJson` below.
|
|
224
|
+
const taskInputsResolution = resolveTaskInputBindings(template.frozenTarget.inputBindings, plan.stepId, scope);
|
|
225
|
+
if (!taskInputsResolution.ok)
|
|
226
|
+
return taskInputsResolution;
|
|
227
|
+
const hasTaskInputs = Object.keys(taskInputsResolution.values).length > 0;
|
|
190
228
|
// Resolve fan-out items: `over` is a single whole-value reference naming
|
|
191
229
|
// its producer explicitly — no ambient key search.
|
|
192
230
|
let items;
|
|
@@ -235,7 +273,18 @@ export function computeStepWorkList(plan, input) {
|
|
|
235
273
|
// An exec unit's budget is frozen on its exec spec (there is no engine to
|
|
236
274
|
// inherit one from); `ir/freeze.ts` resolved it once from unit `timeout:` →
|
|
237
275
|
// `defaults.timeout` → DEFAULT_EXEC_TIMEOUT_MS.
|
|
238
|
-
const timeoutMs = target.kind === "command"
|
|
276
|
+
const timeoutMs = target.kind === "command"
|
|
277
|
+
? (target.runner.timeoutMs ?? null)
|
|
278
|
+
: target.kind === "child-workflow"
|
|
279
|
+
? // A child-workflow target carries no exec spec of its own (§3.5).
|
|
280
|
+
// computeStepWorkList still builds this unit's context
|
|
281
|
+
// unconditionally — the child executor (child-workflow.ts,
|
|
282
|
+
// reached from native-executor.ts's dispatch seam, P3b §3.2) is
|
|
283
|
+
// what actually drives a child-workflow unit, not this line, so
|
|
284
|
+
// `null` only needs to be a value this layer can carry, never one
|
|
285
|
+
// an engine acts on.
|
|
286
|
+
null
|
|
287
|
+
: target.exec.timeoutMs;
|
|
239
288
|
// Step-constant exec context: `AKM_PARAMS` / `AKM_INPUTS` depend only on
|
|
240
289
|
// step-level values, so they are serialized ONCE here and shared by every
|
|
241
290
|
// unit. Building them inside the per-unit loop deep-cloned and re-stringified
|
|
@@ -245,6 +294,11 @@ export function computeStepWorkList(plan, input) {
|
|
|
245
294
|
const execInputsJson = frozenExec && resolvedInputs.length > 0
|
|
246
295
|
? (canonicalJson(Object.fromEntries(resolvedInputs.map((entry) => [entry.reference, entry.value]))) ?? "{}")
|
|
247
296
|
: undefined;
|
|
297
|
+
// P2b Lane A2 (§3.6): the resolved effective task-composition inputs,
|
|
298
|
+
// serialized ONCE here (mirrors execParamsJson/execInputsJson above) —
|
|
299
|
+
// Lane B's delivery (buildUnitPrompt's "## Task inputs" block,
|
|
300
|
+
// buildExecContextEnv's AKM_TASK_INPUTS) reads both back per unit.
|
|
301
|
+
const taskInputsJson = hasTaskInputs ? (canonicalJson(taskInputsResolution.values) ?? "{}") : undefined;
|
|
248
302
|
const ctx = {
|
|
249
303
|
plan,
|
|
250
304
|
input,
|
|
@@ -258,6 +312,7 @@ export function computeStepWorkList(plan, input) {
|
|
|
258
312
|
...(frozenExec ? { frozenExec } : {}),
|
|
259
313
|
...(execParamsJson !== undefined ? { execParamsJson } : {}),
|
|
260
314
|
...(execInputsJson !== undefined ? { execInputsJson } : {}),
|
|
315
|
+
...(hasTaskInputs ? { taskInputs: taskInputsResolution.values, taskInputsJson } : {}),
|
|
261
316
|
};
|
|
262
317
|
const units = items.map((item, index) => buildStepWorkUnit(ctx, unitIds[index], item, index));
|
|
263
318
|
const concurrency = root.kind === "map" ? root.concurrency : 1;
|
|
@@ -277,7 +332,7 @@ export function computeStepWorkList(plan, input) {
|
|
|
277
332
|
* repo's 220-line function bar.
|
|
278
333
|
*/
|
|
279
334
|
function buildStepWorkUnit(ctx, unitId, item, index) {
|
|
280
|
-
const { plan, input, template, isFanOut, resolvedInputs, target, frozenExec } = ctx;
|
|
335
|
+
const { plan, input, template, isFanOut, resolvedInputs, target, frozenExec, taskInputs } = ctx;
|
|
281
336
|
// Gate loops (>= 2) journal under `<unitId>~l<loop>` so loop 1's rows are
|
|
282
337
|
// never clobbered; the content-derived identity (and the prompt's
|
|
283
338
|
// {{UNIT_ID}}) stays the base id.
|
|
@@ -302,6 +357,9 @@ function buildStepWorkUnit(ctx, unitId, item, index) {
|
|
|
302
357
|
params: input.params,
|
|
303
358
|
...(isFanOut ? { item, itemIndex: index } : {}),
|
|
304
359
|
...(resolvedInputs.length > 0 ? { inputs: resolvedInputs } : {}),
|
|
360
|
+
// P2b Lane B (§4.2, B-38/B-39): the composed task's resolved
|
|
361
|
+
// `inputBindings`, when non-empty — see StepWorkUnitContext.taskInputs.
|
|
362
|
+
...(taskInputs && Object.keys(taskInputs).length > 0 ? { taskInputs } : {}),
|
|
305
363
|
...(input.gateFeedback ? { gateFeedback: input.gateFeedback } : {}),
|
|
306
364
|
...(template.schema ? { schema: template.schema } : {}),
|
|
307
365
|
instructions: template.instructions,
|
|
@@ -324,6 +382,10 @@ function buildStepWorkUnit(ctx, unitId, item, index) {
|
|
|
324
382
|
...(template.retry ? { retry: template.retry } : {}),
|
|
325
383
|
onError: template.onError,
|
|
326
384
|
...(template.isolation ? { isolation: template.isolation } : {}),
|
|
385
|
+
// P3b §3.3 step 2: the SAME resolved `with:` bindings `taskInputs` already
|
|
386
|
+
// carries, exposed under the name `child-workflow.ts`'s drive contract
|
|
387
|
+
// reads. Absent (never `{}`) when the step binds nothing.
|
|
388
|
+
...(taskInputs && Object.keys(taskInputs).length > 0 ? { childParams: taskInputs } : {}),
|
|
327
389
|
prompt,
|
|
328
390
|
inputHash,
|
|
329
391
|
};
|
|
@@ -345,7 +407,8 @@ function buildStepWorkUnit(ctx, unitId, item, index) {
|
|
|
345
407
|
*
|
|
346
408
|
* SIZE is not bounded here, on purpose. A workflow artifact has no bound
|
|
347
409
|
* comparable to an OS environment entry, so `AKM_INPUTS` (and `AKM_PARAMS` /
|
|
348
|
-
* `AKM_ITEM`) can serialize past what `execve` accepts and
|
|
410
|
+
* `AKM_ITEM` / `AKM_TASK_INPUTS`) can serialize past what `execve` accepts and
|
|
411
|
+
* make PROCESS CREATION
|
|
349
412
|
* fail with a bare `E2BIG`. The check belongs at the spawn boundary, where the
|
|
350
413
|
* failure can be journaled as a unit outcome with an actionable message naming
|
|
351
414
|
* the variable: `checkExecContextSize` in `exec/exec-unit.ts`, against
|
|
@@ -369,54 +432,42 @@ function buildExecContextEnv(args) {
|
|
|
369
432
|
}
|
|
370
433
|
if (ctx.execInputsJson !== undefined)
|
|
371
434
|
env.AKM_INPUTS = ctx.execInputsJson;
|
|
435
|
+
// P2b Lane B (spec §4.1, B-35/B-36/B-39, B-N1): ONE variable carrying the
|
|
436
|
+
// composed task's effective `inputBindings` as canonical JSON — never one
|
|
437
|
+
// var per input. Absent when the frozen target carries no `inputBindings`
|
|
438
|
+
// or every resolved value is empty. Sizing is enforced by the SAME generic
|
|
439
|
+
// `checkExecContextSize` loop as every other `AKM_*` entry (exec-unit.ts) —
|
|
440
|
+
// no change there, the roster is just longer by one name (B-37).
|
|
441
|
+
if (ctx.taskInputsJson !== undefined)
|
|
442
|
+
env.AKM_TASK_INPUTS = ctx.taskInputsJson;
|
|
372
443
|
return env;
|
|
373
444
|
}
|
|
374
445
|
/**
|
|
375
|
-
* The canonical dispatch-input envelope
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
* is
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
446
|
+
* The canonical dispatch-input envelope: every field here is an input that
|
|
447
|
+
* changes what the backend is actually asked to do, so a completed unit is
|
|
448
|
+
* reused ONLY when all of them match. `env` carries names only, never
|
|
449
|
+
* resolved secret values. `retry`/`onError` are deliberately excluded — they
|
|
450
|
+
* govern failed-unit re-dispatch, not a completed unit's inputs/output.
|
|
451
|
+
* `gateFeedback` is included conditionally (a gate retry is a materially
|
|
452
|
+
* different ask). `taskInputs` is likewise included conditionally (R-R15,
|
|
453
|
+
* `hashVersion` 7): a reference binding's RESOLVED value reaches the unit's
|
|
454
|
+
* prompt / `AKM_TASK_INPUTS` / `childParams`, so a changed upstream value is a
|
|
455
|
+
* materially different ask even though the binding's authored shape inside
|
|
456
|
+
* `frozenTarget` is unchanged — hashing it makes a resume whose journaled
|
|
457
|
+
* upstream output was altered fail loudly as replay divergence instead of
|
|
458
|
+
* silently reusing the stale row. The key is absent for a unit whose target
|
|
459
|
+
* carries no `inputBindings`, so a binding-free unit's preimage keeps the same
|
|
460
|
+
* shape it had (only the version fields moved 6 → 7). This is the ONE place a
|
|
461
|
+
* unit's inputHash is computed.
|
|
389
462
|
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
* each reaches dispatch and a changed one yields a materially different call.
|
|
393
|
-
* `env` carries NAMES ONLY, never resolved values: hashing a resolved secret
|
|
394
|
-
* would leak it into a durable hash oracle and would spuriously re-dispatch on
|
|
395
|
-
* every secret rotation. `retry`/`onError` are DELIBERATELY excluded — they
|
|
396
|
-
* govern failed-unit re-dispatch and step-level failure reduction, not a
|
|
397
|
-
* COMPLETED unit's inputs/output, so a completed row stays valid across policy
|
|
398
|
-
* changes.
|
|
399
|
-
*
|
|
400
|
-
* `gateFeedback` IS included (conditionally, so a no-feedback unit's preimage
|
|
401
|
-
* is byte-identical to before): it is appended to the prompt by
|
|
402
|
-
* `buildUnitPrompt`, so a gate loop's retry is materially a different ask than
|
|
403
|
-
* the rejected attempt. Replay-safe: feedback is re-derived from the journaled
|
|
404
|
-
* gate decision, so a resumed retry re-hashes identically.
|
|
405
|
-
*
|
|
406
|
-
* Command targets carry their frozen argv/script/cwd/timeout identity in the
|
|
407
|
-
* same target slot used by agent, SDK, and direct-LLM work. The complete
|
|
408
|
-
* current target is hashed once, so a completed unit is reused only for the
|
|
409
|
-
* exact durable request that originally produced it.
|
|
410
|
-
*
|
|
411
|
-
* Ambient config is deliberately excluded because it is not consulted during
|
|
412
|
-
* execution. The frozen target and named environment bindings are the runtime
|
|
413
|
-
* identity boundary; only the values behind those names remain live.
|
|
463
|
+
* See docs/architecture/decisions/0002-unit-reuse-and-input-hash-scope.md for
|
|
464
|
+
* the full field-by-field inclusion/exclusion rationale (reviewer finding #1).
|
|
414
465
|
*/
|
|
415
466
|
function computeUnitInputHash(ctx, item) {
|
|
416
467
|
return createHash("sha256")
|
|
417
|
-
.update("akm.workflow.unit\
|
|
468
|
+
.update("akm.workflow.unit\0v7\0")
|
|
418
469
|
.update(canonicalJsonString({
|
|
419
|
-
hashVersion:
|
|
470
|
+
hashVersion: 7,
|
|
420
471
|
role: "unit",
|
|
421
472
|
stepId: ctx.plan.stepId,
|
|
422
473
|
nodeId: ctx.template.id,
|
|
@@ -428,6 +479,7 @@ function computeUnitInputHash(ctx, item) {
|
|
|
428
479
|
environment: ctx.template.environment,
|
|
429
480
|
schema: ctx.template.schema ?? null,
|
|
430
481
|
isolation: ctx.template.isolation ?? "none",
|
|
482
|
+
...(ctx.taskInputs !== undefined ? { taskInputs: ctx.taskInputs } : {}),
|
|
431
483
|
...(ctx.input.gateFeedback ? { gateFeedback: ctx.input.gateFeedback } : {}),
|
|
432
484
|
}))
|
|
433
485
|
.digest("hex");
|
|
@@ -442,7 +494,7 @@ function computeUnitInputHash(ctx, item) {
|
|
|
442
494
|
* here.
|
|
443
495
|
*/
|
|
444
496
|
export function buildUnitPrompt(input) {
|
|
445
|
-
const { runId, stepId, unitId, params, itemIndex, item, inputs, gateFeedback, schema, instructions } = input;
|
|
497
|
+
const { runId, stepId, unitId, params, itemIndex, item, inputs, taskInputs, gateFeedback, schema, instructions } = input;
|
|
446
498
|
// Function replacements throughout: a string replacement would interpret
|
|
447
499
|
// GetSubstitution patterns ($&, $$, $', $`) inside VALUES and silently
|
|
448
500
|
// corrupt the prompt (e.g. a param value containing "$&").
|
|
@@ -460,6 +512,15 @@ export function buildUnitPrompt(input) {
|
|
|
460
512
|
const inputsBlock = inputs && inputs.length > 0
|
|
461
513
|
? `\n\n## Declared inputs\n${inputs.map((i) => `### ${i.reference}\n${safeJson(i.value)}`).join("\n\n")}`
|
|
462
514
|
: "";
|
|
515
|
+
// P2b Lane B (spec §4.2, B-38/B-39, B-N2): the composed task's resolved
|
|
516
|
+
// `inputBindings`, as a structured fenced JSON block — the same "attached
|
|
517
|
+
// context, never a splice" mechanism as itemBlock/inputsBlock above.
|
|
518
|
+
// `canonicalInputJson` (sorted keys) matches the AKM_TASK_INPUTS env var's
|
|
519
|
+
// own serialization, so the effective-inputs value reads identically on
|
|
520
|
+
// every delivery surface. Absent (or empty) appends nothing (B-39).
|
|
521
|
+
const taskInputsBlock = taskInputs && Object.keys(taskInputs).length > 0
|
|
522
|
+
? `\n\n## Task inputs\nThe composed task's declared inputs resolved to:\n\`\`\`json\n${canonicalInputJson(taskInputs)}\n\`\`\``
|
|
523
|
+
: "";
|
|
463
524
|
// Gate-loop feedback (R2 max_loops): the judge's rejection is appended so
|
|
464
525
|
// the re-executed unit can address it — and so the input hash changes,
|
|
465
526
|
// making the loop's re-dispatch natural instead of a durable-row reuse.
|
|
@@ -474,7 +535,7 @@ export function buildUnitPrompt(input) {
|
|
|
474
535
|
const schemaDirective = schema
|
|
475
536
|
? `\n\nRespond with ONLY a JSON value matching this JSON Schema (no prose, no code fences):\n${safeJson(schema)}`
|
|
476
537
|
: "";
|
|
477
|
-
return `${preamble}\n${instructions}${itemBlock}${inputsBlock}${gateBlock}${schemaDirective}`;
|
|
538
|
+
return `${preamble}\n${instructions}${itemBlock}${inputsBlock}${taskInputsBlock}${gateBlock}${schemaDirective}`;
|
|
478
539
|
}
|
|
479
540
|
/**
|
|
480
541
|
* Content-derived unit identity (module doc): `<node_id>:<sha256>` for a
|
|
@@ -706,7 +767,28 @@ export function reduceStepOutcomes(plan, reducer, isFanOut, onError, units) {
|
|
|
706
767
|
artifactSchemaFailure = true;
|
|
707
768
|
}
|
|
708
769
|
}
|
|
709
|
-
|
|
770
|
+
// P3b §3.4: a composed child workflow that blocked is carried on the
|
|
771
|
+
// failed unit's LIVE-ONLY `childRun` field (child-workflow.ts's
|
|
772
|
+
// driveChildWorkflowUnit). Surfaced here, unconditionally on the unit
|
|
773
|
+
// list, so `finalizeExecutedStep` can check it before deciding whether
|
|
774
|
+
// this step's failure is retryable — an `onError: "continue"` step that
|
|
775
|
+
// tolerates the failure (`ok` stays true) never reaches that check at all.
|
|
776
|
+
const blockedChildUnit = failed.find((u) => u.failureReason === "child_workflow_blocked" && u.childRun !== undefined);
|
|
777
|
+
const childBlocked = blockedChildUnit?.childRun
|
|
778
|
+
? {
|
|
779
|
+
childRunId: blockedChildUnit.childRun.runId,
|
|
780
|
+
childRef: blockedChildUnit.childRun.ref,
|
|
781
|
+
childStepId: blockedChildUnit.childRun.currentStepId,
|
|
782
|
+
}
|
|
783
|
+
: undefined;
|
|
784
|
+
return {
|
|
785
|
+
ok,
|
|
786
|
+
units,
|
|
787
|
+
evidence,
|
|
788
|
+
summary,
|
|
789
|
+
...(artifactSchemaFailure ? { artifactSchemaFailure: true } : {}),
|
|
790
|
+
...(childBlocked ? { childBlocked } : {}),
|
|
791
|
+
};
|
|
710
792
|
}
|
|
711
793
|
/**
|
|
712
794
|
* The reduced outcome of a step whose fan-out list resolved to EMPTY (`over: []`
|
|
@@ -1183,6 +1265,56 @@ async function blockFinalizedStep(input, cause) {
|
|
|
1183
1265
|
});
|
|
1184
1266
|
return { kind: "judge-failed", summary };
|
|
1185
1267
|
}
|
|
1268
|
+
/**
|
|
1269
|
+
* §3.4's exact `blockStepForChildWorkflow` notes — the ONE place the
|
|
1270
|
+
* blocked-child resume sequence is worded, mirroring {@link judgeFailureNotes}.
|
|
1271
|
+
* Two properties this wording pins (each its own test): the CHILD is resumed
|
|
1272
|
+
* FIRST, and the PARENT's own re-drive is what advances it (the child drive
|
|
1273
|
+
* never calls `resumeWorkflowRun` itself, row A-22); the notes name the child
|
|
1274
|
+
* run id and both commands verbatim, so the text renderer needs no change
|
|
1275
|
+
* (B-N15 — Lane A touches no output module).
|
|
1276
|
+
*/
|
|
1277
|
+
function childWorkflowBlockedNotes(runId, stepId, childRunId, childRef, childStepId) {
|
|
1278
|
+
return (`Step "${stepId}" composes child workflow run ${childRunId} (${childRef}), ` +
|
|
1279
|
+
`which is blocked at its own step "${childStepId ?? "(unknown)"}". Nothing in this run advances ` +
|
|
1280
|
+
`until the child does — a gate is a gate for a child workflow too, so \`akm\` will ` +
|
|
1281
|
+
`not resume it for you. Clear it with \`akm workflow resume ${childRunId}\`, then ` +
|
|
1282
|
+
`\`akm workflow resume ${runId}\` and \`akm workflow run ${runId}\` to ` +
|
|
1283
|
+
`continue: re-driving the parent drives the resumed child.`);
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Complete a step `blocked` because the child workflow it composes is
|
|
1287
|
+
* blocked, and return the notes written (P3b §3.4). Sits beside
|
|
1288
|
+
* {@link blockStepForJudgeFailure} — the SAME shape of "infrastructure-like"
|
|
1289
|
+
* block: the step is completed `blocked`, and `akm workflow resume` is what
|
|
1290
|
+
* clears it (of the CHILD first, then the parent) rather than an automatic
|
|
1291
|
+
* in-step re-dispatch.
|
|
1292
|
+
*/
|
|
1293
|
+
export async function blockStepForChildWorkflow(input) {
|
|
1294
|
+
const notes = childWorkflowBlockedNotes(input.runId, input.stepId, input.childRunId, input.childRef, input.childStepId);
|
|
1295
|
+
await completeWorkflowStep({
|
|
1296
|
+
runId: input.runId,
|
|
1297
|
+
stepId: input.stepId,
|
|
1298
|
+
status: "blocked",
|
|
1299
|
+
notes,
|
|
1300
|
+
...(input.evidence !== undefined ? { evidence: input.evidence } : {}),
|
|
1301
|
+
...(input.leaseHolder !== undefined ? { leaseHolder: input.leaseHolder } : {}),
|
|
1302
|
+
});
|
|
1303
|
+
return notes;
|
|
1304
|
+
}
|
|
1305
|
+
/** The finalize path's child-blocked write: the executed units' evidence (the composing unit) is preserved. */
|
|
1306
|
+
async function blockFinalizedStepForChildWorkflow(input, childBlocked) {
|
|
1307
|
+
const summary = await blockStepForChildWorkflow({
|
|
1308
|
+
runId: input.runId,
|
|
1309
|
+
stepId: input.stepId,
|
|
1310
|
+
childRunId: childBlocked.childRunId,
|
|
1311
|
+
childRef: childBlocked.childRef,
|
|
1312
|
+
childStepId: childBlocked.childStepId,
|
|
1313
|
+
evidence: input.result.evidence,
|
|
1314
|
+
...(input.leaseHolder !== undefined ? { leaseHolder: input.leaseHolder } : {}),
|
|
1315
|
+
});
|
|
1316
|
+
return { kind: "child-blocked", summary };
|
|
1317
|
+
}
|
|
1186
1318
|
/**
|
|
1187
1319
|
* Perform ONE completion attempt for an executed step:
|
|
1188
1320
|
*
|
|
@@ -1210,6 +1342,12 @@ export async function finalizeExecutedStep(input) {
|
|
|
1210
1342
|
const { runId, workflowRef, stepId, stepPlan, completionCriteria, gateLoop, loopsRemaining, result } = input;
|
|
1211
1343
|
const lease = input.leaseHolder !== undefined ? { leaseHolder: input.leaseHolder } : {};
|
|
1212
1344
|
if (!result.ok) {
|
|
1345
|
+
// P3b §3.4: a composed child workflow that blocked is never fed into the
|
|
1346
|
+
// bounded gate loop — a gate is a gate for a child workflow too. Checked
|
|
1347
|
+
// FIRST, before the artifactSchemaFailure retry branch below.
|
|
1348
|
+
if (result.childBlocked) {
|
|
1349
|
+
return blockFinalizedStepForChildWorkflow(input, result.childBlocked);
|
|
1350
|
+
}
|
|
1213
1351
|
// Typed-artifact mismatch with loop budget left: regenerate-with-errors
|
|
1214
1352
|
// (the validation errors become the next loop's feedback). No judge ran, so
|
|
1215
1353
|
// no gate row is journaled for this attempt.
|
|
@@ -1290,10 +1428,14 @@ export async function finalizeExecutedStep(input) {
|
|
|
1290
1428
|
engine: engineName,
|
|
1291
1429
|
model: gateTarget.request.model?.resolved ?? null,
|
|
1292
1430
|
runner: gateTarget.runner.kind,
|
|
1431
|
+
// The gate prefix rides the unit prefix's version — unit and gate
|
|
1432
|
+
// hashVersion are one vocabulary (p3a §0.1; the R-R15 fix moved
|
|
1433
|
+
// both 6 → 7 in lockstep even though the gate preimage's own
|
|
1434
|
+
// fields are unchanged).
|
|
1293
1435
|
inputHash: createHash("sha256")
|
|
1294
|
-
.update("akm.workflow.gate\
|
|
1436
|
+
.update("akm.workflow.gate\0v7\0")
|
|
1295
1437
|
.update(canonicalJsonString({
|
|
1296
|
-
hashVersion:
|
|
1438
|
+
hashVersion: 7,
|
|
1297
1439
|
dispatch: gateTarget,
|
|
1298
1440
|
invocation: null,
|
|
1299
1441
|
prompt,
|
|
@@ -35,12 +35,71 @@ export function prepareWorkflowExecution(request, prompt = request.prompt) {
|
|
|
35
35
|
function message(err) {
|
|
36
36
|
return err instanceof Error ? err.message : String(err);
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* The `eventSource` value `dispatchWorkflowExecution` should forward into
|
|
40
|
+
* `dispatchLoweredExecutionRequest`'s options, or `undefined` to forward
|
|
41
|
+
* nothing.
|
|
42
|
+
*
|
|
43
|
+
* Precedence fix (P1b Lane C code review, round 2). The gap-fix originally
|
|
44
|
+
* forwarded `request.eventSource` unconditionally.
|
|
45
|
+
* `dispatchLoweredExecutionRequest` applies a forwarded value as `env: {
|
|
46
|
+
* ...lowered.options.env, AKM_EVENT_SOURCE: eventSource }`
|
|
47
|
+
* (execution-lowering.ts:998-1001) — an unconditional override of that one
|
|
48
|
+
* key — and `lowered.options.env` IS the unit's own authored/resolved `env:`
|
|
49
|
+
* binding (`request.env`, folded in by `prepareWorkflowExecution` above via
|
|
50
|
+
* `request.runtime.environment`), so the unconditional forward let the
|
|
51
|
+
* provenance stamp win over an authored `env: { AKM_EVENT_SOURCE: ... }`
|
|
52
|
+
* binding. That inverts the precedence pre-P1b had (the child env was built
|
|
53
|
+
* from ambient passthrough with `options.env` — the authored binding —
|
|
54
|
+
* applied AFTER it, at highest precedence, in `buildChildEnv`/`spawn.ts`) and
|
|
55
|
+
* disagrees with the sibling "script"/"shell" arm: `exec-unit.ts`'s own
|
|
56
|
+
* `childEnv` stamps its allowlisted base only when the name is absent there,
|
|
57
|
+
* strictly BEFORE the bindings overlay runs, so an authored binding always
|
|
58
|
+
* wins there. Gating the forward on `request.env` not already binding the
|
|
59
|
+
* name restores agreement: an authored binding leaves `eventSource`
|
|
60
|
+
* unforwarded (so the merge above never touches the key, and the authored
|
|
61
|
+
* value in `lowered.options.env` stands), while an absent binding still
|
|
62
|
+
* forwards the resolved value exactly as before.
|
|
63
|
+
*
|
|
64
|
+
* Exported so this precedence rule is pinned directly:
|
|
65
|
+
* `dispatchWorkflowExecution` itself has no injectable
|
|
66
|
+
* `runAgent`/`executeRunner`/`chat` seam to exercise the decision end-to-end
|
|
67
|
+
* without a live agent/LLM dispatch (see the P1b spec's Review log, which
|
|
68
|
+
* records the same constraint for the gap-fix this corrects).
|
|
69
|
+
*/
|
|
70
|
+
export function forwardedDispatchEventSource(request) {
|
|
71
|
+
if (request.eventSource === undefined)
|
|
72
|
+
return undefined;
|
|
73
|
+
if (request.env?.AKM_EVENT_SOURCE !== undefined)
|
|
74
|
+
return undefined;
|
|
75
|
+
return request.eventSource;
|
|
76
|
+
}
|
|
38
77
|
/**
|
|
39
78
|
* Dispatch one frozen workflow engine call through the common prepared/lowered
|
|
40
79
|
* seam. Credential materialization happens inside the final dispatch only.
|
|
41
80
|
*/
|
|
42
81
|
export async function dispatchWorkflowExecution(request, feedback) {
|
|
43
82
|
const prompt = feedback ? `${request.prompt}\n\n${feedback}` : request.prompt;
|
|
83
|
+
// B-N11 (P3b, spec docs/plans/specs/p3b-child-executor.md §1.6): an
|
|
84
|
+
// internal-invariant guard, not a user-facing one. `dispatchJournaledAttempt`
|
|
85
|
+
// (native-executor.ts, P3b §3.2) routes a `child-workflow` unit to the child
|
|
86
|
+
// executor (child-workflow.ts) BEFORE dispatch is ever reached, so arriving
|
|
87
|
+
// HERE with one means that seam was BYPASSED — an engine routing bug, never
|
|
88
|
+
// a not-yet-implemented feature (that premise, P3a Review log R8's, is gone
|
|
89
|
+
// now that P3b ships a production caller). A plain `Error` naming the seam
|
|
90
|
+
// that should have been reached instead, not a `UsageError`: nothing a user
|
|
91
|
+
// can author reaches this line once the seam exists, so there is no
|
|
92
|
+
// user-facing code to carry. Kept here, rather than deleted outright, so a
|
|
93
|
+
// bypassed seam still fails closed instead of falling into the generic
|
|
94
|
+
// `kind !== "command"` guard below, which would blame a legitimate target
|
|
95
|
+
// kind as "not a command target" — the exact false, unhelpful message R8
|
|
96
|
+
// was opened to remove.
|
|
97
|
+
if (request.frozenTarget.kind === "child-workflow") {
|
|
98
|
+
throw new Error(`unit ${JSON.stringify(request.unitId)} targets child workflow ${JSON.stringify(request.frozenTarget.ref)}, ` +
|
|
99
|
+
"but reached dispatchWorkflowExecution directly. The child-workflow dispatch seam " +
|
|
100
|
+
"(src/workflows/exec/child-workflow.ts) should have routed it before dispatch was ever reached — " +
|
|
101
|
+
"this is an engine routing bug, not a problem with the workflow itself.");
|
|
102
|
+
}
|
|
44
103
|
if (request.frozenTarget.kind !== "command") {
|
|
45
104
|
throw new ConfigError(`unit ${JSON.stringify(request.unitId)} is not a command target.`, "INVALID_CONFIG_FILE");
|
|
46
105
|
}
|
|
@@ -68,12 +127,25 @@ export async function dispatchWorkflowExecution(request, feedback) {
|
|
|
68
127
|
}
|
|
69
128
|
let result;
|
|
70
129
|
try {
|
|
130
|
+
const eventSource = forwardedDispatchEventSource(request);
|
|
71
131
|
result = await dispatchLoweredExecutionRequest(lowered, {
|
|
72
132
|
runOptions: {
|
|
73
133
|
stdio: "captured",
|
|
74
134
|
parseOutput: "text",
|
|
75
135
|
...(request.signal ? { signal: request.signal } : {}),
|
|
76
136
|
},
|
|
137
|
+
// Gap fix (P1b Lane C code review, spec §5.2(2)); precedence-gated
|
|
138
|
+
// (round 2, see forwardedDispatchEventSource above): forward the
|
|
139
|
+
// resolved provenance event source so an "agent"/"sdk" unit's
|
|
140
|
+
// dispatched child env carries AKM_EVENT_SOURCE too, not only a
|
|
141
|
+
// "script"/"shell" unit's — but only when the unit's own authored
|
|
142
|
+
// `env:` binding does not already set the name, so an authored binding
|
|
143
|
+
// still wins, mirroring exec-unit.ts's childEnv guard.
|
|
144
|
+
// dispatchLoweredExecutionRequest applies a forwarded value as exactly
|
|
145
|
+
// one child-env key (execution-lowering.ts:998-1001) — the same
|
|
146
|
+
// mechanism the R-07 command-arm fix (command-execution.ts) already
|
|
147
|
+
// uses.
|
|
148
|
+
...(eventSource !== undefined ? { eventSource } : {}),
|
|
77
149
|
});
|
|
78
150
|
}
|
|
79
151
|
catch (err) {
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
* The freeze-time child-output reference check (P3b, spec docs/plans/specs/
|
|
6
|
+
* p3b-child-executor.md §4.4, rows B-28…B-32).
|
|
7
|
+
*
|
|
8
|
+
* A parent step may read `steps.<child>.output(.<name>)*` where step
|
|
9
|
+
* `<child>`'s frozen target is `kind: "child-workflow"`. Only the FIRST path
|
|
10
|
+
* segment is a freeze-time concern: it must name one of the child's declared
|
|
11
|
+
* `outputs:` names, or — when the child declares none — `runId` or `status`
|
|
12
|
+
* (the default `workflowRunExportedResult` shape, `runtime/run-outputs.ts`).
|
|
13
|
+
* A reference AT `steps.<child>.output` with no further segment always
|
|
14
|
+
* accepts — it names the whole exported object. Anything deeper resolves (and,
|
|
15
|
+
* if wrong, fails) at pre-attempt through the existing, unchanged resolver —
|
|
16
|
+
* the value's shape past the first segment is unconstrained unless the
|
|
17
|
+
* output declares a `schema:`.
|
|
18
|
+
*
|
|
19
|
+
* Pure over the frozen step list — no IO, no config.
|
|
20
|
+
*/
|
|
21
|
+
import { UsageError } from "../../core/errors.js";
|
|
22
|
+
import { formatReference, parseReference } from "../program/expressions.js";
|
|
23
|
+
/** The frozen target a step's root unit (or map template) dispatches, if any. */
|
|
24
|
+
function stepFrozenTarget(step) {
|
|
25
|
+
const root = step?.root;
|
|
26
|
+
if (!root)
|
|
27
|
+
return undefined;
|
|
28
|
+
const unit = root.kind === "map" ? root.template : root;
|
|
29
|
+
return unit.frozenTarget;
|
|
30
|
+
}
|
|
31
|
+
/** Every reference string a step's `inputs[]`, `map.over`, `route.input`, and reference-kind `inputBindings[].from` carry. */
|
|
32
|
+
function collectReferenceSites(step) {
|
|
33
|
+
const sites = [];
|
|
34
|
+
const push = (reference) => {
|
|
35
|
+
sites.push({ stepId: step.stepId, reference });
|
|
36
|
+
};
|
|
37
|
+
if (step.route)
|
|
38
|
+
push(step.route.input);
|
|
39
|
+
const root = step.root;
|
|
40
|
+
if (root) {
|
|
41
|
+
const unit = root.kind === "map" ? root.template : root;
|
|
42
|
+
if (root.kind === "map")
|
|
43
|
+
push(root.over);
|
|
44
|
+
for (const reference of unit.inputs ?? [])
|
|
45
|
+
push(reference);
|
|
46
|
+
const bindings = unit.frozenTarget.inputBindings ?? [];
|
|
47
|
+
for (const binding of bindings) {
|
|
48
|
+
if (binding.kind === "reference")
|
|
49
|
+
push(binding.from);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return sites;
|
|
53
|
+
}
|
|
54
|
+
/** The names a `child-workflow` target's own exported result carries at its first segment. */
|
|
55
|
+
function acceptedFirstSegments(target) {
|
|
56
|
+
const declared = target.frozenPlan.outputs;
|
|
57
|
+
return declared ? Object.keys(declared) : ["runId", "status"];
|
|
58
|
+
}
|
|
59
|
+
function exportsDescription(target) {
|
|
60
|
+
const declared = target.frozenPlan.outputs;
|
|
61
|
+
return declared ? `outputs: ${Object.keys(declared).join(", ")}` : "only {runId, status} — it declares no `outputs:`";
|
|
62
|
+
}
|
|
63
|
+
function checkSite(site, stepsById) {
|
|
64
|
+
const parsed = parseReference(site.reference);
|
|
65
|
+
if (!parsed.ok || parsed.expr.kind !== "stepOutput")
|
|
66
|
+
return;
|
|
67
|
+
const targetStep = stepsById.get(parsed.expr.stepId);
|
|
68
|
+
const frozenTarget = stepFrozenTarget(targetStep);
|
|
69
|
+
if (!frozenTarget || frozenTarget.kind !== "child-workflow")
|
|
70
|
+
return;
|
|
71
|
+
const first = parsed.expr.path[0];
|
|
72
|
+
if (first === undefined)
|
|
73
|
+
return; // bare steps.<child>.output — the whole exported object, always accepted.
|
|
74
|
+
const accepted = acceptedFirstSegments(frozenTarget);
|
|
75
|
+
if (typeof first === "string" && accepted.includes(first))
|
|
76
|
+
return;
|
|
77
|
+
throw new UsageError(`Workflow step ${site.stepId} reads "${formatReference(parsed.expr)}", but child workflow ${frozenTarget.ref} ` +
|
|
78
|
+
`exports ${exportsDescription(frozenTarget)}. Declare the output in the child's \`outputs:\` frontmatter, or ` +
|
|
79
|
+
`reference one of the names above.`, "COMPOSITION_INVALID", `Fix the reference to name one of: ${accepted.join(", ")} — or add the missing name to ${frozenTarget.ref}'s ` +
|
|
80
|
+
"own `outputs:` frontmatter.");
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Assert every reference into a `child-workflow`-targeted step's output
|
|
84
|
+
* names an output the child actually exports. Throws `UsageError`
|
|
85
|
+
* (`COMPOSITION_INVALID`) on the first violation found.
|
|
86
|
+
*/
|
|
87
|
+
export function assertChildOutputReferences(steps) {
|
|
88
|
+
const stepsById = new Map(steps.map((step) => [step.stepId, step]));
|
|
89
|
+
for (const step of steps) {
|
|
90
|
+
for (const site of collectReferenceSites(step)) {
|
|
91
|
+
checkSite(site, stepsById);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|