Skip to content

Remove nest_asyncio - #390

Draft
CodexVeritas wants to merge 2 commits into
mainfrom
claude/remove-nest-asyncio-f923d5
Draft

CodexVeritas wants to merge 2 commits into
mainfrom
claude/remove-nest-asyncio-f923d5

Conversation

@CodexVeritas

Copy link
Copy Markdown
Collaborator

Summary

Removes the nest-asyncio dependency and its four nest_asyncio.apply() calls, including the one that ran on import forecasting_tools. The public sync API keeps working.

Changes

  • New forecasting_tools.util.event_loops.run_coroutine_synchronously. Every sync wrapper that used to call asyncio.run(...) now calls this.
    • With no running loop, it just calls asyncio.run.
    • Inside a running loop (Jupyter, or async code calling a sync wrapper), it runs the coroutine on a new loop in a worker thread.
    • It copies context vars into that thread, so MonetaryCostManager and HardLimitManager limits still apply.
    • It logs a warning that points to the async version.
  • New async variants: MetaculusClient.get_all_open_questions_from_tournament_async, MetaculusClient.get_benchmark_questions_async, and async_batching.run_coroutines_while_removing_and_logging_exceptions_async. The sync versions now wrap these.
  • Internal async code now awaits instead of calling sync wrappers. This covers ForecastBot.forecast_on_tournament, question gathering in run_bots.py, Benchmarker.run_benchmark, the Q4 template and Q3 AskNews bots, the key-factors and niche-list researchers, the research coordinator, and the simulator and base-rate Streamlit pages.
  • async_batching.run_coroutines no longer uses asyncio.get_event_loop(). Without the patch, that call fails after an earlier asyncio.run, and on Python 3.14 it fails when no loop exists.
  • Template bots (2025 fall through 2026 fall) run tournament mode in one asyncio.run covering both the seasonal tournament and MiniBench. Their class-level asyncio.Semaphore binds to the first event loop it is contended in. With two asyncio.run calls, the second one raises RuntimeError: ... is bound to a different event loop.
  • The README examples use the async tournament fetch.
  • uv.lock only loses the two nest-asyncio lines under forecasting-tools, and uv lock --check passes. nest-asyncio stays in the lock as a dependency of firecrawl-py, but nothing imports it.

Behavior changes for library users

  1. asyncio.run() inside Jupyter no longer works. The patch is what made it reentrant. Use top-level await instead, or pip install nest_asyncio and call nest_asyncio.apply() in the notebook.
  2. Scripts that call asyncio.run() more than once while sharing a contended asyncio primitive now fail. Before, every asyncio.run in a thread reused the same loop. The common case is a copy of an older template-bot main.py with a class-level _concurrency_limiter = asyncio.Semaphore(...) that runs the seasonal tournament and MiniBench in two separate asyncio.run calls. The fix is to run both in one asyncio.run, as the templates in this PR now do.
  3. Sync wrappers called from async code now block the running loop until they finish. Previously other tasks kept running in the meantime. Each call logs a warning naming the coroutine, pointing to the _async version.
  4. Mocks of the sync methods no longer intercept internal calls. forecast_on_tournament now calls get_all_open_questions_from_tournament_async, and Benchmarker now calls get_benchmark_questions_async.
  5. Sync agent tools (@agent_tool functions that call asyncio.run) depend on the openai-agents version. Versions that run sync tools in a worker thread, such as 0.20, see no change. Older versions now go through the worker-thread path instead of relying on the patch.

Because of 1 and 2, this probably warrants a minor version bump (0.4.0) so ^0.3 pins don't pick it up automatically. The public bot template's main.py should be updated at the same time.

Testing

  • Unit suite: 1039 passed, 141 skipped. On main with nest_asyncio stubbed out, 41 unit tests fail (run_bots question gathering, async_batching, the refreshing bucket rate limiter, hard limit managers). All of them pass on this branch.
  • New test_event_loops.py covers:
    • the no-loop and running-loop paths
    • exception propagation
    • cost-manager context vars carrying over to the worker thread
    • run_coroutines after an earlier asyncio.run
    • the sync tournament fetch inside a running loop
  • Not run: integration tests, the Streamlit pages, the template bots' __main__ end to end, and Python 3.13/3.14.

🤖 Generated with Claude Code

CodexVeritas and others added 2 commits November 2, 2026 15:30
nest_asyncio was applied globally on import so async code could call sync
wrappers that use asyncio.run. Without it:

- Sync wrappers use a new run_coroutine_synchronously helper. With no running
  loop it is asyncio.run; inside a running loop (Jupyter, async callers) it
  runs the coroutine on a fresh loop in a worker thread, copying context vars
  so cost managers still apply, and logs a warning.
- Internal async code awaits async versions instead of sync wrappers. Adds
  MetaculusClient.get_all_open_questions_from_tournament_async,
  get_benchmark_questions_async, and
  async_batching.run_coroutines_while_removing_and_logging_exceptions_async.
- Template bots run tournament mode (seasonal + minibench) in one event loop,
  since their class-level asyncio.Semaphore can't be reused across loops.

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

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