How Python-Backed Skills Expose Async Callables to the Prime Agent Kernel

Python-backed skills in Prime Agent expose async callables by exporting an async def run function that the kernel automatically detects and awaits using inspect.isawaitable inside an asyncio.run wrapper.

Prime Agent treats each skill as a small Python package with a single entry point: a callable named run. This design lets skill authors write either synchronous or asynchronous code without worrying about event-loop management. According to the Prime Intellect source code, the kernel handles all the complexity of dispatching and awaiting async skills transparently.

The Skill Contract: Exporting run

Every Python skill must expose exactly one symbol: a callable named run. The kernel imposes no restrictions on whether this callable is synchronous or asynchronous.


# packages/coding-agent/test/fixtures/skills/python-skill/src/python_skill/__init__.py

async def run(value: str = "ok") -> str:
    """Return the provided value."""
    return value

The file path matters. The skill name derived from the console-script must match the Python import name exactly—dashes become underscores. For a script named python-skill, the module python_skill is imported.

How the Kernel Detects and Awaits Async Callables

The runtime in prime-agent-runtime/src/rlm/skill.py executes skills through a seven-step pipeline:

  1. Resolve module name — Strip the extension from the console-script name and normalize dashes to underscores (cli())
  2. Import the module — __import__(prog) loads the skill; import errors become RuntimeError with context (cli())
  3. Retrieve run — getattr(module, "run", None) fetches the callable; non-callable values raise RuntimeError (cli())
  4. Parse CLI arguments — tyro.cli(skill_fn, args=argv) maps command-line arguments to function parameters (run_cli())
  5. Detect awaitable — inspect.isawaitable(result) checks if the call returned a coroutine (run_cli())
  6. Await if necessary — await result runs the coroutine to completion (run_cli())
  7. Drive event loop — asyncio.run(run_cli(...)) manages the async execution (cli())

Synchronous skills skip steps 5–6 because inspect.isawaitable returns False for regular return values.

Real-World Async Skill Example

The websearch skill demonstrates production-grade async I/O:


# packages/coding-agent/skills/websearch/src/websearch/websearch.py

import aiohttp
import json
from typing import List

async def run(query: str) -> List[str]:
    """Perform a web search and return the top result URLs."""
    async with aiohttp.ClientSession() as session:
        async with session.get(
            "https://api.duckduckgo.com/",
            params={"q": query, "format": "json"},
        ) as resp:
            data = await resp.json()
    return [result["FirstURL"] for result in data.get("Results", [])][:5]

This skill performs HTTP requests without blocking, yet the kernel consumes it identically to a synchronous skill.

Internal Invocation Flow

The kernel's simplified execution path:

import asyncio
import tyro
import inspect

async def _run_skill(module_name: str, argv):
    module = __import__(module_name)
    skill_fn = getattr(module, "run")
    result = tyro.cli(skill_fn, args=argv)   # parses argv → function call

    if inspect.isawaitable(result):
        result = await result
    return result

# Top-level entry point

asyncio.run(_run_skill("websearch", ["--query", "prime agent"]))

The tyro.cli integration provides automatic CLI generation from type hints, making skills self-documenting.

Key Source Files

Summary

  • Skills export a single run callable—sync or async—at module level
  • The kernel uses inspect.isawaitable to detect coroutines and await to resolve them
  • asyncio.run wraps execution so skill authors need no event-loop boilerplate
  • tyro.cli bridges CLI arguments to function parameters automatically
  • The pattern in prime-agent-runtime/src/rlm/skill.py makes async skills indistinguishable from sync ones at the call site

Frequently Asked Questions

Does the kernel require any special decorators or markers for async skills?

No. The kernel detects async automatically via inspect.isawaitable. You simply write async def run instead of def run—no decorators, registration, or configuration changes are needed.

What happens if a sync skill is called through the async path?

Synchronous skills work unchanged. When inspect.isawaitable(result) returns False, the kernel skips awaiting and returns the value directly. The same asyncio.run wrapper handles both cases.

Can skills use other async libraries like asyncio.gather or trio?

Standard asyncio works natively since the kernel runs asyncio.run. Third-party libraries like trio would need compatibility layers; the runtime as implemented in skill.py does not include alternative event-loop support.

How does argument parsing work for async skills?

Arguments parse identically regardless of sync/async. The kernel calls tyro.cli(skill_fn, args=argv) before awaiting, so type hints and default values on run generate the CLI interface automatically.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →