Skip to content

RuntimeError: dictionary changed size during iteration in _approvals during concurrent tool execution #3515

@introspectDaily

Description

@introspectDaily

Bug Description

When running an agent with require_approval configured and multiple tool calls executing concurrently, a RuntimeError: dictionary changed size during iteration occurs in RunContextWrapper._approvals.

Root Cause

In src/agents/run_context.py, _fork_with_tool_input and _fork_without_tool_input share the _approvals dict by reference:

def _fork_with_tool_input(self, tool_input: Any) -> RunContextWrapper[TContext]:
    fork = RunContextWrapper(context=self.context)
    fork._approvals = self._approvals  # shared reference
    ...

When parallel tool calls run concurrently in an async event loop:

  • One coroutine iterates over _approvals (e.g., in _get_approval_status_for_key)
  • Another coroutine calls _get_or_create_approval_entry, which inserts a new key into the same dict

This causes the RuntimeError because the dict size changes during iteration.

Reproduction

  1. Create an agent with MCP server that has require_approval set as a dict mapping tool names to "always"
  2. Send a prompt that triggers multiple tool calls in the same turn
  3. Use state.approve(item, always_approve=True) to approve tools
  4. The error occurs intermittently when the SDK internally checks approval status for one tool while creating an approval entry for another

Environment

  • openai-agents==0.14.6
  • Python 3.11
  • Using Runner.run_streamed() with MCP tools

Suggested Fix

Either:

  1. Use dict.copy() when iterating in _get_approval_status_for_key and related methods
  2. Or use an asyncio.Lock to protect concurrent access to _approvals

Option 1 (minimal change):

def _get_or_create_approval_entry(self, tool_name: str) -> _ApprovalRecord:
    approval_entry = self._approvals.get(tool_name)
    if approval_entry is None:
        approval_entry = _ApprovalRecord()
        self._approvals[tool_name] = approval_entry
    return approval_entry

The get + conditional set pattern is safe, but the issue is when other methods iterate over self._approvals.items() or similar while this method adds a new key. Wrapping iteration sites with list(self._approvals.items()) would prevent the RuntimeError.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions