Skip to content

Python: Monty: run guest code off the event loop and add a default execution limit - #10312

Merged
Eduard van Valkenburg (eavanvalkenburg) merged 6 commits into
microsoft:mainfrom
eavanvalkenburg:monty-async-execution-limit
Nov 13, 2026
Merged

Eduard van Valkenburg (eavanvalkenburg) merged 6 commits into
microsoft:mainfrom
eavanvalkenburg:monty-async-execution-limit

Conversation

@eavanvalkenburg

@eavanvalkenburg Eduard van Valkenburg (eavanvalkenburg) commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

Motivation & Context

InlineCodeBridge ran 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, and asyncio.wait_for or 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.run uses AsyncMonty and the async snapshot types, and awaits every feed_start, resume and resume_auto. Guest code runs in Monty worker processes, the event loop stays responsive, and cancelling or timing out run discards the worker. _handle_function, _schedule_direct_tool and _schedule_call_tool are now async.
    • A default max_feed_duration_secs of 30 seconds applies when resource_limits sets neither max_feed_duration_secs nor the legacy max_duration_secs. An explicit None removes it. The default is applied at run time in the bridge, so the resource_limits property and the serializable state keep the caller-supplied value.
    • README, AGENTS.md and docstrings describe the default, the opt-out, and that concurrent execute_code calls overlap.
    • Calls that can write to the same host files take turns through a new internal 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 that create_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.
    • Tests: the unit-test fakes use the async API. New real-runtime tests cover event-loop responsiveness on the feed and resume paths (host tool, OS error, mount read), timeout and cancellation recovery, the default limit, runs of one provider taking turns, a host tool that starts another run, the wait limit, and reuse from several event loops. test_mount_gate.py tests 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:

    from agent_framework.monty import MontyExecuteCodeTool
    
    # Before: guest code ran on the event-loop thread with no default limit.
    # After: guest code runs in a worker process and is limited to 30 s of guest execution by default.
    execute_code = MontyExecuteCodeTool()
    
    # Raise or remove the limit explicitly.
    execute_code = MontyExecuteCodeTool(resource_limits={"max_feed_duration_secs": 120.0})
    execute_code = MontyExecuteCodeTool(resource_limits={"max_feed_duration_secs": None})
  • What is the impact of these changes?

    • Behavior change: guest code that needs more than 30 seconds of guest execution time (time waiting on host tools is excluded) now fails unless the limit is raised or set to None. The call returns an Execution error; with include_detailed_errors=True the details read TimeoutError: feed time limit exceeded. The 30-second default is a maintainer decision.
    • All execution modes (direct typed calls, call_tool, asyncio.gather fan-out, OS-call rejection, mounts) use the async API and return the same results. Limit errors keep their type and message.
    • Concurrent execute_code calls 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.
    • No public signature change (MontyExecuteCodeTool gains an internal _mount_gate argument next to the existing _fides_warned_metadata). The pydantic-monty range is unchanged; the async API exists since 1.0.
  • What do you want reviewers to focus on? Which resource_limits keys suppress the default, the cancellation and cleanup behavior of InlineCodeBridge.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

  • No material AI assistance was used.
  • This is an AI-assisted contribution. I reviewed, understood, and verified all submitted content and accept responsibility for it.

AI assistance details: implementation, tests, documentation, and this PR description were AI-assisted (GitHub Copilot).

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR links to an agreed issue with no competing open PR, or the Related Issue section documents a trivial-change or repository-automation exception.
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

… 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.
Copilot AI balanced review requested due to automatic review settings November 12, 2026 16:13
@agent-framework-automation agent-framework-automation Bot added documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python breaking change Usage: [PRs], Target: all PRs that introduce changes that are not backward compatible labels Nov 12, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@eavanvalkenburg Eduard van Valkenburg (eavanvalkenburg) removed the breaking change Usage: [PRs], Target: all PRs that introduce changes that are not backward compatible label Nov 13, 2026
@eavanvalkenburg Eduard van Valkenburg (eavanvalkenburg) changed the title Python: [BREAKING] Monty: run guest code off the event loop and add a default execution limit Python: Monty: run guest code off the event loop and add a default execution limit Nov 13, 2026
@eavanvalkenburg
Eduard van Valkenburg (eavanvalkenburg) marked this pull request as ready for review November 13, 2026 10:42

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Merged via the queue into microsoft:main with commit 79a76a1 Nov 13, 2026
64 of 66 checks passed
@eavanvalkenburg
Eduard van Valkenburg (eavanvalkenburg) deleted the monty-async-execution-limit branch November 13, 2026 16:39

This branch was successfully deployed

1 active deployment
github-app-auth — a2a67815 Deployed Nov 13, 2026 by eavanvalkenburg via add_label #27797
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants