Skip to content

fix(decisioning): create_adcp_server_from_platform should not call asyncio.run() from sync init path #700

Description

@bokelley

Surfaced while building #694 (SellerA2AClient).

Problem

create_adcp_server_from_platform() calls asyncio.run() internally (via validate_capabilities_response_shape). That means: the function cannot be called from any context that is already inside a running event loop.

SellerA2AClient._ensure_executor works around this:

async def _ensure_executor(self) -> Any:
    async with self._executor_lock:
        if self._executor is None:
            # create_adcp_server_from_platform calls asyncio.run() internally
            # (via validate_capabilities_response_shape) — must run in a thread
            # to avoid "cannot be called from a running event loop".
            self._executor = await asyncio.to_thread(self._build_executor_sync)
    return self._executor

Every adopter who builds their own server wiring inside an async fixture or a lifespan handler will hit the same RuntimeError: asyncio.run() cannot be called from a running event loop and either burn time debugging it or copy this to_thread workaround.

Root-cause framing

The constructor of a server object should not perform async I/O. If validation requires async work, expose it as await server.validate_capabilities() and let the caller decide when to run it (during lifespan startup, during a CI gate, never).

Proposed shapes

Pick one:

  1. validate_at_init flag (default True for back-compat) — caller opts into the asyncio.run behavior. Pass False from any async context.
  2. Split validation outcreate_adcp_server_from_platform is sync and does not validate; await server.validate_capabilities() is the explicit async step.
  3. Detect running loop — if asyncio.get_running_loop() succeeds, schedule validation as a task on the existing loop and await its completion. Adds asyncness to the function signature, which is a breaking change.

(2) is the cleanest long-term. (1) is the smallest patch.

Acceptance

  • create_adcp_server_from_platform() callable from inside a running event loop without to_thread
  • Existing sync-init callers continue to work
  • SellerA2AClient._ensure_executor can drop the to_thread workaround
  • Capability-shape validation still runs (just not unconditionally during construction)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions