feat(attended): give the bridge a brake, and say when a model pulled it - #22
Conversation
Two changes that are only sound together. 1. `reject_attention`. Flow gained `reject` -- an explicit "stop, this is wrong" that TERMINATES a run. This bridge could already answer `continue`, which resumes the run and can write to the system of record, but had no way to say stop. A configuration that can say "proceed" and cannot say "stop" is not the safer one: the only thing withholding the action removes is the brake. It is NOT gated on `live_actions_ready`. That gate exists because continue and skip need Flow's deployment-bound executor to re-read the application and act on it. Rejecting actuates nothing and resumes nothing, so it has nothing to gate on -- the same reason Flow's own `_allowed_actions` offers it at a pause carrying no resolvable action step at all. The tool description states the two distinctions a model would otherwise collapse: escalate PARKS the run for a colleague, teach changes FUTURE runs, reject ends THIS one and asserts nothing about the saved workflow. Its confirmation flag must be explicitly true; ending a run is not something to do by omission. The schema stays closed with no free-text property. 2. `decided_by="automation"` on EVERY decision this bridge submits, not only on reject. This is the half that makes the first half safe. `operator` is derived from the same `_local_operator_identity()` a person's own console uses, so a model's answer and a person's answer from one machine were indistinguishable in Flow's journal, and any agreement rate computed over it silently mixed the two populations. The bias this fixes was always mostly about `continue`, which has been model-callable all along -- attaching provenance only to the action added last would leave the population that actually matters unlabelled. Measurement integrity does not require withholding the action. It requires knowing who decided. Those are different problems, and only the second one is real here. The `openadapt-flow` floor rises to 1.26.0: `reject`, its `rejected_by_operator` disposition, and the `decided_by` keyword are named symbols that only exist there. A resolver satisfied by 1.25.x fails at CALL time with a TypeError, inside the attended decision path. One stale expectation is corrected: the capability's `allowed_actions` now contains `reject` because the ENGINE offers it, not because this bridge adds one. The bridge still relays Flow's signed set verbatim. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM
The new reject/provenance cases introduced the only formatting drift in this file; main was clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM
|
CI diagnosis (2026-07-28): all four red jobs fail during dependency installation because this branch correctly requires openadapt-flow 1.26 or newer, while PyPI currently stops at 1.25.1. CodeQL, dependency review, and secret scan pass; the focused Agent tests pass against the local Flow decider branch. Do not lower the dependency floor: current Flow main does not yet expose the decided_by API, so that would turn an installation refusal into a runtime incompatibility. Exit condition: merge the reviewed Flow decider contract into the coordinated Flow 1.26 release, publish 1.26, rebase this PR once onto current Agent main, then run one authoritative matrix. |
Independent safety review — changes requiredExact head: GitHub does not let the PR author account submit a formal Request Changes review on its own PR. This comment records the same review result. The new head fixes the transport defect I reproduced at
The core Flow binding is sound at the declared floor. Flow 1.26.0 defines the closed Reject action and disposition, trusted decider provenance, exact signed capability validation, durable namespace checks, uncertain-delivery refusal, and terminal rejection. The bridge retains the opaque queue ID, exact capability digest, allowed-action set, stable idempotency key, resolved run path, and server-derived local OS identity. It marks both official-service and direct-Flow decisions as automation. Verification: the prior exact head |
Two changes, and they are only sound together.
1. The bridge could say "proceed" but not "stop"
openadapt-agentexposes attended answers as model-callable MCP tools. It offerscontinue_attentionandskip_attentionwhenever a deployment-bound live executor is configured — andcontinueresumes the run, which can write to the system of record. It had no way to say stop.That asymmetry is the decisive fact. Withholding
rejectdid not make the configuration safer; the only thing it removed was the brake.reject_attentionis deliberately not gated onlive_actions_ready. That gate exists because continue and skip need the executor to re-read the application and act on it. Reject dispatches no new application action and resumes nothing, so it has nothing to gate on — the same reason Flow's own_allowed_actionsoffers it at a pause carrying no resolvable action step at all. Earlier run actions can still have effects. The public response directs the caller to the protected local report and transaction outcome.The tool description states the two distinctions a model would otherwise collapse:
escalateparks the run for a colleague,teachchanges future runs,rejectends this one and asserts nothing about the saved workflow. Its confirmation flag must be explicitlytrue— ending a run is not something to do by omission — and the schema stays closed with no free-text property.2.
decided_by="automation"on every decision, not only rejectThis is the half that makes the first half safe, and it is the larger finding.
operatoris derived from the same_local_operator_identity()a person's own console uses. So a decision a model submitted and one a person submitted from the same machine carried an identical operator string in Flow's journal. Any agreement rate computed over it silently mixed the two populations, with no way to separate them afterwards.Measurement integrity does not require withholding the action. It requires knowing who decided. Those are different problems, and only the second one was real.
The bias was always mostly about
continue, which has been model-callable all along. Attaching provenance only to the action added last would have left the population that actually matters unlabelled — so every decision this bridge submits declares itself.Mutation proof
Each mutation is caught by the tests written for it.
decided_by="automation"→"human"reject_attentiongated behind / removed from the no-executor setreject_attentionwithdestructiveHint=falsetools/listannotation testsuv lock --lockedand the exact-floor test legNotes
allowed_actionsnow containsrejectbecause the engine offers it, not because this bridge adds one. The bridge still relays Flow's signed set verbatim and never adds, removes, or reorders an action.executegain the defaulted keyword. Two now record the value so a test can pin that the bridge declares itself; a fake that silently acceptedunknownwould hide a bridge that stopped declaring.rejected_by_operatoris a closed enum member, and the tool schema has no unconstrained string property.llms.txt,server.json,manifest.json, design/distribution docs, CLI/MCP instructions, and the emitted Agent Skill plus its golden file now describe Reject and its earlier-effect review requirement.>=1.26,<2and resolves Flow 1.31.0. CI runsuv lock --lockedand samples both dependency edges without adding jobs: Python 3.10 installs Flow 1.26.0 and MCP 1.28.0; Python 3.11 and 3.12 install the newest allowed releases.Exact-head verification
At exact head
b634d18:uv lock --locked, Ruff, formatting forsrcandtests, andgit diff --checkpassed.🤖 Generated with Claude Code
https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM