Skip to content

Agent runtime

Status: experimental

The agent runtime is under active development on the feature/agent-runtime branch. It is off by default. Phase 0 (the in-Revit host) is validated on Revit 2024 and 2025; the CLI and MCP server (Phase 1) are in progress. See Phases.

The agent runtime lets any MCP-capable coding agent (Claude Code, Codex, Cursor, VS Code, OpenCode, ...) read and change a live Revit model by sending Python programs, not by calling hundreds of per-API tools. The Revit API is the toolset; MCP is only the control protocol. The human stays in control: by default, the model changes a run makes are approved inside Revit.

Getting started

Everything below is also available in Revit under pyRevit → Settings → Agent Runtime (MCP). That section enables the host, sets the policy and default engine, shows whether the host is listening, and has one button per MCP client to register the server. The agent runtime is independent of the Routes server and doesn't need it enabled.

  1. Enable the agent host. Run this with the pyrevit.exe of the clone you want the agent to use, then reload pyRevit or restart Revit.

    pyrevit configs agent enable
    
  2. Register the MCP server with your client. The registration command does not change whether the host is enabled.

    pyrevit mcp install claude        # Claude Code, user scope
    pyrevit mcp install codex         # OpenAI Codex
    pyrevit mcp install cursor        # Cursor (~/.cursor/mcp.json)
    pyrevit mcp install vscode        # VS Code (user mcp.json)
    pyrevit mcp install opencode      # OpenCode (~/.config/opencode/opencode.json[c])
    

    Add --project to register for the current directory instead (Claude Code: .mcp.json, Cursor: .cursor/mcp.json, VS Code: .vscode/mcp.json, OpenCode: opencode.json[c]). Codex only supports user-level servers. A JSON config file that contains comments is never rewritten; the command prints the entry to add by hand.

  3. Start Revit (or reload pyRevit), and open a model.

  4. Check the connection:

    pyrevit agent status
    
  5. Ask your agent about the model, for example "How many doors on Level 2 have no Mark?" or "Set the Comments of the selected walls to 'Check fire rating'". Changes show an approval prompt in Revit, with the changed elements selected and temporarily isolated in the active view. Keep commits them as one undo entry named Agent: <title>; Discard rolls them back.

To remove the server: pyrevit mcp uninstall <client>. pyrevit mcp uninstall --all removes the user-level entry from every client, and the pyRevit uninstaller runs it with --owned, which keeps entries that point at another pyRevit install. It only cleans the profile of the account running it, never other users' profiles, so on a shared machine each user who registered the server removes their own entry. When that command runs elevated, it doesn't start the claude or codex command line tools, so those two entries stay; run pyrevit mcp uninstall claude (or codex) from a normal prompt. Project-level entries (--project) are never removed automatically. To turn the host off: pyrevit configs agent disable.

Model data leaves the machine and is recorded locally

Whatever the agent reads (element names, parameter values, results) is sent to the agent's model provider. Check this against your project's confidentiality rules before enabling the agent runtime. The runtime also stores each run's script, request, response, large result, and captured images in plaintext under %APPDATA%\pyRevit\agent for up to 14 days.

Reference

Configuration

Setting Command Values
[agent] enabled pyrevit configs agent (enable \| disable) Starts the in-Revit host on the next pyRevit load. Default false.
[agent] policy pyrevit configs agent policy (readonly \| ask \| auto) readonly: queries and dry runs only; it does not sandbox Python or prevent file, network, or process side effects. ask (default): each modify run needs approval in Revit. auto: modify runs are committed without the prompt; each is still one undo entry, guarded and recorded.
[agent] engine pyrevit configs agent engine (ironpython \| cpython) Engine for runs that don't name one. Default ironpython (the attached IronPython).

Run any of these commands without a value to print the current setting.

MCP tools

Tool Changes the model Purpose
get_skill no Task guidance in markdown (see Skills); the server's instructions tell agents which skill to read first
list_revit_instances no Running Revit sessions with the agent host
get_context no Revit and pyRevit versions, agent policy, scripting (engine and Python version scripts run on), document, open_documents, active view, selection, levels
inspect_elements no Class, category, type, level, location, bounding box and parameters of up to 50 elements
lookup_revit_api no Signatures of a Revit API type or member, reflected from the running Revit. Also its namespace and Python import line, and a creation list: static factories and the doc.Create.New… methods that return the type. A missing member returns found: false with the closest names, including matching values of other enums
show_elements no Select, zoom to, or temporarily isolate / hide elements (by id or category) in the active view, or reset the temporary mode. No approval prompt. If Revit can't zoom, the response has zoomed: false and a zoom_failed error instead of a dialog blocking Revit.
capture_view no PNG of a view for visual checks. export renders any view through Revit; viewport renders an open view through Revit cropped to what its window shows now (zoom, pan, temporary isolate; no selection highlight), in a rolled-back transaction; screen captures the active view window as the user sees it (selection, temporary isolate), and fails with view_obscured when another application's window covers any part of it; view 3d renders a temporary 3D view of model categories only, framed by a section box around the model (or elements) and seen from direction, which is rolled back. Saved under %APPDATA%\pyRevit\agent\captures.
run_query never Run a read-only script; model changes are always rolled back, but the script itself has full access to the machine, so the tool isn't marked read-only for MCP clients. workspace puts a folder of the agent's own modules on sys.path, re-imported fresh every run. timeout_s (default 300) stops a script that runs too long
run_modify after approval Run a changing script against the active document; it is refused when no document is active. dry_run=true previews the change set and rolls back
get_run no A recorded run: response, script, and pages of a large result

Every request that runs on Revit's main thread closes the dialogs Revit opens during it and reports them in dialogs, so a dialog can't leave the call hanging.

Only agent-written code needs approval. show_elements is a fixed host operation that changes presentation, never model elements. Temporary hide/isolate runs in its own small transaction because the Revit API requires one, and that state isn't saved with the model. The agent is told to use it rather than writing run_modify scripts to show things.

run_query and run_modify return a compact result for the agent: status, then error (when present), decision, result, output, and only the non-empty change set, failures, dialogs and blocked operations. The full record stays available through get_run. AttributeError and TypeError failures carry a hint that names the lookup_revit_api call to make, for example 'Collector' does not exist in Autodesk.Revit.DB or check the overloads of 'FilteredElementCollector.OfCategory'.

If the host cannot persist a result or run record, the response keeps the final model decision and adds a warning. The corresponding get_run record can be incomplete or unavailable.

Every tool that talks to Revit accepts an optional revit argument (a year such as "2024", or a process id) for when several Revit sessions run. pyrevit mcp --revit=<year> sets a default.

Long approvals

A run_modify call waits until the user answers the prompt in Revit. The server sends progress notifications every 10 seconds while it waits. If your client still times out, raise its MCP tool timeout (Claude Code: the MCP_TOOL_TIMEOUT environment variable, in milliseconds).

Skills

The guidance agents read lives in markdown, not in the CLI, so it can change without a rebuild:

  • pyrevitlib/pyrevit/agent/skills/INSTRUCTIONS.md is the short entry text the MCP server sends on initialize. {skills} is replaced with the list of skills.
  • Each skill is a folder with a SKILL.md that starts with name and description front matter. PR 1 ships revit-scripting, the core contract for raw Revit API scripts. PR 2 adds domain guidance and library integration.

Agents read a skill with get_skill(name), and other markdown files in its folder with get_skill(name, file). list_skills reports each skill's source and SHA-256 digest before the agent reads it. The server loads only shipped skills by default. Enable user skills with pyrevit configs agent skills enable to load folders from %APPDATA%\pyRevit\agent\skills. Those folders are trusted-account guidance, not organizational policy: a same-name user skill is ignored, and user guidance cannot replace shipped safety instructions or host enforcement. Names, descriptions, files and paths are bounded and validated; reparse-point paths are ignored.

inspect_elements returns an element link with a destination, document reference and element ids. Pass that object to navigate_revit_link to select, zoom to, isolate or temporarily hide the same elements. The Revit host compares its active document with the link immediately before presenting elements. If the document has closed or changed, it returns stale_link; activate the intended document and inspect the elements again. Navigation only uses the fixed presentation operation and never executes a command or changes model elements.

Script contract

Scripts are Python, executed inside Revit by the regular pyRevit engines. The runner injects doc, uidoc, app, uiapp, DB (Autodesk.Revit.DB), UI (Autodesk.Revit.UI) and inputs (the inputs argument as a dict). Assign result to return data. It is serialized to JSON:

  • ElementId becomes an integer.
  • Element becomes {id, category, name, class}.
  • XYZ becomes [x, y, z].
  • .NET numbers become numbers, and collections become lists.

Printed output is captured and returned too.

walls = DB.FilteredElementCollector(doc).OfClass(DB.Wall).WhereElementIsNotElementType()
result = {"count": walls.GetElementCount()}

In run_modify, the script opens its own transactions:

t = DB.Transaction(doc, "Set comments")
t.Start()
for element_id in uidoc.Selection.GetElementIds():
    doc.GetElement(element_id).get_Parameter(
        DB.BuiltInParameter.ALL_MODEL_INSTANCE_COMMENTS
    ).Set(inputs["text"])
t.Commit()

Rules the host enforces:

  • A query that changes the model fails with query_modified_model and is rolled back.
  • run_modify requires an active document and is refused before the script runs when none is active. The other tools (capture_view, inspect_elements, show_elements) also require an active document.
  • Background documents a run created or opened (no view in Revit, not linked, not open when the run started) are closed without saving when the run ends, whatever its outcome, and listed in closed_documents. The host blocks their save and save-as operations while a run is active. get_context.open_documents lists every open document and marks the background ones.
  • While a run is active, the host cancels Revit operations exposed through cancellable save, save-as, synchronize-with-central, file-export and view-export events. The response lists a cancelled operation in blocked.
  • A run in any mode that changes another open document fails with other_document_modified and is rolled back. changes.other_documents lists what changed. A project the script opens or a family from a file during the run counts as one that was already open: changing it fails the run, it is rolled back, and it can't be saved or saved as, so the file on disk stays unchanged. A family from EditFamily or a new document the script creates has no file yet; it is reported there but doesn't fail the run, and isn't rolled back.
  • A script error rolls back everything, and the traceback points at the script's own lines. When a Revit call throws, the message carries the underlying .NET exception and its inner exceptions ([.NET: Autodesk.Revit.Exceptions.… <- …]), not only the generic "A managed exception was thrown".
  • When Revit rolls back one of the script's transactions because of an error-level failure, the run fails with revit_failure, even if the script itself didn't raise.
  • A transaction left open fails the run with transaction_left_open.
  • A script that runs past timeout_s (default 300 seconds, a finite number above 0, at most 3600) is stopped and fails with timeout. The runner checks the deadline between Python lines, so it stops loops. A single blocking call, such as a long Revit API call or time.sleep, can't be interrupted; the run fails with timeout once it returns.
  • Python drops a trace function after it raises once, so the runner compiles the script with every bare except and except BaseException narrowed to except Exception. A script can't swallow its own timeout, and can't catch KeyboardInterrupt or SystemExit that way. Suppression through an alias or contextlib.suppress(BaseException) still bypasses this; on CPython a watchdog thread is the backstop.
  • Revit dialogs are closed automatically and reported in dialogs. Warnings are removed and reported in failures, and errors roll back the failing transaction.
  • The document can't be saved, closed or synchronized during a run.

The MCP server's instructions teach agents these conventions, plus units (internal feet) and ElementId.Value on Revit 2024+.

Which Python runs the script

Agents must not guess the language level. get_context.scripting reports it:

