Repository navigation
Python: Monty: run guest code off the event loop and add a default execution limit - #10312
Conversation
… default execution limit Run Monty guest code through the asynchronous pool API (AsyncMonty and the async snapshot types) so it executes in worker processes instead of on the event-loop thread. Cancelling or timing out InlineCodeBridge.run discards the worker. Apply a default 30 second max_feed_duration_secs when resource_limits sets neither max_feed_duration_secs nor max_duration_secs. An explicit None removes it. The resource_limits property and serializable state keep the caller-supplied value. Convert the unit-test fakes to the async API and add real-runtime tests for event-loop responsiveness, timeout recovery, and the default limit.
There was a problem hiding this comment.
🟡 Changes recommended
The new test callbacks violate the coding standard requiring non-fixture helpers in explicitly imported helper modules and need to be moved into a helper module or restructured as fixtures.
1 open issue
What changed in this PR
This PR moves the Python Monty integration to asynchronous worker-process execution and introduces a configurable 30-second guest execution limit.
Changes:
- Await guest execution and snapshot resumes to keep the event loop responsive.
- Apply the default execution budget at run time, preserving explicit limits, opt-outs, and serialized configuration.
- Update tests and documentation for asynchronous execution, cancellation recovery, and concurrent mount behavior.
| File | Description |
|---|---|
python/packages/monty/tests/monty/test_monty_runtime.py |
Adds runtime checks for responsiveness, timeout recovery, and the default limit. |
python/packages/monty/tests/monty/test_monty_codeact.py |
Converts fakes to async APIs and tests limit configuration. |
python/packages/monty/README.md |
Documents worker execution, limits, and shared-mount concurrency. |
python/packages/monty/AGENTS.md |
Updates contributor guidance for async execution and runtime defaults. |
python/packages/monty/agent_framework_monty/_provider.py |
Documents the provider’s default execution limit. |
python/packages/monty/agent_framework_monty/_monty_bridge.py |
Uses async Monty execution and applies the default budget. |
python/packages/monty/agent_framework_monty/_execute_code_tool.py |
Clarifies limits, opt-out behavior, and concurrent execution. |
Review effort: Balanced
There was a problem hiding this comment.
MAF Automated Review — Iteration 1
Result: Findings reported
Scope: full PR (1 commit(s)): b1967dc3e6f8
Model: gpt-5.6-sol
Overview
The change consistently moves feed and resume operations onto Monty's async worker API, adds a tested 30-second default guest budget, and uses async context managers to discard workers on cancellation. The new concurrency model nevertheless permits cross-request file capture when writable mounts are shared and creates one unbounded worker pool per concurrent execution. Those residual isolation and admission-control risks are captured below.
Reviewed the supplied pull-request change set across correctness, security/reliability, architecture, and failure behavior.
2 verified findings remained after source verification (1 high, 1 medium) across 1 file. Details are attached to the affected lines below.
Affected areas: python/packages/monty/agent_framework_monty/_monty_bridge.py
Captured files are attributed by comparing read-write mounts before and after a run. With guest code now running off the event loop, overlapping runs on one tool could capture each other's written files. Hold a per-tool lock for runs on a tool that has a read-write mount, including workspace_root. Runs without one still overlap. The lock is created on first use and excluded from copies and pickles, because it binds to an event loop once it is contended. Document that each concurrent run starts its own worker process.
A full copy or pickle of the tool also depends on global telemetry state that other tests in the same process change, so that check failed in the full suite. Test the state hooks directly instead: the lock is left out of the state and a restored tool still runs.
…as removal Name the legacy max_duration_secs alias only where the default limit rule needs it, and note that the alias will be removed soon.
… runs Captured files are attributed by comparing read-write mounts before and after a run. The per-tool lock did not cover the runs of one provider, because every agent run gets its own run-scoped tool, so concurrent runs could still capture each other's written files. Replace the lock with a MountGate that a tool shares with the run-scoped tools it creates. The gate serves calls in arrival order and works across event loops and threads, so a tool can be reused from another loop. A call that a host tool starts while its own call has the turn is part of that call and does not wait, so a sub-agent that uses the same provider cannot block on its caller. A waiting call fails with an error after 10 minutes. Read the tool and mount configuration once a call has its turn. Document the scope, the wait limit, and that a slow host tool holds up later calls.
There was a problem hiding this comment.
MAF Automated Review — Iteration 2
Result: Findings reported
Scope: 4 net-new commit(s): d71f985d2e13, 33b3a53c0840, d97618de7c8e, d89601c70617
Model: gpt-5.6-sol
Overview
This iteration adds a FIFO mount gate shared by provider run tools, with explicit timeout, cancellation, cross-loop, and handoff coverage. Those guards serialize ordinary writable-mount calls and protect their pre/post capture windows. Two residual lifecycle gaps remain: an inherited background run can outlive the hold it bypassed and overlap a later request, while a host-tool wait can monopolize the provider's gate indefinitely.
Reviewed the supplied incremental change set across correctness, security/reliability, architecture, and failure behavior.
2 verified findings remained after source verification (1 high, 1 medium) across 2 files. Details are attached to the affected lines below.
Affected areas: python/packages/monty/agent_framework_monty/_execute_code_tool.py, python/packages/monty/agent_framework_monty/_mount_gate.py
…e it have ended A host tool can start a call in the background that outlives the call that had the turn. The gate used to pass the turn on when the first call ended, so a later call could overlap with the background call and capture its files. The gate now counts the running calls of a turn and passes the turn on only when the last one has ended. A call started by a call that is still running joins the turn, also after the first call has ended.

Motivation & Context
InlineCodeBridgeran Monty guest code through the synchronous pool API, so the guest executed on the event-loop thread. While a guest program ran, other tasks in the same process could not make progress, andasyncio.wait_foror task cancellation could not interrupt it. The bridge also applied no default execution limit, so a guest program that never finishes ran until the process was stopped.This change runs guest code through Monty's asynchronous pool API and applies a default 30-second guest execution budget, matching the default of the Hyperlight integration (
execution_timeout_seconds).Description & Review Guide
What are the major changes?
InlineCodeBridge.runusesAsyncMontyand the async snapshot types, and awaits everyfeed_start,resumeandresume_auto. Guest code runs in Monty worker processes, the event loop stays responsive, and cancelling or timing outrundiscards the worker._handle_function,_schedule_direct_tooland_schedule_call_toolare nowasync.max_feed_duration_secsof 30 seconds applies whenresource_limitssets neithermax_feed_duration_secsnor the legacymax_duration_secs. An explicitNoneremoves it. The default is applied at run time in the bridge, so theresource_limitsproperty and the serializable state keep the caller-supplied value.AGENTS.mdand docstrings describe the default, the opt-out, and that concurrentexecute_codecalls overlap.MountGate(_mount_gate.py). Captured files are attributed by comparing the mount before and after a run, so overlapping runs would otherwise capture each other's files. A tool hands its gate to the run-scoped tool thatcreate_run_tool()builds for each agent run, so all runs of one provider share it. The gate is first-in-first-out and works across event loops and threads, so a tool can be reused from another loop. A call that a host tool starts while its own call has the turn (for example a sub-agent that uses the same provider) is part of that call and does not wait. The turn lasts until the call and every call it started have ended, even when a host tool leaves one running in the background. A call waits at most 10 minutes for its turn and then returns an error. Tools and mounts are read once a call has its turn. A copy of a tool shares its gate, and a pickled tool gets a fresh one.test_mount_gate.pytests the gate on its own: ordering, exclusion across loops and threads, timeout and cancellation hand-over, nested calls and calls left running in the background, and copy and pickle.Usage before and after:
What is the impact of these changes?
None. The call returns anExecution error; withinclude_detailed_errors=Truethe details readTimeoutError: feed time limit exceeded. The 30-second default is a maintainer decision.call_tool,asyncio.gatherfan-out, OS-call rejection, mounts) use the async API and return the same results. Limit errors keep their type and message.execute_codecalls now overlap and each starts its own worker process, so applications that serve many callers should bound concurrency themselves. Calls that can write to the same host files take turns: the calls on one tool and all runs of one provider. Separate tools or providers that mount the same host directory do not coordinate, so each needs its own directory. A slow host tool holds up the calls behind it, and a waiting call fails after 10 minutes.MontyExecuteCodeToolgains an internal_mount_gateargument next to the existing_fides_warned_metadata). Thepydantic-montyrange is unchanged; the async API exists since 1.0.What do you want reviewers to focus on? Which
resource_limitskeys suppress the default, the cancellation and cleanup behavior ofInlineCodeBridge.run, and the turn-taking gate in_mount_gate.py(in particular the fixed 10-minute wait limit and that a call has no limit on how long it keeps its turn).Related Issue
N/A - no tracking issue; this is a maintainer-authored change.
AI Assistance
AI assistance details: implementation, tests, documentation, and this PR description were AI-assisted (GitHub Copilot).
Contribution Checklist
breaking changelabel (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.