4. Runnables & Transport
Expose capabilities through one declaration. The kernel generates REST, MCP, and CLI bindings.
The Gap
So far, other Python code calls checker.check() directly. But what if you want it callable via REST API? Or as an MCP tool? Or as a CLI command? Without the kernel, you’d write three implementations. With the kernel: declare it once.
@runnable — Schema Declaration
from pydantic import BaseModel
from signalpy.kernel import runnable
class CheckParams(BaseModel):
text: str
language: str = "EN"
@component("checker")
@requires(dictionaries=list[IDictionary])
class SpellChecker:
@runnable("check", params=CheckParams, description="Check spelling")
async def check_text(self, params):
misspelled = [w for w in params.text.split()
if not self.by_language[params.language].check_word(w)]
return {"misspelled": misspelled}@runnable declares a callable schema — a name ("check"), a typed param schema (CheckParams), and a description. Both def and async def work. Transport adapters discover these schemas via kernel.runnables() and call the handler directly.
Calling Other Components
Use @requires and direct method calls — no string-based dispatch:
@component("search-app")
@requires(checker=ISpellChecker)
class SearchApp:
async def search(self, query: str):
errors = await self.rt.checker.check_text(CheckParams(text=query))
# ... use errorsComponents call each other through @requires injection and direct method calls. The kernel wires dependencies; you call methods on the injected service. No string-based invoke() needed.
Transport Config on @component
To expose runnables via REST, MCP, or CLI, declare transport config directly on @component:
@component("checker",
rest={"prefix": "/spell", "version": "v1"}, # → POST /api/v1/spell/check
mcp={"name": "spell-tools"}, # → MCP tool "checker.check"
cli={"prefix": "spell"}) # → spell check --text "hello"
@requires(dictionaries=list[IDictionary])
class SpellChecker:
@runnable("check", params=CheckParams, description="Check spelling")
async def check_text(self, params): ...The component never knows how it’s called. @runnable declares a schema. Transport config on @component tells transport adapters — components that render REST/MCP/CLI surfaces, covered in tutorial 5 — which runnables to expose. REST endpoints, MCP tools, and CLI commands are generated from the same declaration.
Native-Only Runnables
Some operations should be callable within the kernel but not exposed via external transports:
@runnable("_warmup", params=BaseModel, transports=["native"], description="Warm cache")
async def warmup(self, params): ...transports=["native"] restricts the runnable to in-kernel callers only. Transport adapters skip it when building external surfaces.
Typed Params
Params are Pydantic models. They give you validation, defaults, auto-generated schemas, and attribute access (params.text, not params["text"]).