Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/flow/docs/AFTER_A_STOP.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,9 @@ from openadapt_flow.after_stop import approve_undo, offer_undos, run_undo

offers = offer_undos(run_dir, workflow)
approve_undo(run_dir, workflow, "undo-triage", approver="nurse-a")
result = run_undo(run_dir, workflow, "undo-triage", replayer=replayer, bundle_dir=bundle)
result = run_undo(
run_dir, workflow, "undo-triage", replayer=replayer, bundle_dir=bundle
)
assert result.status == "verified"
```

Expand Down
6 changes: 4 additions & 2 deletions packages/flow/docs/CODE_AS_ACTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ result = compile_first_run(
"Open the referral for Jane Sample, start a Triage encounter, and save this note: {note}",
params={"note": "Follow up in two weeks"},
agent=ClaudeAgent(vision="code", model="claude-sonnet-5-5"),
url="http://localhost:8080/#tasks", verify="verify.yaml",
expect={"patient_id": "p1"}, on_screen=["Jane Sample", "MRN P1"],
url="http://localhost:8080/#tasks",
verify="verify.yaml",
expect={"patient_id": "p1"},
on_screen=["Jane Sample", "MRN P1"],
)
```

Expand Down
12 changes: 6 additions & 6 deletions packages/flow/docs/COMPILE_AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,17 +186,17 @@ result = compile_first_run(
on_screen=["Jane Sample", "MRN P1"],
agent=ClaudeAgent(vision="selective"),
model_key=load_customer_model_key("anthropic"),
confirm_checks="dana@clinic.example", # or a callback that shows them to a person
confirm_checks="dana@clinic.example", # or a callback that shows them to a person
drop_fields=["key"],
canary_params=[{"note": "Canary test note"}], # a real save; choose safe values
admitter=agent_principal, # principal_from_key(...)
admitter=agent_principal, # principal_from_key(...)
)
result.status # "admitted"
result.run1["agent"] # model calls, tokens, cost, seconds
result.emitted # {"skill": ..., "mcp_server": ..., "cli": ...}
result.status # "admitted"
result.run1["agent"] # model calls, tokens, cost, seconds
result.emitted # {"skill": ..., "mcp_server": ..., "cli": ...}

later = run_compiled(result.dir, note="Recheck knee in ten days")
later.verified, later.model_calls, later.receipt_outcome # True, 0, "VERIFIED"
later.verified, later.model_calls, later.receipt_outcome # True, 0, "VERIFIED"
```

`FirstRunCompiler` exposes the same steps one at a time (`record`,
Expand Down
14 changes: 8 additions & 6 deletions packages/flow/docs/GUARDED_SAVE.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,19 @@ guarded = GuardedSave.from_file("verify.yaml", "runs/guarded", approval="each_it
task = load_task("tasks/referral-task-01.json")

try:
guard = guarded.begin(task, screen_text=harness.open_record_text(), agent_id="cua-01")
guard = guarded.begin(
task, screen_text=harness.open_record_text(), agent_id="cua-01"
)
step = guard.commit(screenshot_png=harness.screenshot())
while step.status == "waiting_for_approval":
step = guard.commit(wait_s=30) # a person approves on Desktop or a phone
step = guard.commit(wait_s=30) # a person approves on Desktop or a phone
except GuardRefused as refused:
stop(refused.code) # nothing was written
stop(refused.code) # nothing was written

harness.click_save() # once
harness.click_save() # once
result = guard.confirm()
result.verdict # GuardVerdict.VERIFIED, REFUTED, or RECONCILIATION_REQUIRED
result.receipt_path # runs/guarded/<guard_id>/effect-receipt.json
result.verdict # GuardVerdict.VERIFIED, REFUTED, or RECONCILIATION_REQUIRED
result.receipt_path # runs/guarded/<guard_id>/effect-receipt.json
```

- `begin(task, screen_text=..., agent_id=...)` checks identity and reads the
Expand Down
8 changes: 6 additions & 2 deletions packages/flow/docs/HARNESS2.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,12 @@ from openadapt_flow.integrations.harness2.tasks import mockmed_tasks
adapter = OpenAdaptGuiAdapter(
mockmed_tasks(("lookalike",)), ScriptedGuiAgent(), Path("runs"), evidence="readback"
)
prop = ScriptedProposer(ChannelSpec("claude_cli", "scripted"), audit_dir=Path("runs/audit"))
summary = run_task("mockmed/lookalike", 1, adapter=adapter, prop=prop, work_dir=Path("runs/work"))
prop = ScriptedProposer(
ChannelSpec("claude_cli", "scripted"), audit_dir=Path("runs/audit")
)
summary = run_task(
"mockmed/lookalike", 1, adapter=adapter, prop=prop, work_dir=Path("runs/work")
)
print(summary["chosen_score"]["outcome"]) # verified
```

Expand Down
33 changes: 24 additions & 9 deletions packages/flow/docs/JIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ the system of record before the run reports `VERIFIED`.

```python
from openadapt_flow.jit import (
TaskSignature, WebApp, compile_agent, load_customer_model_key,
TaskSignature,
WebApp,
compile_agent,
load_customer_model_key,
)
from openadapt_flow.runtime.effects import RestRecordVerifier

Expand All @@ -29,11 +32,13 @@ jit = compile_agent(
model_key=load_customer_model_key("anthropic"), # your key, not OpenAdapt's
)

first = jit.run(note="Follow up in two weeks") # status: needs_human_admit (DRAFT)
confirmed = jit.confirm_params(principal_id="dana@clinic.example",
accept_params=["note"], confirm="ok")
jit.review(principal_id="dana@clinic.example", reviewed_steps=confirmed["steps"],
confirm="ok")
first = jit.run(note="Follow up in two weeks") # status: needs_human_admit (DRAFT)
confirmed = jit.confirm_params(
principal_id="dana@clinic.example", accept_params=["note"], confirm="ok"
)
jit.review(
principal_id="dana@clinic.example", reviewed_steps=confirmed["steps"], confirm="ok"
)
jit.canary(principal_id="dana@clinic.example", note="Canary test note")
jit.admit(principal_id="dana@clinic.example", confirm="ok") # at a terminal
# Headless (CI): jit.admit_with_key(principal_from_key(load_encrypted_key(...), key_dir))
Expand Down Expand Up @@ -182,8 +187,14 @@ To keep the loop in your own code, leave out the agent and use
`recording()` as a context manager:

```python
jit = compile_agent(task=..., app=..., verifier=..., policy="clinical-write",
dialect="anthropic", model_key=load_customer_model_key("anthropic"))
jit = compile_agent(
task=...,
app=...,
verifier=...,
policy="clinical-write",
dialect="anthropic",
model_key=load_customer_model_key("anthropic"),
)
with jit.recording(note="Follow up in two weeks") as rec:
my_cua_loop(rec.request.instruction, rec.computer.execute)
print(rec.result.status) # needs_human_admit, unsupported, or capture_failed
Expand Down Expand Up @@ -294,7 +305,11 @@ exact values the save may write, and any other text it may type.
from openadapt_flow.mockmed.teach_scope import encounter_scope

jit = compile_agent(
agent, task=task, app=app, verifier=verifier, policy="clinical-write",
agent,
task=task,
app=app,
verifier=verifier,
policy="clinical-write",
teach_scope=lambda params: encounter_scope(
base_url, patient_id="p1", note=params["note"]
),
Expand Down
4 changes: 3 additions & 1 deletion packages/flow/docs/JIT_ADMISSION.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ The agent then runs canaries and admits, with its own key:

```python
from openadapt_flow.jit.authority import (
SignedAdmissionPolicy, load_encrypted_key, principal_from_key,
SignedAdmissionPolicy,
load_encrypted_key,
principal_from_key,
)

