feat: workflow-program IR Phase 1 — typed params, guards, wait_until (additive, back-compatible)#71
Merged
Merged
Conversation
…(additive, back-compatible) Implements the RFC's Phase 1 (docs/design/WORKFLOW_PROGRAM_IR.md): the first additive, backward-compatible step from a linear macro IR toward a parameterized program. Typed parameters on Workflow (substituted at replay), an optional per-step guard (deterministic precondition; fail-safe), and wait_until (bounded readiness predicate that subsumes the SCROLL closed-loop). A bundle with none of these replays byte-identically to today. $0 / zero model calls. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CKrVJJy5jWVCkXAqgUqtqZ
…-ir-phase1 # Conflicts: # openadapt_flow/compiler/compile.py # openadapt_flow/ir.py
abrichr
added a commit
that referenced
this pull request
Jul 13, 2026
…ons → guards/params (ask, don't guess) (#74) * feat: workflow-program IR Phase 1 — typed params, guards, wait_until (additive, back-compatible) Implements the RFC's Phase 1 (docs/design/WORKFLOW_PROGRAM_IR.md): the first additive, backward-compatible step from a linear macro IR toward a parameterized program. Typed parameters on Workflow (substituted at replay), an optional per-step guard (deterministic precondition; fail-safe), and wait_until (bounded readiness predicate that subsumes the SCROLL closed-loop). A bundle with none of these replays byte-identically to today. $0 / zero model calls. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CKrVJJy5jWVCkXAqgUqtqZ * feat: interactive disambiguation — Socrates-style compile-time questions → guards/params (ask, don't guess) Implements the RFC (docs/design/WORKFLOW_PROGRAM_IR.md §3 [3]) induction stage: where a single demonstration under-specifies intent, surface CONCRETE multiple-choice questions and apply the answer deterministically as a Phase-1 guard/param — instead of silently freezing an accidental interpretation. New module openadapt_flow/compiler/disambiguation.py detects three ambiguity kinds structurally (ZERO model calls): - parameter candidate — an untagged typed value → ParamSpec + param binding - absent-result handling — an identity-armed entity selection after a search with no 0/>1-match branch → Guard(ANCHOR_RESOLVES, on_unmet="halt") - optional dialog — a once-handled popup → Guard(TEXT_PRESENT, on_unmet="skip") Answers map to #71's Guard/Predicate/ParamSpec types verbatim (no new IR fields). Refuse-rather-than-guess (mirrors runtime.identity): an UNANSWERED consequential ambiguity (one gating an irreversible write) is flagged and the resolved skill is marked NOT certified until answered — never silently defaulted. Non-consequential unanswered ambiguities fall back to a safe no-op default. Core is a pure, testable API — detect_ambiguities(workflow) and apply_answers(workflow, answers) — plus a thin `disambiguate` CLI subcommand (interactive prompt or --answers JSON). compile.py is UNCHANGED; disambiguation is an opt-in pass over a compiled bundle. Stacks on #71 (feat/workflow-program-ir-phase1); retarget base to main after #71 merges. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CKrVJJy5jWVCkXAqgUqtqZ --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements Phase 1 of the merged workflow-program RFC (
docs/design/WORKFLOW_PROGRAM_IR.md§6) — the additive, backward-compatible first step from the linear macro IR toward a parameterized program. "A demonstration is evidence, not a specification." This is the compiler-vs-recorder gap both external reviews flagged.What ships (all additive & optional — a bundle with none of these replays byte-identically to today; $0 / zero model calls)
Workflow.param_specs: dict[str, ParamSpec]alongside the frozenWorkflow.params: dict[str, str](NOT a replacement — back-compat).ParamSpeccarriestype(string/date/enum/number/entity_ref),example(the recorded demo value, also the replay default),required, and enumchoices. At replay each spec'sexamplefolds in as a default; caller-supplied values win; a required spec with neither a value nor an example → fail fast at run start naming it (before any step executes).Step.wait_until: Optional[Predicate]— a bounded readiness predicate the replayer polls before proceeding; timeout → HALT (never proceed-anyway). Subsumes the existing SCROLL closed-loop: a SCROLL step's default readiness is now theANCHOR_RESOLVESpredicate on the next anchored step's anchor — today's closed loop expressed as the first concrete predicate rather than a special case. An explicitwait_untilon a SCROLL overrides that default.Step.guard: Optional[Guard]— a deterministic precondition over the entry frame. Semantics chosen (RFC left the single-guard skip-vs-halt open):Guard.on_unmetdefaults to"halt"(the safe direction for an unmet precondition, per the repo's refuse-rather-than-guess posture);"skip"turns the step into a no-op success (the expected-but-optional case, e.g. dismiss a survey modal only when present — a guarded branch without the Phase-2 state machine). Full multi-way branches/loops/subflows are Phase 2, intentionally not built here.Predicatekinds (model-free, evaluated byreplayer._predicate_holds):anchor_resolves,text_present,text_absent,param_equals, andand/or/notcomposition.worklist_nonempty(loops) is deferred to Phase 2.Orthogonality / concurrency (this PR merges LAST, after #64 secret / #69 StructuralLocator / #70 healing / #66 effects)
New IR fields are independent additive fields —
params/param_specs,wait_until,guarddo not restructureStep/Workflowand are orthogonal toStep.secret(#64),Step.effects(#66),Step.risk, andStructuralLocator(#69). Reconcile by keeping both sets of fields.Exact touch-points for reconciliation
openadapt_flow/ir.py(+148): new classesParamKind,ParamSpec,PredicateKind,Predicate,Guard(defined beforeStep;Predicate.model_rebuild()for the self-referentialoperands). New optional fields:Step.wait_until,Step.guard;Workflow.param_specs;StepResult.skipped. No existing field changed;schema_versionleft at 1 deliberately (the fields load additively regardless of version, and keeping it unbumped preserves byte-identical output for featureless bundles — bump can be a follow-up).openadapt_flow/runtime/replayer.py(+284, localized):run()param merge now foldsparam_specsexamples in as defaults + fail-fast required-param check._run_step()calls the new_apply_step_gates()(guard then wait_until) before resolve/act, early-returning on skip/halt. New methods_apply_step_gates,_predicate_holds,_describe_predicate._act_scroll()re-expressed to poll await_untilpredicate as its stop condition (default = next-anchorANCHOR_RESOLVES); the old_probe_anchorwas folded into_predicate_holdsand removed._act()/scroll call threadsparams. Identity ladder, resolution ladder, effects, postconditions, heal all untouched.openadapt_flow/compiler/compile.py(+14): emits aParamSpec(type=string, example=<demo value>)per recorded param alongsideparams(generalizes the recorder's single note value). No other compiler behavior changed.tests/test_program_ir_phase1.py(new, +336, 12 tests): worked add-patient-note-style example (typed param + wait_until + guard) round-tripping through bundle save/load; param defaulted-from-example and caller-override at replay; missing required param → fail fast naming it; wait_until holds→proceed and timeout→HALT; guard halt-default, skip-noop, and met-executes; wait_until subsumes SCROLL (explicit predicate + default next-anchor); a no-fields bundle replaying identically.Tests
Full suite green: 1008 passed, 16 skipped (clean py3.12 venv,
pip install -e '.[dev]'). The 12 new Phase-1 tests + all existing replayer/compiler/emit tests pass unchanged (back-compat).🤖 Generated with Claude Code
https://claude.ai/code/session_01CKrVJJy5jWVCkXAqgUqtqZ