agentleFS
VercelKnown publishervercel/vercel-py/.agents/skills/iter-coroutine/SKILL.md

iter-coroutine

Skill34 starsChanged 5 months agoMITView on GitHub

Use when refactoring vercel-py sync and async modules to share transport-agnostic business logic with the iter_coroutine pattern, including base/runtime splits and shared HTTP request clients.

The file

We split this skill into the rules it gives an agent

7 rules. About 0 are common, 6 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈6

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Internal core is async-first: All shared business logic lives in async methods on a base class.

    line 23
  2. Distinctive:

    All business logic goes in async methods on a base class:

    line 136
  3. Distinctive:

    When sync and async paths differ materially (e.g., threading vs asyncio for concurrent uploads), use a runtime object:

    line 210
  4. Distinctive:

    Keep these in the base layer, not duplicated per runtime:

    line 268
  5. Very specific:

    Prefer integration-style tests with respx that verify real request flow and sync/async parity.

    line 298
  6. Distinctive:

    Do not rely only on monkeypatch tests that assert internal call shape.

    line 300
  7. Distinctive:

    Test both sync and async paths for every operation.

    line 301

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Iter-Coroutine + Base/Runtime Migration

Refactor sync+async modules to eliminate duplication using the iter-coroutine pattern. This skill covers the full migration workflow: identifying candidates, structuring internal modules, writing transport-agnostic business logic, and wiring up sync/async public APIs.

When to use

  • A module has parallel sync and async implementations with duplicated logic.
  • You're adding a new feature that needs both sync and async public APIs.
  • You're refactoring an existing feature to use the shared HTTP transport layer.

Core principles

  • Public API is stable. Same exported names, signatures, return types, and behavior. Migrations are internal refactors, not API changes.
  • Internal core is async-first. All shared business logic lives in async methods on a base class.
  • Sync entrypoints are thin wrappers over the async core via iter_coroutine(...), used only when the wrapped coroutine is non-suspending in sync mode.
  • HTTP goes through vercel._internal.http transports. Never construct httpx.Client or httpx.AsyncClient directly in feature modules.

Architecture overview

Public API (sync wrappers + async functions)
    │
    │ iter_coroutine() one-shot bridge (sync path)
    │ await (async path)
    ▼
Concrete Clients (SyncFooClient / AsyncFooClient)
    │
    │ inherit from
    ▼
BaseFooClient (all async methods — shared business logic)
    │
    │ uses
    ▼
RequestClient (transport-agnostic async API)
    │
    ▼
Transport (SyncTransport or AsyncTransport)
    │
    ▼
httpx.Client or httpx.AsyncClient

How iter_coroutine works

# src/vercel/_internal/iter_coroutine.py
def iter_coroutine(coro: Coroutine[None, None, _T]) -> _T:
    """Execute a non-suspending coroutine synchronously."""
    try:
        coro.send(None)
    except StopIteration as ex:
        return ex.value
    else:
        raise RuntimeError(f"coroutine {coro!r} did not stop after one iteration!")
    finally:
        coro.close()

It drives a coroutine forward exactly one step. If the coroutine completes without suspending, its return value is extracted from the StopIteration exception. If it tries to yield/await, it raises RuntimeError.

Safety rules

SafeUnsafe
Coroutines that only call sync code via async APICoroutines that await real I/O
SyncTransport.send() (sync under async facade)AsyncTransport.send() (real await)
Sync sleep, sync file I/Oasyncio.sleep, asyncio.create_task
inspect.isawaitable() guarded branchesUnguarded await on user callbacks

Step-by-step migration

1. Create the transport-agnostic request client

The request client wraps a BaseTransport and exposes an async API. The transport implementation determines whether I/O is actually sync or async.

# src/vercel/_internal/foo/core.py
from vercel._internal.http import (
    AsyncTransport, BaseTransport, SyncTransport,
    create_base_client, create_base_async_client,
)

class FooRequestClient:
    def __init__(
        self,
        *,
        transport: BaseTransport,
        sleep_fn: SleepFn = asyncio.sleep,
        await_callback: bool = True,
    ) -> None:
        self._transport = transport
        self._sleep_fn = sleep_fn
        self._await_callback = await_callback

    async def request(self, method: str, path: str, **kwargs) -> Any:
        # Retry logic, error mapping, progress callbacks — all async-shaped
        # but non-suspending when backed by SyncTransport
        resp = await self._transport.send(method, path, **kwargs)
        ...

Factory functions configure transport + sleep + callback behavior:

def create_sync_request_client(timeout: float = 30.0) -> FooRequestClient:
    transport = SyncTransport(create_base_client(timeout=timeout))
    return FooRequestClient(
        transport=transport,
        sleep_fn=_sync_sleep,         # time.sleep, not asyncio.sleep
        await_callback=False,          # don't await user callbacks
    )

def create_async_request_client(timeout: float = 30.0) -> FooRequestClient:
    transport = AsyncTransport(create_base_async_client(timeout=timeout))
    return FooRequestClient(transport=transport)  # defaults are async-friendly

2. Write the shared base client

All business logic goes in async methods on a base class:

class BaseFooClient:
    def __init__(self, *, request_client: FooRequestClient) -> None:
        self._request_client = request_client

    async def do_thing(self, path: str, *, token: str) -> Result:
        # Validation, request building, response parsing — all here
        raw = await self._request_client.request("POST", path, token=token)
        return build_result(raw)

3. Add sync and async concrete clients

class SyncFooClient(BaseFooClient):
    def __init__(self) -> None:
        super().__init__(request_client=create_sync_request_client())

    def close(self) -> None:
        self._request_client.close()

    def __enter__(self) -> SyncFooClient:
        return self

    def __exit__(self, *args: object) -> None:
        self.close()


class AsyncFooClient(BaseFooClient):
    def __init__(self) -> None:
        super().__init__(request_client=create_async_request_client())

    async def aclose(self) -> None:
        await self._request_client.aclose()

    async def __aenter__(self) -> AsyncFooClient:
        return self

    async def __aexit__(self, *args: object) -> None:
        await self.aclose()

4. Wire up public API functions

# src/vercel/foo/ops.py
from vercel._internal.iter_coroutine import iter_coroutine

_T = TypeVar("_T")

def _run_sync(
    operation: Callable[[SyncFooClient], Coroutine[None, None, _T]],
) -> _T:
    with SyncFooClient() as client:
        return iter_coroutine(operation(client))


# Sync public API
def do_thing(path: str, *, token: str | None = None) -> Result:
    token = ensure_token(token)
    return _run_sync(lambda c: c.do_thing(path, token=token))


# Async public API
async def do_thing_async(path: str, *, token: str | None = None) -> Result:
    token = ensure_token(token)
    async with AsyncFooClient() as client:
        return await client.do_thing(path, token=token)

Base + runtime split

When sync and async paths differ materially (e.g., threading vs asyncio for concurrent uploads), use a runtime object:

# Sync runtime — ThreadPoolExecutor
class _SyncUploadRuntime:
    def upload(self, *, parts, upload_fn, ...) -> list[PartResult]:
        with ThreadPoolExecutor(max_workers=MAX_CONCURRENCY) as executor:
            # Submit work, collect results
            ...

# Async runtime — anyio task group
class _AsyncUploadRuntime:
    async def upload(self, *, parts, upload_fn, ...) -> list[PartResult]:
        semaphore = anyio.Semaphore(MAX_CONCURRENCY)
        async with anyio.create_task_group() as tg:
            # Start tasks with semaphore limiting
            ...

The base client calls the runtime polymorphically:

class BaseFooClient:
    async def _do_upload(self, ...) -> list[PartResult]:
        parts = await _await_if_necessary(
            self._runtime.upload(
                parts=chunks,
                upload_fn=self._make_upload_fn(),
                ...
            )
        )
        return order_parts(parts)

Concrete clients inject the right runtime and upload function:

class SyncFooClient(BaseFooClient):
    def __init__(self) -> None:
        super().__init__(runtime=create_sync_runtime())

    def _make_upload_fn(self):
        # Wrap async method with iter_coroutine for sync execution
        return lambda **kw: iter_coroutine(self._client.upload_part(**kw))


class AsyncFooClient(BaseFooClient):
    def __init__(self) -> None:
        super().__init__(runtime=create_async_runtime())

    def _make_upload_fn(self):
        # Return the async method directly
        return self._client.upload_part

Shared orchestration

Keep these in the base layer, not duplicated per runtime:

  • Input validation
  • Chunk/part iteration helpers
  • Result normalization and ordering
  • Final response shaping

Callback handling

For callbacks that may be sync or async:

async def _emit_progress(
    callback: Callable[[Event], None] | Callable[[Event], Awaitable[None]] | None,
    event: Event,
    *,
    await_callback: bool,
) -> None:
    if callback is None:
        return
    result = callback(event)
    if await_callback and inspect.isawaitable(result):
        await cast(Awaitable[None], result)
  • Sync clients set await_callback=False — callbacks are always sync.
  • Async clients set await_callback=True — callbacks may be awaitable.

Testing expectations

  • Prefer integration-style tests with respx that verify real request flow and sync/async parity.
  • Do not rely only on monkeypatch tests that assert internal call shape.
  • Test both sync and async paths for every operation.
  • Validate before commit:
    • ./scripts/lint.sh
    • ./scripts/test.sh (targeted or full)

Reference implementation

The blob module is the canonical example of this pattern:

  • Transport: src/vercel/_internal/http/transport.py
  • Request client: src/vercel/_internal/blob/core.py → BlobRequestClient
  • Base client: src/vercel/_internal/blob/core.py → BaseBlobOpsClient
  • Sync client: src/vercel/_internal/blob/core.py → SyncBlobOpsClient
  • Async client: src/vercel/_internal/blob/core.py → AsyncBlobOpsClient
  • Runtime split: src/vercel/_internal/blob/multipart.py
  • Public API: src/vercel/blob/ops.py
  • Types re-export: src/vercel/blob/types.py

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.