Files
semif-agent/SKILL.md
T
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

132 lines
5.8 KiB
Markdown

# 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
```python
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:
```python
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.