Skip to content

feat(agent): teach a workflow over MCP, then call it with no model calls - #1335

Open
abrichr wants to merge 9 commits into
mainfrom
claude/mcp-teach-compile-a652e8
Open

abrichr wants to merge 9 commits into
mainfrom
claude/mcp-teach-compile-a652e8

Conversation

@abrichr

@abrichr abrichr commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

An agent connected over MCP can now turn a task it can't do yet into a workflow it calls, without leaving the conversation. It does the task once through OpenAdapt; the run is read back, compiled and admitted; and the result is a new tool that runs with no model calls.

Stacked on #1277 (compile-agent). Its base is that branch; merge #1277 first, then this retargets to main.

How it works

openadapt-agent serve --teach verify.yaml --url <test app> (or --config deploy.yaml for a native or remote app) adds:

Tool What it does
teach_start Opens the operator's app and starts run 1 of the agent's task (inputs, the record fields the save must produce, the record's identity on screen).
teach_observe, teach_act, teach_screen The agent does the task one action at a time, through compile-agent's harness and save gate.
teach_finish Compiles the run and reads the system of record. Returns the draft: steps, parameters, the proposed record check, and what still needs a person.
teach_confirm_checks A person confirms the record check in the client's form, with their name. No argument stands in for them.
teach_admit Canaries, then JIT admission under a person-signed policy.
taught_<name> One per admitted program; also listed in list_workflows / run_workflow.

The connected agent is the inner agent of FirstRunCompiler: compile_agent/driven.py runs record() on its own thread and feeds it one action per tool call. Nothing in the record check, the proposed checks, the canaries or the emit step is new code.

Admission stays inside JIT_ADMISSION.md

  • The operator gives the server an agent key and a signed policy (--teach-key, --teach-policy). An admitter key is refused: on a server the agent drives, it would let the agent approve its own draft.
  • The server records run 1 as that agent's principal, so teach_admit is self-admission. Lifecycle refuses it unless the policy sets allow_self_admission, before any canary runs.
  • No key: the draft names no agent principal, so only a person (or an admitter key) can admit. teach_admit returns the terminal command: openadapt-flow compile-agent admit TASK_DIR --admit-as NAME (new).
  • After admission the server sends notifications/tools/list_changed (it now advertises tools.listChanged). A server started later lists every taught program.

Every backend

FirstRunCompiler(surface=<deployment backend section>) uses a new SurfaceHarness off the web. It gates the same actions, but it can't read which record a native or remote form shows, so a person approves every gated action in the client's form; a client without forms can't save. The approval is bound to the control the person was shown. Native backends list no controls yet (BACKEND_PARITY gap), so the agent reads the screen (--teach-screens scrubbed, fails closed) and clicks pixels. On RDP and Citrix, admission is refused with the gate's reason (pixel identity is not armed yet). Network RDP is refused at start.

Tests (synthetic data, no model calls)

  • tests/e2e/test_mcp_teach_e2e.py (added to the flow-critical job, which now installs the agent extra): a scripted MCP SDK client on MockMed. Full loop: record, read back, person confirms, agent self-admits under policy, tools/list_changed, the new tool runs done with 0 model calls. Refusals: admit before the check is confirmed; no policy; recording agent without allow_self_admission (no canary runs); a save on the wrong record fails the read-back; the agent can't confirm the check without a person. Also a person admitting at a terminal.
  • tests/test_mcp_teach_surfaces.py: teach over MCP through the real RemoteDisplayBackend and CitrixWorkspaceBackend (kiosk window, pixels only); every click approved by a person; canary refused with its reason.
  • tests/test_compile_agent_surface_harness.py, tests/test_mcp_teach_cli.py: unit tests for the surface harness and the server flags.

Local: those files pass; agent shim tests (mcp_server, cli, authoring, readme commands, bridge) pass; ruff, ruff format, mypy clean. The host's load average was 60-270 during runs, and one browser test flaked once while starting the app; CI is the real check.

Review fixes (independent review, commit 328e4fb)

Finding Fix
A person's save approval was a label string, so it could apply to another run or a changed screen The approval is bound to the run id, the exact action and the count of actions performed; any change refuses it and nothing is done. The form says which action it is (click at x=.., press Enter).
Off the web, Space after Tab, a coordinate match on a wrapper, and Yes/OK/Add clicks passed the gate Every click is gated unless it is on a ref'd text field, check box, tab or plainly navigating control; x,y clicks, Enter, typing outside a text field, single-letter keys and shortcuts are gated; focus resets on Tab.
On the web, a typed space on a focused button was not gated Gated.
A timed-out action could still run later The run ends instead.
A run that failed to start could overlap a second run 1 It stays tracked until its thread ends.
Run 1, canaries and taught runs could write at the same time One app lock.
An unfinished run 1 kept its browser after the server exited serve stops it on exit.

Documented, not changed: on the web without --teach-hold, the agent names the identity the gate checks; the run-1 record check and the person who confirms it are what catch a weak one.

Docs

packages/agent/README.md (Teach a new workflow from your agent, linked from Connect your own agent), packages/agent/docs/DESIGN.md, packages/flow/docs/COMPILE_AGENT.md, packages/flow/docs/BACKEND_PARITY.md, and CAPABILITIES.md (summary, agents table, and the command list).

🤖 Generated with Claude Code

abrichr and others added 6 commits October 19, 2026 09:01
openadapt-agent serve --teach VERIFY_YAML adds teach_start, teach_observe,
teach_act, teach_screen, teach_finish, teach_abort, teach_status,
teach_confirm_checks and teach_admit. The connected agent is the inner agent
of compile-agent's FirstRunCompiler (compile_agent.driven.DrivenRun): run 1
goes through the same harness and save gate, the record check reads the
system of record, a person confirms the proposed check in the client's form,
and teach_admit admits only as the registered agent the operator configured,
under a person-signed policy (self-admission needs allow_self_admission).
An admitted program joins list_workflows and run_workflow and gets a
taught_<name> tool; the server sends tools/list_changed.

FirstRunCompiler takes surface= (a deployment backend section) and uses a
new SurfaceHarness off the web: same gating, and a person approves every
gated action because the harness can't read which record a form shows.
openadapt-flow compile-agent admit TASK_DIR is the person's terminal path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the web

Review fixes for the MCP teach tools:
- An approval covers one run, one exact action and the app as it was
  (run id, action, actions performed so far). A changed screen, another run
  or another action in between refuses it; nothing is done.
- Off the web, every click is gated unless it is on a ref'd text field,
  check box, tab or a plainly navigating control; clicks at x,y, Enter,
  typing outside a text field, single-letter keys and shortcuts are gated.
  Focus resets after Tab and other focus keys, so Space after Tab is gated.
  The gate uses the ref's control, not a coordinate match.
- On the web, typing a space on a focused button is gated.
- type with a ref refuses a ref that isn't a text field.
- An action that times out ends run 1 instead of running later.
- A run that fails to start stays tracked until its thread ends.
- One app lock for run 1, canaries and taught runs.
- The server stops an unfinished run 1 on exit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ests)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
abrichr and others added 3 commits October 19, 2026 15:04
Keeps main's page-less CommitHint gating in the web harness next to the
teach path's ref-aware commit_target. The teach unit tests no longer import
the MCP SDK at collection (test_emit checks emission doesn't import it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keeps both sides: the task ledger's tools and the teach tools in the MCP
server (a taught workflow's own tool goes through the ledger like
run_workflow), _attach_ledger on teach servers, both README sections (teach,
then the ledger), both .gitleaksignore fingerprint sets, and the split
command map with a compile-agent admit note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
abrichr added a commit that referenced this pull request Oct 19, 2026
#1335's files (agent/cli.py, agent/mcp.py, .gitleaksignore, the agent README)
taken from its updated branch, which already merged this main; the guard
section added back to the README. The guard command moved to
docs/commands/11-guard-an-agent-you-already-run by
scripts/migrate_branch_to_split_files.py, which also regenerated the command
list, the verification docs and the path-only artifact inventory.
llms-full.txt regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
abrichr added a commit that referenced this pull request Oct 19, 2026
Takes #1335's harness docstring and its MCP teach tests (which no longer load
mcp at collection); keeps the guard's CI tests, README section and capability
rows. llms-full.txt regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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