Skip to content

feat(testing): SellerA2AClient — max_events constructor parameter for event-drain budget #698

Description

@bokelley

Follow-up to #678 / #694.

Gap

SellerA2AClient.invoke() drains the A2A event queue with a hardcoded budget:

for _ in range(32):
    event = await asyncio.wait_for(queue.dequeue_event(), timeout=timeout_seconds)
    ...

32 is documented as "bounded to a small number of intermediate state events." That's true for the current A2A executor surface, which produces at most a few status transitions before the terminal Task.

Why this matters later

The three deferred follow-ups in #694's PR body — push-notification capture, intermediate-state observer, cancel/resume harness — all increase the per-invoke event count:

  • Intermediate-state observer wants to capture working / input_required transitions over potentially many cycles.
  • Cancel/resume drives task lifecycle that may emit a dozen events for one logical invocation.
  • Push-notification interleaving with task transitions doubles the event count again.

Adding max_events later is a breaking API change for any adopter who has set timeout_seconds and relied on the implicit 32-event cap. Easier to add now.

Proposed

async def invoke(
    self,
    skill: str,
    payload: dict[str, Any] | None = None,
    *,
    timeout_seconds: float = 5.0,
    max_events: int = 32,
) -> ToolInvokeResult:
    ...

Acceptance

  • max_events kwarg on invoke(), default 32 (no behavior change for existing callers)
  • Error message on exhaustion calls out both bounds: "produced no terminal Task within {timeout_seconds}s × {max_events} events"
  • Test that exercises a handler emitting > 32 events with the default cap (raises) and with max_events=128 (succeeds)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions