Skip to content
This repository was archived by the owner on Oct 17, 2026. It is now read-only.
Merged
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
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ claude mcp add openadapt -- uvx --python 3.12 --from 'openadapt-agent[tutorial]

When the server starts, Flow records the workflow once and, if needed, downloads a browser (about 300 MB). The first request waits for that. In our test on a Mac, the first request took about 80 seconds. The call returned `running` after 60 seconds, and the agent called `get_run` for the result. If setup takes more than 15 minutes, the request ends `did_not_run`. With Flow, a `done` result has `proof: "local"` instead of `"simulated"`.

Until 2.0.2 is on PyPI, use the command above even if your agent suggests a shorter one. The shorter command installs 2.0.1, which has no sandbox.
Until 2.0.2 is on PyPI, use the command above. The simulated sandbox suggests the same command in its `list_workflows` result. A command that installs `openadapt-agent[tutorial]` from PyPI gets 2.0.1, which has no sandbox: the server exits with an error that `--bundles` is required.

## What your agent gets back

Expand Down Expand Up @@ -150,7 +150,7 @@ The check that stops a second entry covers one runs folder. See `--runs-dir` und
- `what_happened` and `next_action`: fixed sentences you can show a person. They never contain input values or screen text.
- `proof`: `local` for a `done` that was checked on your customer's computer, `simulated` for a sandbox run that didn't open the app, and `none` for every other outcome.
- `model_calls`: on runs that reached Flow, how many AI model calls the run made. It's 0 unless model grounding, a fallback that asks an AI model where to click, is turned on (see [Security](#security)).
- `record_changed`: `yes`, `no`, or `unknown`. With `request_id_conflict`, its `no` describes this call only. The earlier request may have saved.
- `record_changed`: `yes`, `no`, or `unknown`. It's `no` only when OpenAdapt can show that nothing was written. `request_id_conflict` gives `unknown`, because the earlier request may have saved.

## Three tools

Expand Down Expand Up @@ -311,9 +311,12 @@ Card text reaches your agent's model, so keep patient details out of it. The ser

`openadapt-agent emit-skill BUNDLE --out ~/.claude/skills` writes a `SKILL.md` for one workflow: what it does, its inputs, how to call `run_workflow`, and what to do after each outcome. The skill holds no recorded values or secrets, and you still add the server separately. `--include-bundle` also copies the bundle next to the skill. That copy can contain values from the recording, so keep it as private as the recording.

Each skill is named after its workflow, so `enter_referral` becomes `enter-referral`. If the name would come out as `computer-use`, emit-skill uses `openadapt-gui-write` instead, so an assistant doesn't load the skill for every request to control the screen.
Each skill is named after its workflow, so `enter_referral` becomes `enter-referral`. Two names are reserved:

This repository also ships a general skill for any workflow, [skills/openadapt-gui-write/SKILL.md](skills/openadapt-gui-write/SKILL.md). A skill emitted under the `openadapt-gui-write` name into the same folder replaces it.
- `computer-use`, because an assistant would load that skill for every request to control the screen.
- `openadapt-gui-write`, the general skill for any workflow that this repository ships at [skills/openadapt-gui-write/SKILL.md](skills/openadapt-gui-write/SKILL.md).

If a workflow's name would come out as either one, emit-skill names the skill `openadapt-` plus the workflow's id, such as `openadapt-workflow-1a2b3c4d5e6f7a8b9c0d1e2f`. An emitted skill never replaces the general skill in the same folder.

### Record a first workflow

Expand Down
21 changes: 13 additions & 8 deletions docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ makes:
| `done` | Saved and checked by reading the record back | false |
| `needs_review` | Stopped before saving; nothing written; a person decides | false |
| `not_sure_if_saved` | A save may have gone through; a person checks the record | false |
| `did_not_run` | Nothing was written | true when a retry may succeed |
| `did_not_run` | This call wrote nothing. After `request_id_conflict`, an earlier request with the same `request_id` may have saved | true when a retry may succeed |

`running` is the one non-terminal value. The plain result is a pure
function of Flow's `transaction_outcome`, the process facts (did Flow
Expand All @@ -140,15 +140,19 @@ pause, and a consistency check of the persisted report:
| Flow could not be launched | `platform_error` | `did_not_run` |
| the app isn't reachable, checked before anything is sent (sandbox) | `app_unreachable` | `did_not_run` |
| invalid inputs, unknown workflow, runs disabled | `invalid_input`, `unknown_workflow`, `runs_disabled` | `did_not_run` |
| a used `request_id` with a different workflow or inputs | `request_id_conflict`, with `record_changed: "unknown"` | `did_not_run` (no retry) |
| a `request_id` whose five attempts each proved nothing was written | `retry_limit` | `did_not_run` (no retry) |

Only `done` says the change was saved. `record_changed: "no"` and
`safe_to_retry: true` appear only when the evidence proves nothing was
written. A coarse `HALTED` without a transaction outcome is uncertain,
never "nothing changed". A failed Needs Attention lookup counts as an open
pause, so the run waits for a person. `proof` (`local`, `sealed`,
`simulated`, or `none`) is separate from the outcome: a verified local run
is `done` with `proof: "local"`, never an error. A partner whose policy
requires a signed receipt checks `proof`.
written. `request_id_conflict` starts nothing, but an earlier attempt
under that `request_id` may have saved, so it reports
`record_changed: "unknown"`. A coarse `HALTED` without a transaction
outcome is uncertain, never "nothing changed". A failed Needs Attention
lookup counts as an open pause, so the run waits for a person. `proof`
(`local`, `sealed`, `simulated`, or `none`) is separate from the outcome:
a verified local run is `done` with `proof: "local"`, never an error. A
partner whose policy requires a signed receipt checks `proof`.

Every sentence in `what_happened` and `next_action` is fixed copy keyed by
a closed reason code. No observed screen text, recorded value, input value,
Expand All @@ -164,7 +168,8 @@ the winner. The same `request_id` returns the latest attempt's result with
`replayed: true`. A new attempt starts only when the latest one ended in a
reason from `RETRYABLE_REASONS`, each of which proves nothing was written,
and at most five times. A `request_id` reused with different inputs is
refused as `request_id_conflict`. If Flow's ledger can't be opened, local
refused as `request_id_conflict`, with `record_changed: "unknown"` because
the earlier attempt may have saved. If Flow's ledger can't be opened, local
exclusive-create claim files give the same at-most-once guarantee for that
runs directory.

Expand Down
4 changes: 2 additions & 2 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Use this when an AI agent has decided what to enter in an app with no usable API
## The contract: three tools

- `list_workflows`: each workflow's `name`, `purpose`, `done_means`, and typed `inputs` (types, enums, formats).
- `run_workflow(workflow, inputs, request_id, wait_seconds?)`: one piece of work. `request_id` is the caller's own id and is required. The same `request_id` returns the first result and never writes twice.
- `run_workflow(workflow, inputs, request_id, wait_seconds?)`: one piece of work. `request_id` is the caller's own id and is required. The same `request_id` returns the latest result and never writes twice.
- `get_run(run_id, wait_seconds?)`: the result of a run that returned `outcome: running`.

Every result has `outcome`, `safe_to_retry`, `what_happened` (one plain sentence), `next_action`, `proof`, `run_id`, and `request_id`. Tools publish an MCP `outputSchema` and return `structuredContent`.
Expand All @@ -17,7 +17,7 @@ Every result has `outcome`, `safe_to_retry`, `what_happened` (one plain sentence
- `done`: saved and checked by reading the record back. `proof` is `local` (checked on the customer's computer), `sealed` (signed receipt), or `simulated` (sandbox, no app opened). A verified local run is done; it is never reported as an error.
- `needs_review`: stopped before saving; nothing was written; a person decides. Don't start the same work again.
- `not_sure_if_saved`: a save may have gone through. Never retry. A person checks the record. Never tell the user nothing changed.
- `did_not_run`: nothing was written. Fix the cause and retry with the same `request_id` when `safe_to_retry` is true.
- `did_not_run`: this call wrote nothing. Fix the cause and retry with the same `request_id` when `safe_to_retry` is true. After `request_id_conflict`, an earlier request with the same `request_id` may have saved; call `get_run` with `first_run_id` to see it.

Only `done` is a success. Results come from openadapt-flow's `transaction_outcome`, never from the coarse halt label alone.

Expand Down
4 changes: 2 additions & 2 deletions skills/openadapt-gui-write/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ Use this when your agent has decided what to enter and the app has no usable API
| `done` | Saved and checked. | Nothing more for this request. |
| `needs_review` | Stopped before saving. Nothing was written. A person decides. | Don't start the same work again. Check back with `get_run`. |
| `not_sure_if_saved` | It may or may not have saved. | Never retry. A person checks the record. |
| `did_not_run` | Nothing was written. | Fix the problem in `what_happened`, then retry with the same `request_id` if `safe_to_retry` is true. |
| `did_not_run` | This call wrote nothing. After `request_id_conflict`, an earlier request with the same `request_id` may have saved. | Fix the problem in `what_happened`, then retry with the same `request_id` if `safe_to_retry` is true. |

Only outcome done means the change was saved and checked. needs_review means it stopped before saving and a person decides, so don't start the same work again. not_sure_if_saved means a person must check the record, so never retry it. did_not_run means nothing was written: fix the problem and retry with the same request_id when safe_to_retry is true.
Only outcome done means the change was saved and checked. needs_review means it stopped before saving and a person decides, so don't start the same work again. not_sure_if_saved means a person must check the record, so never retry it. did_not_run means this call wrote nothing: fix the problem and retry with the same request_id when safe_to_retry is true. After request_id_conflict, an earlier request with the same request_id may have saved, so call get_run with first_run_id to see it.

`proof` says how strong the evidence behind `done` is: `local` means the record check ran on the computer that ran the workflow, `sealed` means a signed receipt exists, and `simulated` means a sandbox result that opened no app.

Expand Down
9 changes: 5 additions & 4 deletions src/openadapt_agent/bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,11 @@ class ToolSpec:
"list_workflows, its inputs, and your own request_id for this piece of "
"work. The outcome is done (saved and checked), needs_review (stopped "
"before saving; a person decides), not_sure_if_saved (a person must check "
"the record; never retry), or did_not_run (nothing was written; fix it "
"and retry when safe_to_retry is true). Reuse the same request_id when "
"you retry: the same id never writes twice. If the outcome is running, "
"call get_run with the run_id."
"the record; never retry), or did_not_run (this call wrote nothing; fix "
"it and retry when safe_to_retry is true; after request_id_conflict, an "
"earlier request with the same request_id may have saved). Reuse the same "
"request_id when you retry: the same id never writes twice. If the "
"outcome is running, call get_run with the run_id."
)
_RUN_WORKFLOW_USAGE = (
"run_workflow needs workflow (a name from list_workflows), inputs (an "
Expand Down
3 changes: 2 additions & 1 deletion src/openadapt_agent/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@ def _cmd_serve(args: argparse.Namespace) -> int:
return 0

if mode == "sandbox":
from openadapt_agent.copy import TUTORIAL_SERVE_COMMAND
from openadapt_agent.sandbox import SandboxEngine, default_sandbox_dir

runs_dir = Path(args.runs_dir).expanduser() if args.runs_dir else default_sandbox_dir()
Expand All @@ -422,7 +423,7 @@ def _cmd_serve(args: argparse.Namespace) -> int:
if sandbox_engine.unavailable_reason == "browser_extra_missing":
note = (
" To drive the synthetic app in a hidden browser instead, "
"install the tutorial extra."
f"install the tutorial extra: {TUTORIAL_SERVE_COMMAND}"
)
print(
f"openadapt-agent {__version__}: sandbox mode. One workflow "
Expand Down
19 changes: 13 additions & 6 deletions src/openadapt_agent/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
- ``record_changed: "no"`` and ``safe_to_retry: true`` appear only for results
whose evidence proves nothing was written. Uncertain delivery (including a
timeout after Flow started) is ``not_sure_if_saved`` with
``safe_to_retry: false``.
``safe_to_retry: false``. A ``request_id_conflict`` starts nothing, but an
earlier attempt under that ``request_id`` may have saved, so it reports
``record_changed: "unknown"``.
- A run with an open durable pause is ``needs_review``: a person holds the
decision, so the caller must not start the same work again.
- Every sentence is fixed copy keyed by a closed reason code. No observed screen
Expand Down Expand Up @@ -246,7 +248,7 @@ class Reason:
"the server closed. The change may or may not be saved.",
_CHECK_RECORD,
),
# -- did_not_run: proven nothing written -------------------------------
# -- did_not_run: this call wrote nothing --------------------------------
"invalid_input": Reason(
"did_not_run",
True,
Expand Down Expand Up @@ -341,13 +343,16 @@ class Reason:
"Don't retry this request. If the work is still needed, a person "
"decides how, and a new attempt uses a new request_id.",
),
# The earlier request under this request_id may have saved, so this
# result never says nothing was written.
"request_id_conflict": Reason(
"did_not_run",
False,
"no",
"unknown",
"fix_inputs",
"Didn't start because this request_id was already used for a "
"different request. Nothing new ran.",
"different request. Nothing new ran, but the earlier request may "
"have saved.",
"Use a new request_id for new work. To see the earlier result, call "
"get_run with first_run_id.",
),
Expand Down Expand Up @@ -729,8 +734,10 @@ def build_result(
"description": (
"done: saved and checked. needs_review: stopped before saving; a "
"person decides. not_sure_if_saved: a person must check the record "
"before anything runs again; never retry. did_not_run: nothing was "
"written. running: call get_run later."
"before anything runs again; never retry. did_not_run: this call "
"wrote nothing; after request_id_conflict an earlier request "
"with the same request_id may have saved. running: call get_run "
"later."
),
},
"label": {"type": "string"},
Expand Down
17 changes: 15 additions & 2 deletions src/openadapt_agent/copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
"Only outcome done means the change was saved and checked. needs_review means "
"it stopped before saving and a person decides, so don't start the same work "
"again. not_sure_if_saved means a person must check the record, so never retry "
"it. did_not_run means nothing was written: fix the problem and retry with the "
"same request_id when safe_to_retry is true."
"it. did_not_run means this call wrote nothing: fix the problem and retry with "
"the same request_id when safe_to_retry is true. After request_id_conflict, an "
"earlier request with the same request_id may have saved, so call get_run with "
"first_run_id to see it."
)
#: Older name for ``OUTCOME_RULES``.
SKILL_HONESTY = OUTCOME_RULES
Expand All @@ -38,3 +40,14 @@
)
#: Older name for ``FIRST_COMMAND``.
THREE_LINE_INSTALL = FIRST_COMMAND

#: The tutorial extra from the GitHub source. PyPI has 2.0.1, which has no
#: sandbox, so ``openadapt-agent[tutorial]`` from PyPI starts a server that
#: exits. Drop the ``@ git+...`` part once 2.0.2 is on PyPI.
TUTORIAL_REQUIREMENT = (
"openadapt-agent[tutorial] @ git+https://github.com/OpenAdaptAI/openadapt-agent"
)
#: Starts the sandbox with the real engine. The README shows the same command.
TUTORIAL_SERVE_COMMAND = (
f"uvx --python 3.12 --from '{TUTORIAL_REQUIREMENT}' openadapt-agent serve"
)
20 changes: 14 additions & 6 deletions src/openadapt_agent/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from mcp.server.lowlevel import Server
from mcp.server.stdio import stdio_server

from openadapt_agent import __version__
from openadapt_agent.attended import ATTENDED_TOOLS
from openadapt_agent.authoring import AuthoringBridge
from openadapt_agent.bridge import AgentBridge, BridgeError
Expand Down Expand Up @@ -116,11 +117,13 @@ def _server_instructions(
"saved and checked; needs_review means it stopped before saving and "
"a person decides, so don't start the same work again; "
"not_sure_if_saved means a person must check the record, so never "
"retry it; did_not_run means nothing was written, so fix the problem "
"and retry only when safe_to_retry is true. Reuse the same request_id "
"when you retry: the same id never writes twice. If the outcome is "
"running, call get_run with the run_id. Tell the user what_happened "
"in your own words and never call anything but done a success."
"retry it; did_not_run means this call wrote nothing, so fix the "
"problem and retry only when safe_to_retry is true, and after "
"request_id_conflict an earlier request with the same request_id may "
"have saved. Reuse the same request_id when you retry: the same id "
"never writes twice. If the outcome is running, call get_run with "
"the run_id. Tell the user what_happened in your own words and "
"never call anything but done a success."
)
if bridge.mode == "sandbox":
text += (
Expand Down Expand Up @@ -253,7 +256,11 @@ def call() -> dict[str, Any]:
return types.CallToolResult(content=text)

if hasattr(Server, "list_tools"):
server = Server(SERVER_NAME, instructions=_server_instructions(authoring, bridge))
server = Server(
SERVER_NAME,
version=__version__,
instructions=_server_instructions(authoring, bridge),
)
server.list_tools()(_list_tools)

async def _call_v1(name: str, arguments: dict[str, Any] | None):
Expand All @@ -271,6 +278,7 @@ async def _call_v2(context: Any, params: types.CallToolRequestParams):

server = Server(
SERVER_NAME,
version=__version__,
instructions=_server_instructions(authoring, bridge),
on_list_tools=_list_v2,
on_call_tool=_call_v2,
Expand Down
5 changes: 3 additions & 2 deletions src/openadapt_agent/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@

from openadapt_agent.cards import WorkflowCard
from openadapt_agent.contract import parse_failed_checks, reason_for_run
from openadapt_agent.copy import TUTORIAL_REQUIREMENT, TUTORIAL_SERVE_COMMAND
from openadapt_agent.service import (
CatalogEntry,
EngineResult,
Expand Down Expand Up @@ -237,7 +238,7 @@ def __init__(
elif engine == "flow":
raise RuntimeError(
"the flow sandbox engine needs the tutorial extra: "
"pip install 'openadapt-agent[tutorial]'"
f"pip install '{TUTORIAL_REQUIREMENT}'"
)
else:
self.engine = "simulated"
Expand Down Expand Up @@ -285,7 +286,7 @@ def describe(self) -> dict[str, Any]:
info["engine_detail"] = (
"Results are simulated without opening the app. To drive the "
"synthetic app in a hidden browser, install the tutorial extra: "
"uvx --python 3.12 --from 'openadapt-agent[tutorial]' openadapt-agent serve"
+ TUTORIAL_SERVE_COMMAND
)
return info

Expand Down
Loading
Loading