agent = principal_from_key(
Expand Down
4 changes: 2 additions & 2 deletions packages/flow/docs/L1_INTEGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ the happy path.
from openadapt_flow.emit.l1_artifact import emit_l1_artifact

ref = emit_l1_artifact(
downloaded_pdf, # produced by a replayed workflow step
extraction_dir, # the root the L2 layer watches
downloaded_pdf, # produced by a replayed workflow step
extraction_dir, # the root the L2 layer watches
file_number="P1",
date="2026-07-06",
doctype="referral",
Expand Down
93 changes: 93 additions & 0 deletions packages/flow/docs/LIMITS.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,99 @@ The binding ties the click to the entry the run made. It doesn't check that
the entry is right: the values typed come from the run's parameters, and only
a record check can confirm what the application saved.

A labelled save on a create-new form (Trac's **Create ticket**, NetBox's
**Create**) has the same problem: no existing record is on screen next to it,
so it compiled with no identity check, and a policy that refuses unarmed saves
refused it. Its record is the one the run is typing into the form. When the
recording shows the save is the submit button of the one form that owns every
field typed since the last save, the compiler gives it a **form identity**
(`Anchor.form_identity`). Before the click, a run requires (identity mode
`form`):

- the button is still that form's submit button, and the form owns exactly the
fields this run typed into, each holding exactly what the run typed;
- the form submits to the same origin, path and query parameter names with the
same method, and would send the same set of field names and types, hidden
and file ones included, so an edit form that looks like the create form, or
an address, method or record id a script changed or added, halts;
- every field the run did not type into (a hidden parent or record id, a
select left on its default, a checked box, a query parameter's value) holds
its demonstrated value, stored as a salted digest. Only a per-page token is
not compared: a hidden field or query value named like one (`csrf`, `token`,
`nonce` and the like) whose value is shaped like one (16 characters or more,
not all digits) and is not shaped like a record id (hex groups joined by `-`
or `_`, as a UUID is) unless it changed while the demonstration was filled
in. A short, numeric or record-shaped value under such a name, and a value a
script set while the form was filled in, are compared. A program that
should create under a different parent on each run therefore halts until
that parent is checked another way.

The checks above read the page before the click; they are pre-checks. The
safety layer is the wire check (`openadapt_flow/wire_check.py`, shared with
taught browser writes): from the check until the next step, a Playwright route
on the browser context holds every write request from any of its pages (a
popup or new tab the click opens included), and a GET navigation to the
form's address, at the network, where page scripts can't reach it. A request
leaves only when its method, address, query and fields equal what the checked
button would send (form fields or a flat JSON object, every value except the
exempt tokens), and only once; anything else is aborted before it is sent. So
a page listener that runs after the check (a submit, capture or `formdata`
listener, a click handler that fills in or enables a field, a timer), a save
sent by script (`form.submit()`, `fetch`), a write from a popup, a submit from
another form and a second submit can't send anything the check didn't see. A
save sent in another shape (nested JSON, JSON with a repeated key, a custom
encoding) can't be compared, so it is held too. The run watches the wire check
for two seconds after the click, then again at the start of the next step and
at the end of the run, and halts on any held request.

What the wire check doesn't see:

- **Service workers.** Their requests skip routes. The runner's browsers block
service workers; on a page one controls anyway, the save is refused before
the click, and one that starts while the save is held halts the run.
- **WebSockets.** One opened while the save is held is never connected to the
server. A message sent on one opened earlier can't be stopped: the run
halts on it, after it was sent. A socket that a save opens just as the page
navigates away closes with the page; it isn't connected, and the run may
not report it.
- **Redirects.** A 307 or 308 answer to the checked save makes the browser send
the same request again to the address the server names, possibly another
site. The server that accepted the save chooses that.
- **GET requests.** Reads pass. An app whose GET addresses change records
(some do, OpenEMR among them) is outside the wire check; its record check
still applies.
- **Late requests.** A submission a script delays past the end of the run is
still held until the page closes, but the run, already over, doesn't
report it.

A page whose own listener changes a field as the form is sent changes what is
sent after the check, so its save halts even when the demonstration did the
same: a false halt, in the safe direction. The compiler treats a click on the
submit button of the form the demonstration typed into as a save, whatever
the screen did afterwards, so a slow recording still arms the check. When the
check can't be armed on such a save, the step names why, and the save is
refused.

The digests of untouched values are salted; with the per-bundle salt stored in
the bundle (the default, unless `OPENADAPT_FLOW_IDENTITY_SALT` is set), a
low-entropy value such as a numeric id or a select option can be recovered by
trying candidates. The recording itself holds those values in plain text, as
it holds the typed ones.

A backend that can't read the page leaves the identity unreadable, which halts
too. Each field must hold exactly what the run typed, so a controlled input
that reformats the value halts: one that reformats on every keystroke stops at
the typed-value read-back, and one that reformats when it loses focus (a
phone-number mask, for example) stops at the form check. A field that a React
keyed remount replaces with a new element, even with the same value, also
halts: the new element isn't one the run typed into. A submit button outside
the `<form>` element counts when its `form=` attribute names that form. One
with no form owner at all (a React portal that submits from script) gets no
form identity, so its save stays unarmed.

The form identity ties the click to the entry the run made in the demonstrated
form. It doesn't prove the app saved it correctly: only a record check does.

For consequential or entity-sensitive workflows, require a policy that refuses
unarmed actions, review the inferred risk and tags, and test identity against
real near-match and same-name records. Pure-pixel substrates should expect more
Expand Down
17 changes: 13 additions & 4 deletions packages/flow/docs/RECORDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,12 @@ is derived from it:

```python
ChannelRead(channel="export", raw="AC-1042", source_kind="live_session")
ChannelRead(channel="export", raw="AC-1042", session="independent",
source_kind="committed_store")
ChannelRead(
channel="export",
raw="AC-1042",
session="independent",
source_kind="committed_store",
)
```

A workflow can use either kind. A write step that needs `system_of_record` values
Expand Down Expand Up @@ -247,8 +251,13 @@ In a workflow, set `read_gate` on the writing step. Replay checks it against
the values read so far in the run and halts before the step acts:

```python
Step(id="post", intent="post amount", action=ActionKind.CLICK, anchor=save,
read_gate=GradeGate(min_grade="same_screen", fields=("amount_due",)))
Step(
id="post",
intent="post amount",
action=ActionKind.CLICK,
anchor=save,
read_gate=GradeGate(min_grade="same_screen", fields=("amount_due",)),
)
```

These grades describe how a read value was checked. Write checks keep their
Expand Down
31 changes: 22 additions & 9 deletions packages/flow/docs/SELF_REPAIR.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,17 +217,23 @@ Four objects make up the API:

```python
from openadapt_flow.repair.self_repair import (
AppSession, RepairProposal, ReplayTarget, ScreenAccess, SelfRepair,
AppSession,
RepairProposal,
ReplayTarget,
ScreenAccess,
SelfRepair,
)
from openadapt_flow.backends.playwright_backend import PlaywrightBackend
from openadapt_flow.runtime.effects import RestRecordVerifier

URL = "http://127.0.0.1:8765/?fault=ok&idempotency=demo&drift=redesign#tasks"


def open_session(fault):
backend, close = PlaywrightBackend.launch(fault or URL)
return AppSession(backend=backend, close=close)


target = ReplayTarget(
open_session,
effect_verifier=RestRecordVerifier("http://127.0.0.1:8765", records_path="/api/db"),
Expand Down Expand Up @@ -280,15 +286,17 @@ contains:

```python
class MyAgent:
dialect = "anthropic" # any name in openadapt_flow.jit.dialects()
agent_id = "agent:my-repair" # recorded; can never approve its own fix
requires_model_key = True # False only for an agent that calls no model
dialect = "anthropic" # any name in openadapt_flow.jit.dialects()
agent_id = "agent:my-repair" # recorded; can never approve its own fix
requires_model_key = True # False only for an agent that calls no model

def propose(self, request):
# request.intent, request.recorded_label, request.recorded_crop_png,
# request.live_png, request.viewport, request.previous_refusals,
# request.model_key (the customer's key; never an OpenAdapt key)
action = ask_my_model(request) # e.g. {"action": "left_click", "coordinate": [846, 457]}
action = ask_my_model(
request
) # e.g. {"action": "left_click", "coordinate": [846, 457]}
return RepairProposal(action=action, model_calls=1, cost_usd=0.004)
```

Expand Down Expand Up @@ -616,8 +624,9 @@ standard = StandardChanges(
key_dir=Path("~/.openadapt/jit").expanduser(),
test_record=test_record,
)
repair = SelfRepair(agent, target, store="repair-store",
screen_access=access, standard_changes=standard)
repair = SelfRepair(
agent, target, store="repair-store", screen_access=access, standard_changes=standard
)
```

`test_record_target` is a `ReplayTarget` whose sessions open the test record,
Expand Down Expand Up @@ -648,8 +657,12 @@ window. An item that halts for any other reason stays halted for a person.
```python
from openadapt_flow.repair.preflight_queue import QueueItem, WorklistQueue

queue = WorklistQueue("bundles/triage", test_record, "runs/worklist-2026-10-18",
self_repair=lambda target: SelfRepair(agent, target, ...))
queue = WorklistQueue(
"bundles/triage",
test_record,
"runs/worklist-2026-10-18",
self_repair=lambda target: SelfRepair(agent, target, ...),
)
result = queue.run([QueueItem(item_id, target_for(item_id)) for item_id in ids])
print(result.status, [(i.item_id, i.status) for i in result.items])
```
Expand Down
2 changes: 1 addition & 1 deletion packages/flow/docs/SHADOW_CANDIDATES.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ A label that Capture scrubbed (`<dynamic_label>`) is treated as no label.
```python
from openadapt_flow.compiler.motif_induction import induce_candidate, write_candidate

candidate = induce_candidate(export) # dict or ShadowEpisodesExport
candidate = induce_candidate(export) # dict or ShadowEpisodesExport
write_candidate(candidate, "shadow-candidates/abc")
```

Expand Down
Loading
Loading