huxley
Reference

Skill Cheat Sheet

Every SDK primitive a skill author touches. One page, look up fast.

This page is a quick reference. Each primitive has a one-line description and a representative usage. For full explanations, follow the links into Concepts and Build pages.

Imports

from huxley_sdk import (
    # Core protocols (you implement Skill)
    Skill,
    SkillContext,

    # Tool definitions
    ToolDefinition,
    ToolResult,

    # Side effects
    AudioStream,
    PlaySound,
    CancelMedia,
    SetVolume,
    InputClaim,
    ClaimEndReason,

    # Focus / proactive
    InjectPriority,

    # Content type for AudioStream
    ContentType,

    # Errors
    ClaimBusyError,
)
from huxley_sdk.audio import load_pcm_palette
# load_pcm_palette is sync and takes the role names you want to load.
# It returns a {role: pcm_bytes} dict; missing roles are silently skipped.
palette = load_pcm_palette(
    ctx.persona_data_dir / "sounds",
    roles=["news_start", "book_start", "book_end"],
)

The skill protocol

class MySkill:
    name = "my_skill"  # required, must be unique

    @property
    def tools(self) -> list[ToolDefinition]:
        return [...]

    async def setup(self, ctx: SkillContext) -> None: ...
    async def handle(self, tool_name: str, args: dict) -> ToolResult: ...
    async def teardown(self) -> None: ...

Concepts: Skills · Build: First skill

ToolDefinition

ToolDefinition(
    name="search_audiobooks",
    description="Search the user's library by title or author.",
    parameters={
        "type": "object",
        "properties": {
            "query": {"type": "string", "description": "..."},
        },
        "required": ["query"],
    },
)

Build: Tools and parameters

ToolResult

return ToolResult(
    output=json.dumps({"matches": [...]}),  # text the model narrates
    side_effect=PlaySound(pcm=...),          # optional
)

output is a string. JSON is conventional but not required. side_effect is optional and at most one per ToolResult.

Side effects

ClassUse forArgument
PlaySoundOne-shot chime before model audiopcm: bytes
AudioStreamLong-form playback after modelSee full signature below
CancelMediaStop current AudioStream() — no args
SetVolumePush volume change to clientlevel: int (0–100)
InputClaimTake over mic + speakerSee full signature below

AudioStream

AudioStream(
    factory: Callable[[], AsyncIterator[bytes]],   # required
    on_complete_prompt: str | None = None,         # narrate on natural end
    completion_silence_ms: int = 0,                # silence before prompt
    content_type: ContentType = ContentType.NONMIXABLE,
    label: str | None = None,                      # shown in dev UI
    preroll_ms: int = 0,
    on_patience_expired: Callable[[], Awaitable[None]] | None = None,
    patience: timedelta | None = None,
)

Concepts: Side effects · Cookbook: Audio streaming

InputClaim

InputClaim(
    on_mic_frame: Callable[[bytes], Awaitable[None]],  # mic now goes here
    speaker_source: AsyncIterator[bytes] | None = None, # speaker reads from here
    on_claim_end: Callable[[ClaimEndReason], Awaitable[None]] | None = None,
    title: str | None = None,
)

ClaimEndReason values: NATURAL, USER_PTT, PREEMPTED, ERROR.

SkillContext

Available in setup and stored for use in handlers.

Attribute / methodWhat it gives you
ctx.languagePersona's resolved language code ("es", etc.)
ctx.persona_data_dirPath to server/personas/<persona>/data/
ctx.configPersona's skill config dict (your block)
ctx.loggerSkillLogger (see below)
ctx.storageSkillStorage (see below)
ctx.catalog()Returns a Catalog for fuzzy-matched search
await ctx.inject_turn(prompt, ...)Make the persona speak proactively
await ctx.inject_turn_and_wait(prompt, ...)Like inject_turn, but blocks until the model finishes speaking. Falls back to plain enqueue if the coordinator is busy — do not assume the model finished just because this returned.
await ctx.start_input_claim(claim)Activate an InputClaim
await ctx.cancel_active_claim(reason=...)End the current claim
ctx.background_task(name, factory, ...)Spawn a supervised async task

Some fields you might expect aren't there yet — ctx.name, ctx.timezone, ctx.constraints. For now: read your skill's name from self.name, get timezone from your persona config, and treat constraints as prompt-only (see Concepts: Constraints).

inject_turn

await ctx.inject_turn(
    "Tell the user: time for medication.",       # prompt — LLM instruction
    dedup_key="med_2026-04-29",                  # prevents duplicate queue
    priority=InjectPriority.BLOCK_BEHIND_COMMS,  # see priorities
)

InjectPriority values:

  • NORMAL — fires only when the coordinator is fully idle (no active turn, content stream, or call). Queues until the next quiet moment.
  • BLOCK_BEHIND_COMMS — preempts CONTENT (audiobooks, music), but yields to COMMS (active calls). Default for timers/reminders.
  • PREEMPT — preempts everything, including active phone calls. Emergency only.

Prompts that should be spoken must say so: "Tell the user: ...". Bare statements may be treated as silent notifications.

SkillStorage

await ctx.storage.set_setting(key: str, value: str) -> None
await ctx.storage.get_setting(key: str, default: str | None = None) -> str | None
await ctx.storage.list_settings(prefix: str = "") -> list[tuple[str, str]]
await ctx.storage.delete_setting(key: str) -> None

Per-skill namespace. Per-persona database. Strings only — JSON-encode structured values.

SkillLogger

await ctx.logger.adebug(event: str, **kwargs)
await ctx.logger.ainfo(event: str, **kwargs)
await ctx.logger.awarning(event: str, **kwargs)
await ctx.logger.aerror(event: str, **kwargs)
await ctx.logger.aexception(event: str, **kwargs)  # captures traceback

Auto-namespaces under your skill name. Auto-includes turn ID. Use this; never print or stdlib logging.

background_task

handle = ctx.background_task(
    name="my_listener",
    coro_factory=self._listen,            # callable returning a coroutine
    restart_on_crash=True,
    max_restarts_per_hour=10,
    on_permanent_failure=self._on_dead,
)

For long-lived listeners (restart_on_crash=True), one-shot waiters (restart_on_crash=False).

Catalog

For personal-content skills (libraries, contacts, recipes):

catalog = ctx.catalog()
await catalog.upsert(
    id="el_principito",
    fields={"title": "El Principito", "author": "Saint-Exupéry"},
    payload={"path": "/data/audiobooks/el_principito.m4b"},
)
hits = await catalog.search("principito", limit=5)
prompt_text = catalog.as_prompt_lines(
    limit=20,
    line=lambda h: f"- {h.fields['title']} ({h.fields['author']})",
)

Returns Hit(id, fields, payload, score).

Audio helpers

from huxley_sdk.audio import load_pcm_palette

# Load all WAVs in a directory as PCM blobs.
sounds = load_pcm_palette(ctx.persona_data_dir / "sounds")
chime = sounds["news_start"]  # bytes, ready for PlaySound

PCM format throughout: 16-bit little-endian, 24kHz, mono.

Errors you might raise/catch

from huxley_sdk import ClaimBusyError

try:
    handle = await ctx.start_input_claim(claim)
except ClaimBusyError:
    # Another skill has an active claim on COMMS.
    ...

What you don't import

  • Anything from huxley (the runtime). Stick to huxley_sdk. The wall is intentional.
  • Anything that mentions coordinator, app, session — those are framework-internal.

Next

On this page