Skip to content

feat(attended): give the bridge a brake, and say when a model pulled it - #22

Draft
abrichr wants to merge 2 commits into
mainfrom
feat/attended-reject-and-decider
Draft

feat(attended): give the bridge a brake, and say when a model pulled it#22
abrichr wants to merge 2 commits into
mainfrom
feat/attended-reject-and-decider

Conversation

@abrichr

@abrichr abrichr commented Jul 28, 2026

Copy link
Copy Markdown
Member

DRAFT — blocked on an openadapt-flow release. CI installs Flow from PyPI, and this needs decided_by + reject, which are on main (openadapt-flow#295, #301) but unreleased. The floor here is >=1.26.0, so install will fail until that release exists. Verified locally against an editable build of Flow main: 16 passed. Un-draft after the release that batches openadapt-flow #294/#296/#301.

Two changes, and they are only sound together.

1. The bridge could say "proceed" but not "stop"

openadapt-agent exposes attended answers as model-callable MCP tools. It offers continue_attention and skip_attention whenever a deployment-bound live executor is configured — and continue resumes the run, which can write to the system of record. It had no way to say stop.

That asymmetry is the decisive fact. Withholding reject did not make the configuration safer; the only thing it removed was the brake.

reject_attention is deliberately not gated on live_actions_ready. That gate exists because continue and skip need the 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 — and the schema stays closed with no free-text property.

2. decided_by="automation" on every decision, not only reject

This is the half that makes the first half safe, and it is the larger finding.

operator is 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.

Mutation Fails
Revert all bridge production code 5 tests, including all 4 new ones
decided_by="automation""human" the two provenance tests + the refusal fake's assertion
reject_attention gated behind / removed from the no-executor set the gating test, the end-to-end reject test, the schema test

Notes

  • One stale expectation 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 and never adds, removes, or reorders an action.
  • Three test doubles that implement execute gain the defaulted keyword. Two now record the value so a test can pin that the bridge declares itself; a fake that silently accepted unknown would hide a bridge that stopped declaring.
  • No free text is added anywhere. rejected_by_operator is a closed enum member, and the tool schema has no unconstrained string property.

Local verification

tests/test_attended_bridge.py: 16 passed against an editable openadapt-flow at main. ruff check src tests: clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM

abrichr and others added 2 commits July 27, 2026 23:29
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
@abrichr

abrichr commented Jul 28, 2026

Copy link
Copy Markdown
Member Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant