UNPKG

akm-cli

Version:

akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur

89 lines (77 loc) • 4.69 kB
# 0008 — Task input binding normalization and re-binding across a composition ## Context P2b gave a task-composing workflow step a `with:` record that freezes into typed `TaskInputBinding[]` against the target's declared `inputs:` contract. P3a then made workflows composable — a task-wrapped step can target a child WORKFLOW rather than a plain task — which raises a second problem `freezeTaskInputBindings` alone does not solve: a v4 task's own already-classified bindings (literal or reference) need to be re-expressed against the CHILD workflow's declared `params:` contract, by name, without re-deriving each entry's classification from its value's shape a second time. ## Decision Moved verbatim from `src/workflows/freeze/task-bindings.ts`'s module header: > The pure `with:` -> `TaskInputBinding[]` normalizer (spec > docs/plans/specs/p2b-input-bindings.md §3.2 A2, §3.3), plus the pure > re-binder a recursive composition needs on top of it (spec > docs/plans/specs/p3a-plan-v5-child-freeze.md A-N8). > > `freezeTaskInputBindings` classifies a genuinely AUTHORED `with:` record > against a contract; `src/workflows/freeze/targets/task.ts`'s > `taskDispatch` calls it with THIS step's own authored `with:` against the > composed task's own `InputContract`, never anything from an outer > composition (§3.5 — there is no merge across a composition chain). > `rebindTaskInputBindings` instead takes an ALREADY-classified > `TaskInputBinding[]` — a v4 task's own effective inputs — and re-binds it > by name against a DIFFERENT contract, trusting each entry's existing > `kind` rather than re-deriving it from the value's shape; > `src/workflows/freeze/targets/child-workflow.ts`'s `childWorkflowDispatch` > is its only caller, re-binding a task-wrapped composition's effective > inputs against the child workflow's declared `params:`. > > Everything decidable at FREEZE time is decided here: an unknown `with:` > key, a missing required-without-default input, a literal value failing > its declared schema, and a reference's syntax + "names an earlier step > that exists" / "names a declared workflow param" structural check. A > reference's *resolved* value is validated PRE-ATTEMPT instead > (`src/workflows/exec/step-work.ts`, §3.6) — this module never resolves > anything, so it needs no run params and no step outputs. > > Pure function: no IO, no config reads. Imports only > `src/execution/input-contract.ts`, `src/workflows/program/expressions.ts`, > and `src/core/errors.ts`. And from `rebindTaskInputBindings`'s own doc comment — the part explaining WHY re-binding cannot simply reuse `freezeTaskInputBindings`: > This exists because a `kind: "literal"` binding's VALUE can be an > arbitrary JSON value, including one shaped exactly like the `{from: > "<ref>"}` reference grammar (e.g. a declared default for an object-typed > input). Round-tripping such a binding back through a `with:`-shaped > record and re-running `freezeTaskInputBindings`'s value-shape-driven > classification (`normalizeOneEntry`) would silently reinterpret that > literal as a live reference binding — the composing task's OWN contract > already settled `kind` once; this function trusts that answer instead of > asking the value's shape again (code-review finding, docs/plans/specs/ > p3a-plan-v5-child-freeze.md). ## Consequences - `freezeTaskInputBindings` and `rebindTaskInputBindings` must never be merged into one shape-sniffing function, even though they look similar — doing so would reintroduce exactly the silent-reinterpretation bug the code-review finding above caught: a literal value that happens to look like `{from: "..."}` would flip from literal to reference on its second pass through composition. - A reference binding's `from` target (an earlier step or declared param) never changes across a re-bind — only which CONTRACT it validates against changes. A future caller that tries to re-target a reference's `from` during re-binding is outside this module's contract and needs new reasoning, not an extension of `rebindTaskInputBindings`. - Both functions stay pure and IO-free by construction — a caller that needs a resolved value (not just a structurally-validated binding) is looking for `step-work.ts`'s pre-attempt resolution instead, not a change to this module. ## Provenance - Source: `src/workflows/freeze/task-bindings.ts`, module header and `rebindTaskInputBindings`'s doc comment (code-review finding). - Spec: `docs/plans/specs/p2b-input-bindings.md` §3.2 A2, §3.3; `docs/plans/specs/p3a-plan-v5-child-freeze.md` A-N8. - Extracted: P4 (`docs/plans/specs/p4-deletions-closeout.md` §4.2), 2026-08-27.