Imperal Docs
Recipes

Skeleton — surface state and entities to Webbee

Surface live counts and compact entity cards to Webbee so the agent always sees current state and entities before every chat turn.

A skeleton is LLM-facing, not user-facing. Every time the user sends a chat message, the web-kernel injects ambient state and entity awareness into the intent classifier and routing context.

Because of this ambient layer:

  • When the user asks "what tasks do I have today?", Webbee answers directly from the skeleton's live counts.
  • When the user says "deploy the Mobile App project", Webbee immediately grounds "Mobile App" to project_id="proj_10" from the ambient entity cards.

Complete recipe: Tasks & Projects Skeleton

Below is a complete, production-grade example from tasks/skeleton.py showing both scalar counters and compact entity summaries:

skeleton.py — complete entity & counter skeleton
from typing import Any
from imperal_sdk import Extension

ext = Extension(
    "tasks",
    display_name="Tasks",
    description="Task management with ambient entity tracking.",
    icon="icon.svg",
    actions_explicit=True,
)


# ── Skeleton: Surface pending counts + active project entity cards ───────────
# alert=True: The web-kernel calls skeleton_alert_tasks when overdue counts change.
@ext.skeleton(
    "tasks_and_projects",
    alert=True,
    ttl=120,
    description="Live overdue/today task counters and active project entity cards.",
)
async def skeleton_refresh_tasks(ctx) -> dict[str, Any]:
    """Refresh task counters and active projects. Pure read — idempotent."""
    uid = ctx.user.imperal_id
    if not uid:
        return {"response": {"note": "anonymous user"}}

    # 1. Fetch live metrics from storage / upstream service
    overdue_count = await ctx.store.count("tasks", where={"is_overdue": True})
    today_count   = await ctx.store.count("tasks", where={"is_due_today": True})
    total_active  = await ctx.store.count("tasks", where={"is_completed": False})

    # 2. Fetch key active project entities for ambient entity grounding
    projects = await ctx.store.query(
        "projects",
        where={"status": "active"},
        order_by="-updated_at",
        limit=10,
    )

    # 3. Shape compact entity cards (id, name, status, task_count)
    active_project_cards = [
        {
            "project_id": p.id,
            "name": p.data.get("name", "Untitled"),
            "task_count": p.data.get("pending_tasks", 0),
            "status": p.data.get("status", "active"),
        }
        for p in projects.data
    ]

    # 4. Return structured envelope
    return {
        "response": {
            "overdue_count": overdue_count,
            "today_count": today_count,
            "total_active_tasks": total_active,
            "active_projects": active_project_cards,
        }
    }


# ── Skeleton Alert: Proactively warn user when overdue tasks spike ────────────
@ext.tool(
    "skeleton_alert_tasks",
    description="Fires when task statistics change across refresh cycles.",
)
async def skeleton_alert_tasks(
    ctx,
    old: dict[str, Any] | None = None,
    new: dict[str, Any] | None = None,
) -> dict[str, str]:
    """Notify the user proactively if overdue tasks increased."""
    if not new:
        return {"response": ""}

    overdue     = new.get("overdue_count", 0)
    old_overdue = (old or {}).get("overdue_count", 0)
    today       = new.get("today_count", 0)
    old_today   = (old or {}).get("today_count", 0)
    
    alerts: list[str] = []
    if overdue > 0 and overdue > old_overdue:
        delta = overdue - old_overdue
        alerts.append(f"{delta} new overdue task(s) (now {overdue} total)")
    if today > 0 and today != old_today:
        alerts.append(f"{today} task(s) due today")
        
    return {"response": " · ".join(alerts)}

Best Practices Breakdown

  1. Compact Entity Cards: Notice how active_projects includes only project_id, name, task_count, and status. This is enough for Webbee to resolve "how is the Mobile App project doing?" to project_id="proj_10".
  2. Key Entities Selection: Surface active, high-priority, or recently modified entities. If there are 100 projects, surface the active ones; the agent will use list_projects() if the user asks for the full historical catalog.
  3. No Heavy Payloads: Never return full descriptions, markdown content, or nested task lists in the skeleton. Keep it tight!
  4. Resilient Fallbacks: Check resp.ok and handle missing user context gracefully so background refreshes never throw unhandled exceptions.

On this page