"scripting": {
  "default_engine": "ironpython",
  "default_python": "3.4.2",
  "default_syntax": "Python 3.4 syntax plus f-strings, ...",
  "engines": {
    "ironpython": {"available": true, "implementation": "IronPython", "python": "3.4.2", "syntax": "..."},
    "cpython":    {"available": true, "implementation": "CPython",    "python": "3.12.3", "syntax": "..."}
  }
}

The IronPython entry follows the attached engine (IronPython 2.7.12 or 3.4.2). Scripts run on default_engine (the [agent] engine setting) unless a run passes engine. Every run response carries an engine field with the interpreter's own sys.version, which confirms what actually executed.

available means a run on that engine can start. CPython loads its engine DLL from the clone attached to the Revit version or, in a session pyRevit isn't attached to, from the clone the session was loaded from. When that clone has no CPython engine, CPython is available: false with an unavailable_reason, and a run that asks for it fails with engine_unavailable before any code runs. get_context.pyrevit reports attached, and clone and the IronPython python are null when the session doesn't know them, instead of Unknown or 0.

Syntax measured in Revit on IronPython 3.4.2:

Syntax IronPython 3.4.2
f-strings, variable annotations, keyword-only arguments, */** unpacking in literals supported
walrus :=, async/await, 1_000 numeric literals, positional-only /, match SyntaxError

IronPython 2.7.12 is Python 2.7 syntax. CPython 3.12 supports everything, but reaches the Revit API through pythonnet, and pyrevitlib features built for IronPython may be limited.

CLI

pyrevit agent status [--json]              running Revit sessions with the agent host
pyrevit agent context [--revit=<year>]     get_context of a session
pyrevit agent run <script_file> [--mode=query|dry_run|modify] [--engine=<e>]
                  [--title=<t>] [--inputs=<json>] [--revit=<year>]
                  [--timeout=<seconds>] [--workspace=<folder>]
pyrevit agent runs [--limit=<n>]           recent runs
pyrevit agent show <run_id>                request, script and response of a run
pyrevit mcp [--revit=<year>]               the MCP server (stdio); started by MCP clients
pyrevit mcp (install | uninstall) (claude | codex | cursor | vscode | opencode) [--project]
pyrevit mcp uninstall --all [--owned]      every client's user-level entry; --owned keeps
                                           entries that point at another pyRevit install

--timeout defaults to 300 and is capped at 3600. --workspace must be an existing folder.

The agent and mcp commands ship with the pyRevit CLI of the same build. An older pyrevit on PATH doesn't know them and prints the global usage; upgrade the CLI or call the one from the build that provides the agent runtime.

Run records

Every run is recorded under %APPDATA%\pyRevit\agent\runs\<timestamp>-<run_id>\:

  • script.py: the submitted source
  • request.json: title, mode, engine and inputs
  • response.json: status, decision, changes, failures, dialogs, output and result
  • result.json: present when the result was too large to return inline

Run records and capture_view images older than 14 days are deleted when the host starts.

Design

Architecture

 Claude Code / Codex / Cursor / VS Code / OpenCode
            │  MCP over stdio
            ▼
 ┌──────────────────────────────┐   pyRevitCLI (net8, out of process)
 │ pyrevit mcp                  │   MCP protocol, tool schemas, instructions,
 │                              │   instance discovery, run records
 └──────────────┬───────────────┘
                │  named pipe  \\.\pipe\pyrevit-agent-<pid>  (current user only)
                ▼
 ┌──────────────────────────────┐   inside Revit (PyRevit.Runtime, Agent/)
 │ AgentHost                    │
 │  AgentDispatcher ─► ExternalEvent
 │  AgentRunGuard: TransactionGroup, DocumentChanged diff,
 │                 save/sync/close blocking, dialog + failure capture
 │  approval + isolate preview, AgentScriptRunner, run records,
 │  AgentInspector, AgentApiLookup
 └──────────────┬───────────────┘
                ▼
     ScriptExecutor ─► IronPython / CPython ─► Revit API + pyrevitlib

Why two processes:

  • Dependency isolation: MCP handling and its dependencies stay out of revit.exe. Assembly clashes with other add-ins are a real risk on .NET Framework 4.8.
  • Multi-instance routing: the CLI finds every running host and routes each call to one.
  • Stability: the agent keeps its connection to pyrevit mcp across Revit restarts.
  • Client support: every MCP client supports stdio.

Why a named pipe: no TCP port, no firewall prompt, and nothing on the network. The pipe's access list allows only the current Windows user and denies network logons, and that is the authentication. The trust boundary is the Windows account: any process running as that user can connect to the host and submit scripts.

The CLI also checks that the process serving the pipe is the Revit recorded in the instance file, and refuses to send a request otherwise, so another local process can't take over a pipe name while the host is down.

Remote use is through Remote Desktop only. An RDP session is an interactive logon, so pyrevit mcp started inside it reaches Revit. SSH is refused on purpose: an SSH logon carries the Network SID, so connecting fails with UnauthorizedAccessException, even under the same account.

Discovery: each host writes %APPDATA%\pyRevit\agent\instances\<pid>.json with the pipe name, the Revit version and the process id, and removes it when Revit exits.

Existing pieces this builds on

Piece Where Used for
Queued ExternalEvent dispatcher ShellExternalEventDispatcher in dev/pyRevitLabs.PyRevit.Shell/ShellLauncher.cs Pattern for the agent request queue (task-based wait, no spin)
Script execution ScriptExecutor, ScriptRuntimeConfigs.Variables Running agent scripts on the IronPython / CPython engines
Config accessors PyRevitConfigs [agent] settings, shared by the host and the CLI
Structured command results __result__ / script.get_results() Returning command output to the agent (Phase 3)
Command lookup sessionmgr.execute_command() Running a command by unique id (Phase 3)
Bundle metadata ParsedBundle (pyRevitExtensionParser) Opt-in agent: block (Phase 3)
Batch runs pyrevit run --models= Multi-model agent jobs (Phase 5)

Deliberately not reused:

  • Routes server (pyrevit.routes): it has no authentication (# TODO: auth in pyrevitcore_api.py) and falls back to 0.0.0.0. Code execution must never be exposed through it.
  • ScriptExecutor.RequestExecuteScript: it spins on IsPending and uses a single handler slot that a concurrent request can overwrite. The agent host owns its own queue.

Where the code lives

Area Contents
dev/pyRevitLabs.PyRevit.Runtime/Agent/ Host, dispatcher, pipe server, run guard, approval and isolate preview, script runner, inspector, API lookup. It lives in the runtime so it can call ScriptExecutor directly; every per-year runtime build shares the source.
pyRevitAssemblyBuilder SessionManagerService Starts, refreshes or stops the host on every LoadSession to match the config.
pyrevitlib/pyrevit/agent/ In-engine runner: executes the agent source, captures output, serializes result. Must stay parseable by IronPython 2.7, IronPython 3.4 and CPython 3.
pyRevitLabs.PyRevit PyRevitConfigs Get/SetAgentEnabled, Get/SetAgentPolicy, Get/SetAgentEngine.
pyRevitCLI PyRevitAgentClient (discovery and pipe protocol), PyRevitCLIAgentCmds (agent, configs agent, mcp install and uninstall), PyRevitMcpServer (pyrevit mcp).
pyRevitDevTools Debug.panel/Agent Tests.pulldown In-Revit end-to-end tests that drive the host through AgentHost.HandleRequest.
dev/pyRevitLabs/tests/pyRevitLabs.PyRevit.Runtime.Agent.Tests Unit tests of the run guard against Revit test doubles; run in CI.

Pipe protocol

Newline-delimited JSON-RPC 2.0, one request per connection. Revit serves one connection at a time and closes one that sends nothing for 30 seconds. Methods: ping, get_context, run, inspect_elements, show, capture, lookup_api. Errors carry a stable data.type, such as revit_busy, no_active_document, policy_readonly or invalid_params.

The guarded run

Every run executes in one ExternalEvent callback on the Revit main thread:

  1. Arm guards (only while the run is active):
    • DocumentSynchronizingWithCentral is cancelled, and so are DocumentSaving, DocumentSavingAs and DocumentClosing for every document open when the run started. DocumentSaving and DocumentSavingAs are also cancelled for a project or family the script opens from a file (DocumentOpened); the script may still close it, and the host closes it without saving when the run ends. A family from EditFamily or a new document the script created has no file yet and may be saved and closed.
    • DialogBoxShowing is captured and dismissed.
    • FailuresProcessing warnings are recorded and deleted, and errors roll back the failing transaction, in every non-linked document. A failure in a family or a new document fails the run with revit_failure instead of raising a dialog.
    • DocumentChanged collects added, modified and deleted ids, per document.
  2. TransactionGroup.Start("Agent: <title>") on the active document and on every other open, editable, non-linked document, and later on each project or family the script opens from a file.
  3. Run the script. Its own transactions nest inside the group.
  4. Decide:
    • any mode, another open document changed: roll back every group. The run fails with other_document_modified. Only the active document's group is ever assimilated.
    • query: always roll back. A recorded change becomes a query_modified_model error.
    • dry_run: roll back and return the change set.
    • modify with policy readonly: the policy is read before the script and again here, and the stricter value (readonly, then ask, then auto) decides. A run queued before the switch to readonly rolls back with policy_readonly instead of committing, and a script that rewrites the policy can't loosen it for its own run.
    • modify with policy auto: Assimilate() straight away. The response says approval: auto.
    • modify with policy ask: while the group is still open, select and temporarily isolate the changed elements, and show the approval prompt. The isolation is switched off again inside the group before deciding. Keep → Assimilate(), one undo entry. Discard → RollBack().
  5. Disarm guards and write the run record.

The approval must happen inside the same callback, because Revit won't keep a transaction group open after the callback returns. The benefit is that the human approves inside Revit, and no agent tool can approve a run.

Honest limits:

  • Rollback protects the model, not the file system or the network. There is no sandbox; readonly means query-only model access, not restricted Python execution.
  • The guard covers the run itself, not code the run leaves behind. A script has the live Revit API, so it can subscribe to a Revit event such as Idling, or start a thread, that changes the model or answers the approval prompt after the guard has ended. Those changes are neither approved nor recorded. Approval and readonly stop an agent's mistakes, not a hostile script: connect only agents you trust, and don't feed them untrusted content.
  • Revit serves one agent request at a time. While one session's run is executing, which can last until its timeout_s, a second agent session can't connect and gets revit_busy after the CLI's 10-second connect timeout. Keep runs short, and run one agent per Revit.
  • ExternalEvents only fire while Revit is idle, so a busy Revit returns revit_busy after 30 seconds. The error names Revit's open windows and whether an earlier agent request is still running: a modal dialog Revit shows while idle, such as the save reminder, isn't part of any run and blocks every request until the user answers it.
  • A rollback must not touch committed work, but rolling back a dry run once undid the previous committed run as well (that run used StairsEditScope; Revit's journal logged UndoElement::simplify PROBLEMS). The host remembers a sample of the elements each committed run created and checks them after every rollback and before every run; a loss comes back as warnings in the run result.
  • Revit regeneration inflates change sets: two new walls can report 100+ modified elements (rooms, tags, paths of travel, schedules). The MCP tools therefore return only counts, categories and the first added ids; the element list is in get_run. Phase 2 will rank the elements the script touched ahead of these side effects.

pyRevit commands as agent tools (Phase 3)

Commands will opt in through bundle.yaml:

agent:
  expose: true
  mode: query
  description: Reports doors missing company-standard parameters.
  inputs:
    level:
      type: string
      description: Level name to limit the check to
      required: false
  returns: List of door ids with the missing parameter names
  • forms prompts read agent.inputs[key]. When an input is missing they raise InputRequired, and the tool returns needs_input with the options so the agent can call again.
  • Output-window markdown and tables become text. __result__ and agent.result() become structured data.

Phases

Phase Deliverable Status
0: Spike In-Revit host, pipe, guarded run, script runner, test client Done on Revit 2024 (IronPython 3.4). CPython and Revit 2026 passes pending.
1: MCP MVP pyrevit mcp, pyrevit agent, pyrevit configs agent, mcp install, get_context, inspect_elements, lookup_revit_api, run_query, run_modify, get_run, policy In progress
2: Writes Ranked change sets with before/after values, cancellation (timeout_s is done) In progress
3: Commands agent: bundle block, list_commands / run_command, forms agent mode Planned
4: Authoring save_as_command (promote a script to a ribbon button), shipped agent skill Planned
5: Scale batch_run across models, persistent script sessions Planned

Open questions

  • Model data leaves the machine through the LLM provider; an admin lock for the [agent] settings should be available.
  • Whether the CPython engine is mature enough to be the default for agent scripts.
  • How to relate to the third-party mcp-server-for-revit-python extension.
  • Behaviour on workshared models with elements owned by other users.

Phase 0 results

Validated on Revit 2024 (build 24.1.11.26), IronPython 3.4.2, Snowdon Towers sample:

Check Result
Pipe, ExternalEvent, main-thread queue Works; warm runs take under 1 s
DocumentChanged inside a TransactionGroup Fires for every inner commit, so query detection and change sets work
Query and dry-run rollback Verified in the model afterwards
Warning capture Overlap warning recorded and removed, no popup
Dialog capture API TaskDialog dismissed and reported. DialogBoxShowing fires for API dialogs too, so the host disarms it before its own prompt.
Save blocking Revit itself refuses to save while an API transaction group is open; the DocumentSaving cancel covers runs without a group
Script error Rolled back, traceback points at the script line
Transaction left open Detected; Revit cleans up the open transaction
Modify, Keep / Discard Committed or rolled back as chosen, verified in the model
Isolate preview View isolation cleared after the prompt

In-Revit agent tests

The pyRevitDevTools extension has an Agent Tests pulldown under pyRevitDev > Debug. Its buttons drive the host from inside Revit through AgentHost.HandleRequest, which runs the same JSON-RPC requests the pipe carries synchronously on the Revit main thread. A command can't use the pipe itself: the pipe executes requests through an ExternalEvent that never fires while a command runs.

Button Covers
Run Agent Tests get_context, inspect_elements, show, capture, lookup_api; what each request refuses (invalid parameters, unsupported or closed views); query runs on both engines (results, error lines, timeouts, workspaces, records, dismissed dialogs); modify runs (decisions, change sets, rollback, open transactions, save and export blocks, other documents, warnings, readonly, a policy changed during the run or outside Revit); the agent settings; the real named pipe (ping, lookup_api, errors, revit_busy while a command runs)
Run Agent Tests (CPython) The same tests started from a CPython command, which also checks that the command survives nested agent runs
Run Agent Library Tests The shipped and user skills, lookup_pyrevit_api and list_automation through a short-lived pyrevit mcp process, and the pyrevitlib side of the automation operations on both engines
Run Agent Approval Tests The ask prompt's Keep and Discard, answered through Revit's DialogBoxShowing event; if a prompt stays on screen, click the button its title names

The tests create two scratch projects in the temp folder, set the policy to auto (or ask for the approval tests) while they run, then close the projects without saving, reactivate the document that was open before, and restore the policy. The settings tests change the real pyRevit config and put every value back. The pipe tests are skipped when the host is disabled, and the other tests have only been run with it enabled. Each test is written to %TEMP%\pyrevit-agent-tests.log before it starts, so if Revit ever hangs, the last line of that file names the test that was running.

run_automation, navigate_revit_link and the hints the MCP server adds to run errors call back into Revit through the pipe, which can't answer while a command runs, so these buttons don't cover them.