6.4 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:
- A manifest — the registry entry that makes it navigable and describes what it does.
- A code body — a runnable Python module implementing the
predictandactphases.
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."""
ctxis anActionContextwithctx.engine(the real SemIf engine) andctx.config(the agent config dict).requestis theRequestbeing handled.Prediction(text: str, decisions: list)andActionResult(action_log: str, new_state: str, needs_input: str | None = None)are imported fromsemif_agent.skills; return those exact types.decisionscarries any(DecisionRequest, DecisionResult)pairs made during predict so they are logged as training rows.needs_inputcarries a question for the human; see the rules below.
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 callctx.engine.call(decision); return it insidePrediction.decisions. - Never swallow the request. If the skill cannot act, return an
ActionResultwith a shortaction_logexplaining why and setnew_stateback torequest.text. - Request input when data is missing. If a required piece of data is not
in the request or in local files, do not fail silently: return an
ActionResult(action_log="...", new_state=request.text, needs_input="<question>"). The run pauses and the human is asked. The answer arrives onrequest.user_inputandactis called again with the same prediction — checkrequest.user_inputon the resume pass to finish the run (or ask again if it is still insufficient). - 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
predictandactexactly.
Conventions
- Single purpose, single file, single module.
- Avoid duplicating an existing skill in the same category.
predictresolves ambiguity (arguments, recipients, targets) with SemIf sub-decisions, mirroring howemail.composeresolves its recipient.actperforms the concrete action and writes a human-readableaction_logthat the self-assessment LLM can judge.
Acceptance criteria
A generated skill is accepted only if:
- It compiles (
compile(..., "exec")succeeds) and defines bothpredictandact. - Its
namematches the manifest regex and itscategoryis given. - Its body imports nothing outside the stdlib and the agent package.
- It uses
ctx.engine(never mocks) and returns properPrediction/ActionResulttypes. - 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.