Files
Denton Social 440e49e76e Author runnable skill bodies via OpenAI-compatible codegen model
create_skill now writes a real predict/act body: the small decision model
still authors title + description (engine.generate), then a larger
OpenAI-compatible model (default qwen38-iq3s) writes the runnable code
against the SKILL.md contract. Bodies persist to data/skills/<cat>/<name>.py,
are hot-loaded via importlib, merged into the running tree, and the request
re-dispatches to the new leaf. The dashboard decision-flow view shows the
title/description with a writing badge while the body is being written.
Codegen failure degrades to a navigable stub.
2026-09-24 00:36:35 -05:00

5.8 KiB

SKILL.md — the contract for new skills

This file is the authoritative spec for what a new skill is and how its code body must be written. It is fed verbatim to the code-generation model so every generated skill is consistent, and it is read by humans who want to know what a good skill looks like.

What a skill is

A skill is a leaf in the agent's skill tree, reached by a chain of SemIf decisions (category -> skill). It is one specific, single-purpose action the agent can take — never a broad bucket (that is a category's job). It runs the standard skill loop: observe -> predict -> act -> observe -> assess.

A skill is two things:

  1. A manifest — the registry entry that makes it navigable and describes what it does.
  2. A code body — a runnable Python module implementing the predict and act phases.

Manifest schema

Registered in data/categories.json (and mirrored in the running tree). Fields:

Field Meaning
name category.skill dotted snake_case id. Lowercase letters, digits, _ and . only ([a-z0-9_]+(?:\.[a-z0-9_]+)*). Single purpose.
category The top-level category bucket the skill lives under.
description One to two sentences: what the skill does, for navigation.
allowed_inputs What the skill accepts / needs as input context.
actions The concrete actions the skill takes.
cost_budget Relative budget for one run; a run that exceeds it fails fast.
decision_log_ref Reference to the decision rows this skill logged during a run.

Only name and description are required for a stub; the rest fill in as the skill is exercised.

Code body contract

The generated module is persisted to data/skills/<category>/<name>.py and imported at runtime. It must satisfy all of the following:

Required functions

def predict(ctx, request) -> Prediction:
    """Forecast + make any SemIf sub-decisions. Return the prediction."""

def act(ctx, request, prediction) -> ActionResult:
    """Execute the action. Return the result + new state."""
  • ctx is an ActionContext with ctx.engine (the real SemIf engine) and ctx.config (the agent config dict).
  • request is the Request being handled.
  • Prediction(text: str, decisions: list) and ActionResult(action_log: str, new_state: str) are imported from semif_agent.skills; return those exact types. decisions carries any (DecisionRequest, DecisionResult) pairs made during predict so they are logged as training rows.

Rules (hard requirements)

  • Stdlib only. No third-party imports, no files outside the project. The core agent is pure-stdlib and runs on the thin dev box.
  • No mocking. Sub-decisions use the real engine: build a DecisionRequest(state, question, options=[Option(id, description), ...]) and call ctx.engine.call(decision); return it inside Prediction.decisions.
  • Never swallow the request. If the skill cannot act, return an ActionResult with a short action_log explaining why and set new_state back to request.text.
  • Write files under configured data dirs only (e.g. ctx.config["drafts"]), never anywhere else on disk.
  • Fail fast on budget. Keep the work small; do not loop or retry in code.
  • Names match the manifest. The module is imported as its manifest name; the functions are predict and act exactly.

Conventions

  • Single purpose, single file, single module.
  • Avoid duplicating an existing skill in the same category.
  • predict resolves ambiguity (arguments, recipients, targets) with SemIf sub-decisions, mirroring how email.compose resolves its recipient.
  • act performs the concrete action and writes a human-readable action_log that the self-assessment LLM can judge.

Acceptance criteria

A generated skill is accepted only if:

  1. It compiles (compile(..., "exec") succeeds) and defines both predict and act.
  2. Its name matches the manifest regex and its category is given.
  3. Its body imports nothing outside the stdlib and the agent package.
  4. It uses ctx.engine (never mocks) and returns proper Prediction / ActionResult types.
  5. It is single-purpose and does not duplicate an existing category leaf.

Worked example

email.compose resolves its recipient with a SemIf sub-decision, then writes a draft file:

def predict(ctx, request):
    contacts = read_contacts(ctx.config)          # config-driven, local data
    decision = DecisionRequest(
        state=f"{request.text} [current process: none]",
        question="Which contact is the intended recipient?",
        options=[Option(c["name"], c.get("description", "")) for c in contacts]
        + [Option("none", "None of the listed contacts.")],
    )
    result = ctx.engine.call(decision)
    return Prediction(text=f"recipient is {result.selected}",
                      decisions=[(decision, result)])

def act(ctx, request, prediction):
    recipient = prediction.text.removeprefix("recipient is ")
    drafts = Path(ctx.config.get("drafts", "data/drafts"))
    drafts.mkdir(parents=True, exist_ok=True)
    target = drafts / f"{request.id}.txt"
    target.write_text(f"To: {recipient}\nBody: {request.text}\n")
    return ActionResult(
        action_log=f"email.compose: wrote draft {target} for {recipient!r}.",
        new_state=f"Draft written to {target.name} for {recipient}.",
    )

Write skill bodies in this shape: resolve ambiguity in predict via ctx.engine, do the work in act, keep both stdlib-only, and return the proper types.