smllm
smllm guides an LLM agent through a workflow with a state machine: the agent sees one state at a time, and moves on only by firing one of the state’s events. This documentation teaches you to write a machine, connect it to your agent, and look up each rule and finding of smllm validate.
Where to start
- You are new to smllm: install it as the README shows, then follow Your first state machine.
- You want an agent to follow your machine: follow Connect an agent, then Check the agent’s work.
- You run the engine in your own program: follow Run the engine in Node.js.
- You want to know why smllm uses a state machine: read Why a state machine.
- You want to see where an agent is: follow Show smllm in your status line.
smllm validatereported a finding: look up its code in The rules of smllm validate, and its cause and fix on the page of the code, such as FMT-1: The machine file.
The sections
- Tutorials: lessons that build one working thing, step by step.
- How-to guides: one task each, from start to finish.
- Reference: the facts to look up, such as each rule of
smllm validateand the findings under it. - Explanations: why smllm works the way it does.
Your first state machine
In this tutorial you write a state machine that guides an agent through a bug fix, from a test that reproduces the bug to the fix. You check the machine, then fire its events from the shell, as the agent fires them through the smllm tool. At the end you can write a machine for a workflow of your own.
Before you begin
- smllm is installed, as the README shows.
- Claude Code is installed, and you have run
smllm harness install claudein your project’s folder to connect smllm to it, as the install section of Harnesses shows. - You have a project folder that is a git repository.
Step 1: create a config
smllm reads its state machines from the config of the project, so a project starts with one. Run this in the project’s folder:
$ smllm init
created .smllm/config.toml
Step 2: scaffold a machine
smllm new writes a template machine and lists it in the config, so that smllm finds it:
$ smllm new fix --write
created .smllm/fix.smllm.yaml and listed it in .smllm/config.toml
Step 3: write the states and check them
Replace the template with this machine. It has three states. In TRIAGE the agent writes a test that reproduces the bug, in FIX it makes the test pass, and DONE ends the work. Each event moves the agent to the next state, and setRef names the instance after the bug.
.smllm/fix.smllm.yaml
id: fix
description: Fix a bug, from a test that reproduces it to the fix.
initial: TRIAGE
meta:
smllm: 1
instance:
kind: bug
ref: { param: bugId, pattern: "^BUG-[0-9]+$", description: "The bug's id, such as BUG-7." }
events:
reproduced:
description: Fire after a test reproduces the bug.
fixed:
description: Fire after the test passes.
states:
TRIAGE:
meta: { entryPoint: true }
entry: { type: prompt, params: { text: "Write a test that reproduces the bug, then fire reproduced with the bug's id." } }
on:
reproduced: { target: FIX, actions: setRef }
FIX:
entry: { type: prompt, params: { text: "Change the code until the test passes, then fire fixed." } }
on:
fixed: DONE
DONE:
type: final
entry: { type: prompt, params: { text: "Summarise the cause of the bug and the fix." } }
smllm validate checks every machine of the config before an agent runs it:
$ smllm validate
0 errors, 0 warnings, 0 info
A finding names the file and the line of the problem, and ends with the code of the rule that it breaks, such as (FMT-1). The rules of smllm validate says what each rule checks.
Step 4: start a session
Start Claude Code in the project. smllm gives the agent a session and lists the machines that it can enter. The agent sees this:
<smllm>
session sm-nqvfvw · idle
State machines:
- fix: Fix a bug, from a test that reproduces it to the fix.
bug id param bugId (pattern: ^BUG-[0-9]+$): The bug's id, such as BUG-7.
starts at: TRIAGE; entry points: TRIAGE
Fire one event: smllm({ session: "sm-nqvfvw", event, params })
<events>
- enter: Start a new instance of a state machine, or continue one.
stateMachine (required, one of: fix): The state machine to enter.
bugId (optional): The id param that the list of state machines names for the machine: an existing id or ref continues that instance, and a new ref starts one with it. Omit it to start a new instance.
state (optional): An entry point to enter instead, or any state for an instance whose saved state no longer exists. Omit it to enter a new instance at the initial state, or an existing one at its saved state.
</events>
</smllm>
sm-nqvfvw is the key of the session. Your key differs.
Step 5: enter the machine
The agent enters the machine with the event enter. Fire it from the shell, with your session’s key:
$ smllm fire --session sm-nqvfvw enter --param stateMachine=fix
<smllm>
session sm-nqvfvw · fix › TRIAGE · bug i-5z4x7n
Arrived by: enter (new bug)
Params: stateMachine = "fix"
<instructions>
Write a test that reproduces the bug, then fire reproduced with the bug's id.
</instructions>
</smllm>
The block is what the agent reads: where it is, how it got there, and what the state asks.
Step 6: try to stop
When the agent tries to stop in TRIAGE, smllm refuses, and lists the events that the agent can fire instead:
<smllm>
session sm-nqvfvw · fix › TRIAGE · bug i-5z4x7n
Fire one event: smllm({ session: "sm-nqvfvw", event, params })
<events>
- reproduced: Fire after a test reproduces the bug.
bugId (required, pattern: ^BUG-[0-9]+$): The bug's id, such as BUG-7.
- yield: Stop for now and stay in TRIAGE.
note (optional): What you are waiting for.
- pause: Put bug i-5z4x7n aside and return to idle.
- unmatched: The request fits none of these; handle it from idle, then resume.
</events>
</smllm>
The agent fires one of these events before it stops: yield waits for you, and pause puts the bug aside. If the agent tries to stop again without firing one, smllm lets it stop, so that an agent that can’t go on doesn’t loop.
Step 7: finish the fix
Fire the two events of the machine, as the agent does after each step of the work:
$ smllm fire --session sm-nqvfvw reproduced --param bugId=BUG-7
<smllm>
session sm-nqvfvw · fix › FIX · bug BUG-7
Arrived by: reproduced from TRIAGE
Params: bugId = "BUG-7"
<instructions>
Change the code until the test passes, then fire fixed.
</instructions>
</smllm>
$ smllm fire --session sm-nqvfvw fixed
<smllm>
session sm-nqvfvw · fix › DONE · bug BUG-7
Arrived by: fixed from FIX
<instructions>
Summarise the cause of the bug and the fix.
</instructions>
Completed bug BUG-7. You are now in idle.
State machines:
- fix: Fix a bug, from a test that reproduces it to the fix.
bug id param bugId (pattern: ^BUG-[0-9]+$): The bug's id, such as BUG-7.
starts at: TRIAGE; entry points: TRIAGE
Fire one event: smllm({ session: "sm-nqvfvw", event, params })
<events>
- enter: Start a new instance of a state machine, or continue one.
stateMachine (required, one of: fix): The state machine to enter.
bugId (optional): The id param that the list of state machines names for the machine: an existing id or ref continues that instance, and a new ref starts one with it. Omit it to start a new instance.
state (optional): An entry point to enter instead, or any state for an instance whose saved state no longer exists. Omit it to enter a new instance at the initial state, or an existing one at its saved state.
</events>
</smllm>
The two replies show how the instance ends:
- The instance takes the name
BUG-7fromsetRef. DONEis a final state, so the bug is complete, and the session returns to idle with the list of machines that it can enter.
What’s next
- Connect an agent
- Check the agent’s work
- Why a state machine
- Show smllm in your status line
- FMT-1: The machine file
- The rules of smllm validate
Connect an agent
In this tutorial you install smllm into Claude Code for a project with a state machine, and you follow the machine as the agent sees it: the text at the start of a session, the reply to each event, and a stop that smllm blocks. At the end you can connect smllm to a project of your own and read what the agent reads.
Before you begin
- You have written and checked a machine, as Your first state machine shows.
- smllm is installed, as the README shows.
- Claude Code is installed.
- You have a new project folder for this tutorial, which is a git repository. A new folder has no smllm files yet, so each run prints what the page shows.
Step 1: add a machine to the project
The agent can follow only the machines that the project’s config lists. Write a config that lists one machine, and the machine itself. The machine guides the agent through a small feature: in PLAN the agent writes a plan, in BUILD it builds the feature, and DONE ends the work.
.smllm/config.toml
[machines]
files = ["feature.smllm.yaml"]
.smllm/feature.smllm.yaml
id: feature
description: Build a small feature, from a plan to working code.
initial: PLAN
meta:
smllm: 1
instance:
kind: feature
events:
planned:
description: Fire after the user agrees to the plan.
built:
description: Fire after the feature works and its tests pass.
states:
PLAN:
meta: { entryPoint: true }
entry: { type: prompt, params: { text: "Write a short plan for the feature, and ask the user to agree to it." } }
on:
planned: BUILD
BUILD:
entry: { type: prompt, params: { text: "Build the feature as the plan says, with a test for it." } }
on:
built: DONE
DONE:
type: final
entry: { type: prompt, params: { text: "Tell the user what changed." } }
Check the machine before an agent runs it:
$ smllm validate
0 errors, 0 warnings, 0 info
Step 2: install smllm into Claude Code
smllm harness install claude writes the files that connect Claude Code to smllm. Run it in the project’s folder:
$ smllm harness install claude
claude:
created CLAUDE.md (instructions)
created .claude/settings.json (hooks)
created .mcp.json (mcp)
created .claude/settings.json (permissions)
created .claude/skills/smllm-statusline (statusline)
note: smllm is not in your Claude Code status line; ask Claude to add it (skill smllm-statusline)
Each line names a part and the file that holds it:
- The instructions in
CLAUDE.mdtell the agent to follow the text in<smllm>blocks, and to fire an event when its work is done. - The hooks in
.claude/settings.jsonrun smllm when a session starts, when you send a prompt, and when the agent tries to stop. .mcp.jsongives the agent the smllm tool, with which it fires events, and the permission lets the agent call the tool without asking you.- The skill
smllm-statuslineadds smllm to your status line when you ask Claude to, as Show smllm in your status line shows.
Commit these files, so that everyone who works on the project gets the same setup. After you upgrade smllm, run the command again: smllm harness status claude shows whether each part is current.
$ smllm harness status claude
claude:
current CLAUDE.md (instructions)
current .claude/settings.json (hooks)
current .mcp.json (mcp)
current .claude/settings.json (permissions)
current .claude/skills/smllm-statusline (statusline)
note: smllm is not in your Claude Code status line; ask Claude to add it (skill smllm-statusline)
Step 3: start a session
Start Claude Code in the project, and approve the smllm server when Claude Code asks. The session start hook gives the agent a session and the list of machines that it can enter. The agent sees this text before your first prompt:
<smllm>
session sm-nqvfvw · idle
State machines:
- feature: Build a small feature, from a plan to working code.
feature id param ref
starts at: PLAN; entry points: PLAN
Fire one event: smllm({ session: "sm-nqvfvw", event, params })
<events>
- enter: Start a new instance of a state machine, or continue one.
stateMachine (required, one of: feature): The state machine to enter.
ref (optional): The id param that the list of state machines names for the machine: an existing id or ref continues that instance, and a new ref starts one with it. Omit it to start a new instance.
state (optional): An entry point to enter instead, or any state for an instance whose saved state no longer exists. Omit it to enter a new instance at the initial state, or an existing one at its saved state.
</events>
</smllm>
sm-nqvfvw is the key of the session. Your key differs. The agent passes the key on every call of the smllm tool.
Step 4: ask for the work
Ask Claude for a feature, such as “Add a --verbose flag to the build script”. The agent picks the machine whose description fits the request, and fires enter through the smllm tool. The tool’s reply is the text that smllm fire prints for the same event, so this tutorial shows each call of the agent as a run of smllm fire. You don’t run these commands: the agent makes the calls.
$ smllm fire --session sm-nqvfvw enter --param stateMachine=feature
<smllm>
session sm-nqvfvw · feature › PLAN · feature i-x96snr
Arrived by: enter (new feature)
Params: stateMachine = "feature"
<instructions>
Write a short plan for the feature, and ask the user to agree to it.
</instructions>
</smllm>
The agent now writes a plan and asks you to agree to it.
Step 5: watch a blocked stop
The agent tries to stop, to wait for your answer. Before it stops, the stop hook checks where it is. The agent fired no event, so smllm blocks the stop and gives the agent the events that it can fire:
<smllm>
session sm-nqvfvw · feature › PLAN · feature i-x96snr
Fire one event: smllm({ session: "sm-nqvfvw", event, params })
<events>
- planned: Fire after the user agrees to the plan.
- yield: Stop for now and stay in PLAN.
note (optional): What you are waiting for.
- pause: Put feature i-x96snr aside and return to idle.
- unmatched: The request fits none of these; handle it from idle, then resume.
</events>
</smllm>
The agent fires yield, because it is waiting for you, and then stops. A second stop without an event is allowed, so that an agent that can’t go on doesn’t loop.
Step 6: come back to the work
When Claude Code resumes the conversation, or compacts it, the session start hook runs again for the same session. This time the agent sees where it is, with the state’s instructions and its events:
<smllm>
session sm-nqvfvw · feature › PLAN · feature i-x96snr
<instructions>
Write a short plan for the feature, and ask the user to agree to it.
</instructions>
Fire one event: smllm({ session: "sm-nqvfvw", event, params })
<events>
- planned: Fire after the user agrees to the plan.
- yield: Stop for now and stay in PLAN.
note (optional): What you are waiting for.
- pause: Put feature i-x96snr aside and return to idle.
- unmatched: The request fits none of these; handle it from idle, then resume.
</events>
</smllm>
Step 7: finish the feature
Agree to the plan. The agent fires planned, builds the feature, and fires built:
$ smllm fire --session sm-nqvfvw planned
<smllm>
session sm-nqvfvw · feature › BUILD · feature i-x96snr
Arrived by: planned from PLAN
<instructions>
Build the feature as the plan says, with a test for it.
</instructions>
</smllm>
$ smllm fire --session sm-nqvfvw built
<smllm>
session sm-nqvfvw · feature › DONE · feature i-x96snr
Arrived by: built from BUILD
<instructions>
Tell the user what changed.
</instructions>
Completed feature i-x96snr. You are now in idle.
State machines:
- feature: Build a small feature, from a plan to working code.
feature id param ref
starts at: PLAN; entry points: PLAN
Fire one event: smllm({ session: "sm-nqvfvw", event, params })
<events>
- enter: Start a new instance of a state machine, or continue one.
stateMachine (required, one of: feature): The state machine to enter.
ref (optional): The id param that the list of state machines names for the machine: an existing id or ref continues that instance, and a new ref starts one with it. Omit it to start a new instance.
state (optional): An entry point to enter instead, or any state for an instance whose saved state no longer exists. Omit it to enter a new instance at the initial state, or an existing one at its saved state.
</events>
</smllm>
The two replies show how a machine ends:
- Each reply starts with the state that the event entered, and with how the agent got there.
DONEis a final state, so the feature is complete, and the session returns to idle with the list of machines.
What’s next
Check the agent’s work
In this tutorial you write a state machine that checks the agent’s work with a command, which the agent can’t skip. The agent finishes only when the test passes, and after two failed attempts the machine sends it to you for help. At the end you can guard any step of a machine with a check of your own.
Before you begin
- You have written and run a machine, as Your first state machine shows.
- smllm is installed, as the README shows.
- You have a project folder that is a git repository.
Step 1: write the test and the machine
A command guard runs a command, and passes when the command exits with status 0. Any test of your project can be the command. This tutorial uses a test in a shell script, which checks that greeting.txt says hello. Write the script, and a greeting.txt that fails it:
test.sh
grep -qx "hello" greeting.txt || { echo "greeting.txt doesn't say hello"; exit 1; }
greeting.txt
hi
Write the config, which lists the machine:
.smllm/config.toml
[machines]
files = ["fix.smllm.yaml"]
The machine has three states. In WORK the agent makes the test pass, in ASK it asks you what to try, and DONE ends the work. The event fixed takes the param cause, in which the agent says what made the test fail.
.smllm/fix.smllm.yaml
id: fix
description: Make the failing test pass.
initial: WORK
meta:
smllm: 1
events:
fixed:
description: Fire after you change the code, with the cause of the failure.
params:
type: object
properties:
cause: { description: "What made the test fail, in one sentence." }
required: [cause]
retry:
description: Fire after the user tells you what to try.
states:
WORK:
meta: { entryPoint: true }
entry: { type: prompt, params: { text: "Change the code until sh test.sh passes, then fire fixed." } }
on:
fixed:
- target: DONE
guard: { type: command, params: { run: "sh test.sh" } }
- target: ASK
guard: { type: visits, params: { state: WORK, atLeast: 2 } }
- target: WORK
reenter: true
ASK:
entry: { type: prompt, params: { text: "The test still fails. Ask the user what to try, then fire retry." } }
on:
retry: WORK
DONE:
type: final
entry: { type: prompt, params: { text: "Tell the user the cause of the failure." } }
The event fixed has three transitions, and smllm takes the first whose guard passes:
- The command guard runs
sh test.shin the session’s folder, and goes toDONEwhen the test passes. - The
visitsguard counts how many times the agent has enteredWORK, and goes toASKfrom the second visit. - The last transition has no guard. It leaves
WORKand enters it again, which counts as a visit.
Check the machine:
$ smllm validate
0 errors, 0 warnings, 0 info
Step 2: start the work
Enter the machine, as the agent does through the smllm tool. Without --session, smllm fire enter starts a new session, and prints its key in the header:
$ smllm fire enter --param stateMachine=fix
<smllm>
session sm-efajqg · fix › WORK · instance i-2q5chp
Arrived by: enter (new instance)
Params: stateMachine = "fix"
<instructions>
Change the code until sh test.sh passes, then fire fixed.
</instructions>
</smllm>
sm-efajqg is the key of the new session, and i-2q5chp is the instance. Your key and your instance differ: use yours in each of the following runs.
Step 3: fire an event without its param
The param cause is required, so smllm refuses fixed without it. The agent stays in WORK, and the reply lists the events and their params:
$ smllm fire --session sm-efajqg fixed
<smllm>
session sm-efajqg · fix › WORK · instance i-2q5chp
error: fixed needs param cause
Fire one event: smllm({ session: "sm-efajqg", event, params })
<events>
- fixed: Fire after you change the code, with the cause of the failure.
cause (required): What made the test fail, in one sentence.
- yield: Stop for now and stay in WORK.
note (optional): What you are waiting for.
- pause: Put instance i-2q5chp aside and return to idle.
- unmatched: The request fits none of these; handle it from idle, then resume.
</events>
</smllm>
[exit 1]
Step 4: claim a fix that fails the test
Fire fixed with a cause, but leave greeting.txt as it is. The test fails, so the agent goes back to WORK:
$ smllm fire --session sm-efajqg fixed --param "cause=The greeting was too short."
<smllm>
session sm-efajqg · fix › WORK (visit 2) · instance i-2q5chp
Arrived by: fixed from WORK
Params: cause = "The greeting was too short."
Guard: command `sh test.sh` → false (exited 1: greeting.txt doesn't say hello)
Guard: visits WORK ≥ 2 → false (visits: 1)
<instructions>
Change the code until sh test.sh passes, then fire fixed.
</instructions>
</smllm>
The reply shows the guards and the visit:
- Each
Guard:line shows a guard that smllm checked, and its result. A failed command adds the end of its output, so that the agent can see why the test failed. (visit 2)in the header says that this is the second visit toWORK.
Step 5: fail the test again
Fire fixed again without a change. This time the visits guard passes, and the machine sends the agent to you:
$ smllm fire --session sm-efajqg fixed --param "cause=The greeting was too short."
<smllm>
session sm-efajqg · fix › ASK · instance i-2q5chp
Arrived by: fixed from WORK
Params: cause = "The greeting was too short."
Guard: command `sh test.sh` → false (exited 1: greeting.txt doesn't say hello)
Guard: visits WORK ≥ 2 → true (visits: 2)
<instructions>
The test still fails. Ask the user what to try, then fire retry.
</instructions>
</smllm>
Step 6: make the test pass
Tell the agent what to try, and fire retry, as the agent does after your answer:
$ smllm fire --session sm-efajqg retry
<smllm>
session sm-efajqg · fix › WORK (visit 3) · instance i-2q5chp
Arrived by: retry from ASK
<instructions>
Change the code until sh test.sh passes, then fire fixed.
</instructions>
</smllm>
Change greeting.txt so that it passes the test:
greeting.txt
hello
Fire fixed once more. The test passes, so the agent reaches DONE:
$ smllm fire --session sm-efajqg fixed --param "cause=The greeting said hi, not hello."
<smllm>
session sm-efajqg · fix › DONE · instance i-2q5chp
Arrived by: fixed from WORK
Params: cause = "The greeting said hi, not hello."
Guard: command `sh test.sh` → true
<instructions>
Tell the user the cause of the failure.
</instructions>
Completed instance i-2q5chp. You are now in idle.
State machines:
- fix: Make the failing test pass.
instance id param ref
starts at: WORK; entry points: WORK
Fire one event: smllm({ session: "sm-efajqg", event, params })
<events>
- enter: Start a new instance of a state machine, or continue one.
stateMachine (required, one of: fix): The state machine to enter.
ref (optional): The id param that the list of state machines names for the machine: an existing id or ref continues that instance, and a new ref starts one with it. Omit it to start a new instance.
state (optional): An entry point to enter instead, or any state for an instance whose saved state no longer exists. Omit it to enter a new instance at the initial state, or an existing one at its saved state.
</events>
</smllm>
The cause is now in the instance’s history. smllm instance show prints each event with its time, its params, and its guards:
$ smllm instance show i-2q5chp
fix i-2q5chp (i-2q5chp)
state: DONE status: completed holder: -
visits: ASK=1 DONE=1 WORK=3
history:
2026-01-05 09:01 UTC sm-efajqg enter → WORK
stateMachine = fix
2026-01-05 09:04 UTC sm-efajqg fixed WORK → WORK
cause = The greeting was too short.
Guard: command `sh test.sh` → false (exited 1: greeting.txt doesn't say hello)
Guard: visits WORK ≥ 2 → false (visits: 1)
2026-01-05 09:05 UTC sm-efajqg fixed WORK → ASK
cause = The greeting was too short.
Guard: command `sh test.sh` → false (exited 1: greeting.txt doesn't say hello)
Guard: visits WORK ≥ 2 → true (visits: 2)
2026-01-05 09:06 UTC sm-efajqg retry ASK → WORK
2026-01-05 09:07 UTC sm-efajqg fixed WORK → DONE
cause = The greeting said hi, not hello.
Guard: command `sh test.sh` → true
What’s next
Run the engine in Node.js
In this tutorial you run smllm’s engine inside your own program. You compile a state machine to JSON, load it in Node.js with @six5536/smllm-wasm, and fire events as an agent does, with no smllm process and no harness. At the end you can build a host of your own that gives an agent the smllm tool.
Before you begin
- You have written and run a machine, as Your first state machine shows.
- smllm is installed, as the README shows.
- A current long-term support (LTS) release of Node.js is installed.
Step 1: write the machine
In an empty folder, install the package:
npm install @six5536/smllm-wasm
Write a machine in the folder. It guides an agent through the review of a change: in READ the agent reads the change, in REPORT it writes its findings, and the event reported takes the agent’s verdict.
review.smllm.yaml
id: review
description: Review a change and give a verdict.
initial: READ
meta:
smllm: 1
instance:
kind: review
events:
read:
description: Fire after you have read the whole change.
reported:
description: Fire after you write your findings, with your verdict.
params:
type: object
properties:
verdict: { enum: [approve, reject], description: "Your verdict on the change." }
required: [verdict]
states:
READ:
meta: { entryPoint: true }
entry: { type: prompt, params: { text: "Read the change and the code around it." } }
on:
read: REPORT
REPORT:
entry: { type: prompt, params: { text: "Write your findings, then fire reported with your verdict." } }
on:
reported: DONE
DONE:
type: final
entry: { type: prompt, params: { text: "Post your findings for the author." } }
Check the machine:
$ smllm validate review.smllm.yaml
0 errors, 0 warnings, 0 info
Step 2: compile the machine
The engine reads compiled machines, not YAML. smllm compile checks a machine file, or a config with all its machines, and writes it as JSON. It prints nothing when it succeeds:
$ smllm compile review.smllm.yaml -o review.json
Compile the machine again after each change to it, because the engine reads only review.json.
Step 3: drive the machine from Node.js
Write a script that loads review.json into an engine, binds a session, and makes the calls that an agent makes through the smllm tool:
review.mjs
import { readFileSync } from "node:fs";
import init, { Engine } from "@six5536/smllm-wasm";
// Load the engine's WebAssembly once.
await init();
let n = 0;
const engine = new Engine(readFileSync("review.json", "utf8"), {
// Fixed random numbers give the same keys on every run.
random: () => ++n,
});
const { session, text } = engine.bind("node");
console.log(text);
console.log(engine.callTool({ session, event: "enter", params: { stateMachine: "review" } }).text);
const stop = engine.stop(session);
console.log(stop.decision);
console.log(stop.text);
console.log(engine.callTool({ session, event: "read" }).text);
console.log(engine.callTool({ session, event: "reported", params: { verdict: "approve" } }).text);
The script makes these calls:
initloads the engine’s WebAssembly, once for each process.bindstarts a session for the harnessnode, and returns its key and the text that a session start hook gives the agent.callTooltakes the same arguments as the smllm tool, and returns the tool’s reply.stopdecides whether the agent can stop, as the stop hook does.
Run the script:
$ node review.mjs
<smllm>
session sm-200000 · idle
State machines:
- review: Review a change and give a verdict.
review id param ref
starts at: READ; entry points: READ
Fire one event: smllm({ session: "sm-200000", event, params })
<events>
- enter: Start a new instance of a state machine, or continue one.
stateMachine (required, one of: review): The state machine to enter.
ref (optional): The id param that the list of state machines names for the machine: an existing id or ref continues that instance, and a new ref starts one with it. Omit it to start a new instance.
state (optional): An entry point to enter instead, or any state for an instance whose saved state no longer exists. Omit it to enter a new instance at the initial state, or an existing one at its saved state.
</events>
</smllm>
<smllm>
session sm-200000 · review › READ · review i-400000
Arrived by: enter (new review)
Params: stateMachine = "review"
<instructions>
Read the change and the code around it.
</instructions>
</smllm>
block
<smllm>
session sm-200000 · review › READ · review i-400000
Fire one event: smllm({ session: "sm-200000", event, params })
<events>
- read: Fire after you have read the whole change.
- yield: Stop for now and stay in READ.
note (optional): What you are waiting for.
- pause: Put review i-400000 aside and return to idle.
- unmatched: The request fits none of these; handle it from idle, then resume.
</events>
</smllm>
<smllm>
session sm-200000 · review › REPORT · review i-400000
Arrived by: read from READ
<instructions>
Write your findings, then fire reported with your verdict.
</instructions>
</smllm>
<smllm>
session sm-200000 · review › DONE · review i-400000
Arrived by: reported from REPORT
Params: verdict = "approve"
<instructions>
Post your findings for the author.
</instructions>
Completed review i-400000. You are now in idle.
State machines:
- review: Review a change and give a verdict.
review id param ref
starts at: READ; entry points: READ
Fire one event: smllm({ session: "sm-200000", event, params })
<events>
- enter: Start a new instance of a state machine, or continue one.
stateMachine (required, one of: review): The state machine to enter.
ref (optional): The id param that the list of state machines names for the machine: an existing id or ref continues that instance, and a new ref starts one with it. Omit it to start a new instance.
state (optional): An entry point to enter instead, or any state for an instance whose saved state no longer exists. Omit it to enter a new instance at the initial state, or an existing one at its saved state.
</events>
</smllm>
The output shows three things about the engine:
- The texts are the same as those of the
smllmcommand: the engine is the same. - The stop is
block, because the agent is inREADand fired no event, so a host shows the agent the events and keeps it working. - Without
random, the engine draws random keys, so your keys differ from the page’s.
The engine keeps its state in memory. To keep it between runs, pass a storage as the third argument of Engine, such as nodeFileStorage from @six5536/smllm-wasm/node. The README of @six5536/smllm-wasm describes the whole API: storage, hosts that run command guards, and hosts that drive a model without tools.
What’s next
Show smllm in your status line
When you want to see where an agent’s work is without asking it, show smllm in Claude Code’s status line. The row shows the state machine, the state, and the instance of the session, such as smllm dev › WORK (visit 3) · issue GH-123.
To let Claude make the change, ask it to “add smllm to my status line”. The smllm-statusline skill makes the change and shows it to you first. smllm harness install claude installs the skill, and so does smllm harness install all, unless you add --without statusline. The Claude Code plugin also ships the skill. To make the change by hand, follow these steps.
Steps
-
If you have no status line script yet, create
~/.claude/statusline-command.sh:#!/usr/bin/env bash input=$(cat) printf '%s' "$input" | smllm statuslineThen point Claude Code at the script in
~/.claude/settings.json, and skip the next step:{ "statusLine": { "type": "command", "command": "bash ~/.claude/statusline-command.sh" } } -
If you have a script, add this line at the end of it, after its other output:
if row=$(printf '%s' "$input" | smllm statusline 2>/dev/null) && [ -n "$row" ]; then printf '\n%s' "$row"; fi$inputis the status JSON that your script read from stdin (input=$(cat)). Claude Code runs onestatusLinecommand, and each line that the command prints is a row. -
Check the setup:
smllm harness status claudeThe command reminds you when your status line doesn’t call
smllm statusline.
Troubleshooting
- The whole status line goes blank: Claude Code blanks it when the script exits with a non-zero status. Keep the
ifform of the line in step 2, because a trailing[ -n "$row" ] && printf …exits with a non-zero status whenever smllm has nothing to show. - The row doesn’t appear: smllm prints nothing when the project has no smllm config, or before Claude Code’s session start hook has bound a session to smllm. The row appears as soon as Claude Code starts a session in a project with a config.
smllm harness installdidn’t add the row: it never changes thestatusLinesetting, because the setting is yours, and a setting at project level would override the status lines of your collaborators.
See also
Build a status line row of your own
When the default row shows more or less than you want, build a row of your own from the JSON fields of smllm statusline. You read the fields with jq and print the row with printf.
Before you begin
- A status line script that Claude Code runs, as in Show smllm in your status line.
jqis installed.
Steps
-
Read the fields of the session into a variable, in your status line script:
s=$(printf '%s' "$input" | smllm statusline --json)$inputis the status JSON that your script read from stdin. The status line row lists each field. -
Print the row from the fields. This row shows only the state, in bold yellow:
state=$(jq -r '.state // empty' <<<"$s") if [ -n "$state" ]; then printf '\n\033[1;33m%s\033[0m' "$state"; fiThis one puts the state and the instance on the same row as your other fields, and shows nothing when the session is idle:
where=$(jq -r 'select(.state) | "\(.state) \(.instance.label)"' <<<"$s") printf '%s%s' "$my_other_fields" "${where:+ | $where}"This one shows a warning colour while the agent has yielded:
if [ "$(jq -r '.yielded' <<<"$s")" = true ]; then printf '\n\033[43;30m waiting for you \033[0m' fi
See also
Pause and resume work
When an urgent task interrupts the work of an agent, pause the instance that it works on, and resume the instance when the urgent task is done. smllm keeps the paused instance at its state, so the agent, in this session or in another, continues where it stopped.
Steps
The steps use this config and the machine of Your first state machine:
.smllm/config.toml
[machines]
files = ["fix.smllm.yaml"]
.smllm/fix.smllm.yaml
id: fix
description: Fix a bug, from a test that reproduces it to the fix.
initial: TRIAGE
meta:
smllm: 1
instance:
kind: bug
ref: { param: bugId, pattern: "^BUG-[0-9]+$", description: "The bug's id, such as BUG-7." }
events:
reproduced:
description: Fire after a test reproduces the bug.
fixed:
description: Fire after the test passes.
states:
TRIAGE:
meta: { entryPoint: true }
entry: { type: prompt, params: { text: "Write a test that reproduces the bug, then fire reproduced with the bug's id." } }
on:
reproduced: { target: FIX, actions: setRef }
FIX:
entry: { type: prompt, params: { text: "Change the code until the test passes, then fire fixed." } }
on:
fixed: DONE
DONE:
type: final
entry: { type: prompt, params: { text: "Summarise the cause of the bug and the fix." } }
-
Start work on a bug. The agent enters the machine with the bug’s id, as it does when you ask it to fix
BUG-7:$ smllm fire --session sm-nqvfvw enter --param stateMachine=fix --param bugId=BUG-7 <smllm> session sm-nqvfvw · fix › TRIAGE · bug BUG-7 Arrived by: enter (new bug) Params: stateMachine = "fix", bugId = "BUG-7" <instructions> Write a test that reproduces the bug, then fire reproduced with the bug's id. </instructions> </smllm>sm-nqvfvwis the key of the session. Your key differs. -
To pause the bug, ask the agent to put it aside. The agent fires
pause, and smllm returns the session to idle:$ smllm fire --session sm-nqvfvw pause <smllm> session sm-nqvfvw · idle Paused bug BUG-7 at TRIAGE. State machines: - fix: Fix a bug, from a test that reproduces it to the fix. bug id param bugId (pattern: ^BUG-[0-9]+$): The bug's id, such as BUG-7. starts at: TRIAGE; entry points: TRIAGE Paused: - bug BUG-7 (fix) at TRIAGE Fire one event: smllm({ session: "sm-nqvfvw", event, params }) <events> - enter: Start a new instance of a state machine, or continue one. stateMachine (required, one of: fix): The state machine to enter. bugId (optional): The id param that the list of state machines names for the machine: an existing id or ref continues that instance, and a new ref starts one with it. Omit it to start a new instance. state (optional): An entry point to enter instead, or any state for an instance whose saved state no longer exists. Omit it to enter a new instance at the initial state, or an existing one at its saved state. </events> </smllm>The idle list shows the bug under
Paused:, so the agent sees it each time the session is idle. The session is free for the urgent task. -
List the instances of the project, to see the paused bug from the shell:
$ smllm instance list fix BUG-7 (i-ex2bq1) TRIAGE pausedEach line shows the machine, the instance’s ref and its ID, its state, and its status. To read the history of the bug, run
smllm instance showwith its ref or its ID:$ smllm instance show BUG-7 fix BUG-7 (i-ex2bq1) state: TRIAGE status: paused holder: - visits: TRIAGE=1 history: 2026-01-05 09:01 UTC sm-nqvfvw enter → TRIAGE bugId = BUG-7 stateMachine = fix 2026-01-05 09:02 UTC sm-nqvfvw pause TRIAGE → Paused bug BUG-7 at TRIAGE. -
To resume the bug, ask the agent to continue it. The agent fires
enterwith the bug’s id, and the bug continues at its saved state:$ smllm fire --session sm-nqvfvw enter --param stateMachine=fix --param bugId=BUG-7 <smllm> session sm-nqvfvw · fix › TRIAGE (visit 2) · bug BUG-7 Arrived by: enter Params: stateMachine = "fix", bugId = "BUG-7" <instructions> Write a test that reproduces the bug, then fire reproduced with the bug's id. </instructions> </smllm> -
To pick the bug up in another session, such as a new Claude Code session the next day, pause it in this session first:
$ smllm fire --session sm-nqvfvw pause --json {"ok":true,"session":"sm-nqvfvw","location":{"machine":null,"state":null,"instance":null,"ref":null},"text":"<smllm>\nsession sm-nqvfvw · idle\nPaused bug BUG-7 at TRIAGE.\nState machines:\n- fix: Fix a bug, from a test that reproduces it to the fix.\n bug id param bugId (pattern: ^BUG-[0-9]+$): The bug's id, such as BUG-7.\n starts at: TRIAGE; entry points: TRIAGE\nPaused:\n- bug BUG-7 (fix) at TRIAGE\nFire one event: smllm({ session: \"sm-nqvfvw\", event, params })\n<events>\n- enter: Start a new instance of a state machine, or continue one.\n stateMachine (required, one of: fix): The state machine to enter.\n bugId (optional): The id param that the list of state machines names for the machine: an existing id or ref continues that instance, and a new ref starts one with it. Omit it to start a new instance.\n state (optional): An entry point to enter instead, or any state for an instance whose saved state no longer exists. Omit it to enter a new instance at the initial state, or an existing one at its saved state.\n</events>\n</smllm>\n"}Then the agent of the other session enters the bug, as in step 4. From the shell,
smllm fire enterwithout--sessionstarts a new session, as a second agent would:$ smllm fire enter --param stateMachine=fix --param bugId=BUG-7 <smllm> session sm-6yv68n · fix › TRIAGE (visit 3) · bug BUG-7 Arrived by: enter Params: stateMachine = "fix", bugId = "BUG-7" <instructions> Write a test that reproduces the bug, then fire reproduced with the bug's id. </instructions> </smllm>A session that enters a bug that another session still holds takes the bug over, and the reply names the session that held it. Pause first, so that the first agent doesn’t go on working on the bug.
-
To reopen a bug that is complete, enter it at one of its entry points. Finish the bug first:
$ smllm fire --session sm-6yv68n reproduced --param bugId=BUG-7 --json {"ok":true,"session":"sm-6yv68n","location":{"machine":"fix","state":"FIX","instance":"i-ex2bq1","ref":"BUG-7"},"text":"<smllm>\nsession sm-6yv68n · fix › FIX · bug BUG-7\nArrived by: reproduced from TRIAGE\nParams: bugId = \"BUG-7\"\n<instructions>\nChange the code until the test passes, then fire fixed.\n</instructions>\n</smllm>\n"} $ smllm fire --session sm-6yv68n fixed --json {"ok":true,"session":"sm-6yv68n","location":{"machine":null,"state":null,"instance":null,"ref":null},"text":"<smllm>\nsession sm-6yv68n · fix › DONE · bug BUG-7\nArrived by: fixed from FIX\n<instructions>\nSummarise the cause of the bug and the fix.\n</instructions>\nCompleted bug BUG-7. You are now in idle.\nState machines:\n- fix: Fix a bug, from a test that reproduces it to the fix.\n bug id param bugId (pattern: ^BUG-[0-9]+$): The bug's id, such as BUG-7.\n starts at: TRIAGE; entry points: TRIAGE\nFire one event: smllm({ session: \"sm-6yv68n\", event, params })\n<events>\n- enter: Start a new instance of a state machine, or continue one.\n stateMachine (required, one of: fix): The state machine to enter.\n bugId (optional): The id param that the list of state machines names for the machine: an existing id or ref continues that instance, and a new ref starts one with it. Omit it to start a new instance.\n state (optional): An entry point to enter instead, or any state for an instance whose saved state no longer exists. Omit it to enter a new instance at the initial state, or an existing one at its saved state.\n</events>\n</smllm>\n"} $ smllm instance list fix BUG-7 (i-ex2bq1) DONE completedThen enter the bug with
state, the entry point to reopen it at:$ smllm fire --session sm-6yv68n enter --param stateMachine=fix --param bugId=BUG-7 --param state=TRIAGE <smllm> session sm-6yv68n · fix › TRIAGE (visit 4) · bug BUG-7 Arrived by: enter (reopened from DONE) Params: stateMachine = "fix", bugId = "BUG-7", state = "TRIAGE" Reopened bug BUG-7 at TRIAGE. <instructions> Write a test that reproduces the bug, then fire reproduced with the bug's id. </instructions> </smllm>The bug keeps its history and its visits, and
TRIAGEcounts its fourth visit, because eachenterthat continues or reopens the bug is a visit of its state.
Troubleshooting
- smllm refuses
enterwithbug BUG-7 is completed. To reopen it, fire enter with state: TRIAGE: the bug is complete, andenterwithoutstatedoesn’t reopen it. Addstatewith an entry point that the error names. pauseisn’t offered: the session is idle, and holds no instance to pause. Runsmllm instance listto see which instances are paused.
See also
Name work by your issue id
When each piece of work has an id in your issue tracker, such as GH-123, give the instance that id as its ref. The agent then enters the work by the id, and smllm shows the id in each header and list. You find the instance by the id instead of by its ID, such as i-ysh871.
Steps
The steps use this config and machine. The machine declares the ref in meta.instance, where param names the param that carries the id. pattern sets the form that the id must have, and description tells the agent what to pass. kind is the word for an instance in the text that the agent reads:
.smllm/config.toml
[machines]
files = ["dev.smllm.yaml"]
.smllm/dev.smllm.yaml
id: dev
description: Work on one issue, from a plan to a merged pull request.
initial: PLAN
meta:
smllm: 1
instance:
kind: issue
ref: { param: issue, pattern: "^GH-[0-9]+$", description: "The GitHub issue, such as GH-123." }
events:
planned:
description: Fire after you write the plan.
merged:
description: Fire after the pull request is merged.
states:
PLAN:
meta: { entryPoint: true }
entry: { type: prompt, params: { text: "Read the issue and write a plan." } }
on:
planned: WORK
WORK:
entry: { type: prompt, params: { text: "Carry out the plan, and open a pull request." } }
on:
merged: DONE
DONE:
type: final
Write [0-9] rather than \d in a pattern. In smllm, \d matches every Unicode digit, and in a JavaScript host it matches only the ASCII digits.
-
Check the machine:
$ smllm validate 0 errors, 0 warnings, 0 info -
Start a session. The idle list shows the param of the ref, its description, and its pattern, so the agent knows to pass the id:
<smllm> session sm-nqvfvw · idle State machines: - dev: Work on one issue, from a plan to a merged pull request. issue id param issue (pattern: ^GH-[0-9]+$): The GitHub issue, such as GH-123. starts at: PLAN; entry points: PLAN Fire one event: smllm({ session: "sm-nqvfvw", event, params }) <events> - enter: Start a new instance of a state machine, or continue one. stateMachine (required, one of: dev): The state machine to enter. issue (optional): The id param that the list of state machines names for the machine: an existing id or ref continues that instance, and a new ref starts one with it. Omit it to start a new instance. state (optional): An entry point to enter instead, or any state for an instance whose saved state no longer exists. Omit it to enter a new instance at the initial state, or an existing one at its saved state. </events> </smllm> -
Ask the agent to work on the issue. The agent enters the machine with the issue’s id, and the new instance takes the id as its ref:
$ smllm fire --session sm-nqvfvw enter --param stateMachine=dev --param issue=GH-123 <smllm> session sm-nqvfvw · dev › PLAN · issue GH-123 Arrived by: enter (new issue) Params: stateMachine = "dev", issue = "GH-123" <instructions> Read the issue and write a plan. </instructions> </smllm>sm-nqvfvwis the key of the session. Your key differs. -
Find the instance by its ref:
$ smllm instance list dev GH-123 (i-ysh871) PLAN active held by sm-nqvfvwsmllm instance show GH-123shows the instance with its history, and anenterwithissue=GH-123continues it, as Pause and resume work shows.
Troubleshooting
- smllm refuses
enterwithenter param issue must match ^GH-[0-9]+$ (got "123"): the id doesn’t have the form thatpatternsets. Pass the whole id, such asGH-123, or widen the pattern. - The id isn’t known when the work starts, such as for a bug that has no issue yet: enter without the ref, and name the instance later with a
setRefaction on the transition of an event that carries the ref’s param. Your first state machine names a bug this way.
See also
Use your machines in every project
When you use the same workflow in many projects, such as a review that you do on every pull request, put its machine in your user config. smllm combines the user config with the config of each project, so every session on your computer can enter the machine, and a project needs no copy of it.
Steps
-
Create the user config:
$ smllm init --user created ~/.config/smllm/config.tomlThe user config is
~/.config/smllm/config.tomlon Linux and macOS, and%APPDATA%\smllm\config.tomlon Windows. smllm prints the full path, and this guide shortens your home folder to~. The following steps use the path of Linux and macOS. -
Write a template machine beside the user config, and list it there:
$ smllm new review --write --config ~/.config/smllm/config.toml created ~/.config/smllm/review.smllm.yaml and listed it in ~/.config/smllm/config.toml--configmakessmllm newwritereview.smllm.yamlbeside that config,~/.config/smllm/review.smllm.yaml, and list it in the config’s[machines]table. Replace the template with your machine, as in Your first state machine. This guide uses a short review machine:~/.config/smllm/review.smllm.yamlid: review initial: READ meta: smllm: 1 states: READ: on: read: COMMENT COMMENT: on: commented: DONE DONE: type: finalThe command lists the machine in the user config:
~/.config/smllm/config.toml[machines] files = ["review.smllm.yaml"] -
Check the machine from any project:
$ smllm validate 0 errors, 0 warnings, 0 info $ smllm graph review review (initial READ) READ read → COMMENT COMMENT commented → DONE DONE [final]Both commands read the user config as well as the project’s, and so does every session. A folder without a config of its own runs the machines of the user config alone.
-
To change the machine for one project, define a machine with the same ID in the project’s config. Create the project’s config and a template machine
review:$ smllm init created .smllm/config.toml $ smllm new review --write created .smllm/review.smllm.yaml and listed it in .smllm/config.tomlReplace the template with the project’s machine. This one adds a step that runs the tests:
.smllm/review.smllm.yamlid: review initial: READ meta: smllm: 1 states: READ: on: read: TEST TEST: on: passed: COMMENT COMMENT: on: commented: DONE DONE: type: finalThe project’s machine replaces the user’s, and
smllm validate --inforeports which one it replaces:$ smllm validate --info .smllm/review.smllm.yaml: info: machine review here replaces the one in ~/.config/smllm/review.smllm.yaml (FMT-7) 0 errors, 0 warnings, 1 info
Troubleshooting
- An instance of a user machine shows in
smllm instance listof every project: smllm keeps the instances of each config beside that config, in itsstatefolder, so the instances of a user machine are in~/.config/smllm/state/. To keep them elsewhere, setdirin the[state]table of the user config, relative to the config file. - A machine of the user config doesn’t appear: the project defines a machine with the same ID, which replaces it. Run
smllm validate --infoto see the replacement.
See also
Take JSON in a param
When an event needs more than a line of text from the agent, such as a plan with a list of steps, give the event a param of type: json. The agent then passes a JSON value, and a JSON Schema tells it the shape of the value.
Steps
The steps use a schema of the value, in a JSON file beside the machine. This schema describes a plan as an object with a list of steps:
.smllm/plan.schema.json
{
"type": "object",
"properties": {
"steps": { "type": "array", "items": { "type": "string" }, "minItems": 1 }
},
"required": ["steps"]
}
The config lists the machine. The machine declares the param in the event’s params, with type: json and the schema’s path, relative to the machine file:
.smllm/config.toml
[machines]
files = ["plan.smllm.yaml"]
.smllm/plan.smllm.yaml
id: plan
description: Plan a change as a list of steps, then carry it out.
initial: PLAN
meta:
smllm: 1
events:
planned:
description: Fire with the plan, after you write it.
params:
type: object
properties:
plan:
type: json
description: The plan, as an object with its steps in order.
schema: plan.schema.json
required: [plan]
done:
description: Fire after the last step.
states:
PLAN:
meta: { entryPoint: true }
entry: { type: prompt, params: { text: "Write a plan, then fire planned with it." } }
on:
planned: WORK
WORK:
entry: { type: prompt, params: { text: "Carry out the plan, one step at a time." } }
on:
done: DONE
DONE:
type: final
schema can also hold the schema itself as a map, in place of a path. A schema declares draft-07 or 2020-12 in $schema, or no draft, which smllm reads as 2020-12.
-
Check the machine.
smllm validatereads the schema, and reports an error when the schema is invalid or names a file that smllm can’t read:$ smllm validate 0 errors, 0 warnings, 0 info -
Enter the machine, as the agent does:
$ smllm fire --session sm-nqvfvw enter --param stateMachine=plan <smllm> session sm-nqvfvw · plan › PLAN · instance i-0ekd6p Arrived by: enter (new instance) Params: stateMachine = "plan" <instructions> Write a plan, then fire planned with it. </instructions> </smllm>sm-nqvfvwis the key of the session. Your key differs.The events list shows the param as a JSON value, with its schema, so the agent knows what to pass:
<smllm> session sm-nqvfvw · plan › PLAN · instance i-0ekd6p Fire one event: smllm({ session: "sm-nqvfvw", event, params }) <events> - planned: Fire with the plan, after you write it. plan (required, a JSON value, schema: {"type":"object","properties":{"steps":{"type":"array","items":{"type":"string"},"minItems":1}},"required":["steps"]}): The plan, as an object with its steps in order. - yield: Stop for now and stay in PLAN. note (optional): What you are waiting for. - pause: Put instance i-0ekd6p aside and return to idle. - unmatched: The request fits none of these; handle it from idle, then resume. </events> </smllm> -
Fire the event with a JSON value. The agent passes the value as JSON in the smllm tool’s
params. From the shell,--paramreads the text after=as JSON, so quote it for the shell. smllm checks the value against the schema, and refuses a value that doesn’t match:$ smllm fire --session sm-nqvfvw planned --param 'plan={"steps": []}' <smllm> session sm-nqvfvw · plan › PLAN · instance i-0ekd6p error: planned param plan doesn't match its schema. At /steps: [] has less than 1 item Fire one event: smllm({ session: "sm-nqvfvw", event, params }) <events> - planned: Fire with the plan, after you write it. plan (required, a JSON value, schema: {"type":"object","properties":{"steps":{"type":"array","items":{"type":"string"},"minItems":1}},"required":["steps"]}): The plan, as an object with its steps in order. - yield: Stop for now and stay in PLAN. note (optional): What you are waiting for. - pause: Put instance i-0ekd6p aside and return to idle. - unmatched: The request fits none of these; handle it from idle, then resume. </events> </smllm> [exit 1]Each sentence after the error names a part of the value that fails, by its JSON path. A value that matches the schema moves the instance on:
$ smllm fire --session sm-nqvfvw planned --param 'plan={"steps": ["Write a test that fails", "Fix the parser"]}' <smllm> session sm-nqvfvw · plan › WORK · instance i-0ekd6p Arrived by: planned from PLAN Params: plan = {"steps":["Write a test that fails","Fix the parser"]} <instructions> Carry out the plan, one step at a time. </instructions> </smllm>A command action or guard of the transition reads the value as compact JSON text from the variable
SMLLM_PARAM_PLAN, the param’s name in upper snake case.
Troubleshooting
- smllm refuses the event with
planned param plan must be a JSON value: the text isn’t JSON. Quote a string as JSON, such asplan="\"one step\"", and quote the whole param for your shell. - smllm refuses the event with
planned param plan doesn't match its schema: the value is JSON, but a part of it fails the schema. Correct each part that the error names by its JSON path, and fire the event again. - A string param with
schema, or a JSON param withenumorpattern, is an error:schemabelongs only to a JSON param, andenumandpatternonly to a string param.
See also
See a machine as a diagram
When you review a machine, or show it to someone who doesn’t read YAML, draw it with smllm graph. The command prints the states and transitions as text or as a Mermaid diagram, opens an interactive diagram in your browser, or writes the diagram to a file.
Steps
The steps use this config and the machine of Your first state machine:
.smllm/config.toml
[machines]
files = ["fix.smllm.yaml"]
.smllm/fix.smllm.yaml
id: fix
description: Fix a bug, from a test that reproduces it to the fix.
initial: TRIAGE
meta:
smllm: 1
instance:
kind: bug
ref: { param: bugId, pattern: "^BUG-[0-9]+$", description: "The bug's id, such as BUG-7." }
events:
reproduced:
description: Fire after a test reproduces the bug.
fixed:
description: Fire after the test passes.
states:
TRIAGE:
meta: { entryPoint: true }
entry: { type: prompt, params: { text: "Write a test that reproduces the bug, then fire reproduced with the bug's id." } }
on:
reproduced: { target: FIX, actions: setRef }
FIX:
entry: { type: prompt, params: { text: "Change the code until the test passes, then fire fixed." } }
on:
fixed: DONE
DONE:
type: final
entry: { type: prompt, params: { text: "Summarise the cause of the bug and the fix." } }
-
Print the machine as text. Each state lists its transitions, with the event, the target, and the actions:
$ smllm graph fix fix (initial TRIAGE) TRIAGE [entry point] reproduced → FIX (setRef) FIX fixed → DONE DONE [final]Without an ID,
smllm graphprints every machine of the config. -
To put the diagram in a Markdown file, such as a pull request or a README, print it as Mermaid:
$ smllm graph fix --mermaid --- title: fix --- stateDiagram-v2 [*] --> TRIAGE TRIAGE --> FIX: reproduced FIX --> DONE: fixed DONE --> [*]GitHub and GitLab draw a fenced code block of language
mermaidas a diagram. -
To explore the machine, open it in your browser:
smllm graph fix --openThe page shows the diagram. Select a state or a transition to read its prompts, commands, guards, and events. A note beside the diagram lists the events that every state but a final one offers,
yield,pause, andunmatched, and theresumeof the fallback state. Select the note to read their guidance. smllm opens the page with the default browser, or with the program that the variableBROWSERnames. -
To share the interactive diagram, write it to an HTML page, which needs only a browser:
$ smllm graph fix --html fix.html wrote fix.html -
To put the diagram in a slide or a document, write it as a PNG picture:
$ smllm graph fix --png fix.png wrote fix.pngWhen you name no ID and the config has several machines, each machine goes to a file of its own:
smllm graph --png diagram.pngwritesdiagram-ID.pngfor each machineID, such asdiagram-fix.png.
See also
Check machine files in your editor
When you write a machine file, let your editor check it as you type, before you run smllm validate. smllm describes the machine format as a JSON Schema. An editor that reads JSON Schema for YAML then completes each key, shows its description, and marks a key that the format doesn’t have.
Before you begin
- An editor that runs the YAML language server, such as Visual Studio Code with the YAML extension by Red Hat.
Steps
-
Start each machine from
smllm new. Its template begins with a comment that tells the YAML language server where the schema is:$ smllm new review # yaml-language-server: $schema=https://raw.githubusercontent.com/six5536/smllm/main/schema/smllm.schema.json # An XState v5 machine config, in the subset that smllm reads. smllm's own data is under `meta`. id: review description: What this state machine is for. initial: START meta: smllm: 1 instance: kind: task ref: param: taskId description: "The task's id." events: done: description: Fire when the work that this state asks for is finished. states: START: meta: { entryPoint: true } entry: { type: prompt, params: { text: "Describe what the agent should do here." } } on: done: DONE DONE: type: final entry: { type: prompt, params: { text: "Summarise what you did." } }The comment points at the schema of the latest smllm. For a machine file that you wrote without the template, copy the first line into it.
-
To check against the schema of the smllm that you have installed, and to work offline, write the schema into the project:
smllm info schema > .smllm/smllm.schema.jsonThen point the comment of each machine file at that file, by a path relative to the machine file:
# yaml-language-server: $schema=./smllm.schema.jsonRun the command again after you upgrade smllm, because each version prints the schema of its own format.
-
To check every machine file without a comment in each, map the schema to the files’ names in your editor’s settings. In Visual Studio Code, add this to
.vscode/settings.jsonin the project:{ "yaml.schemas": { "./.smllm/smllm.schema.json": "**/*.smllm.yaml" } }The key is the schema’s path, relative to the folder that you opened, or a URL, such as the URL of step 1. The value is the pattern of the files that the schema checks.
-
Open a machine file, and type a key under
states.START. The editor offers the keys of a state, such asentry,on, andtype, and marks a misspelt key, such asentri.
Troubleshooting
- The editor marks nothing: the comment of the file comes before
yaml.schemas, so a comment with a wrong path turns the check off. Check the path of the comment, or remove the comment to use the setting. - The editor accepts a file that
smllm validaterejects: the schema describes the keys of the file and their types, andsmllm validatechecks more, such as that each target names a state. Runsmllm validatebefore an agent runs the machine.
See also
Drive smllm from a script
When an agent runs in a harness that has no MCP, or when a script or a test drives a machine, call smllm fire from the shell. smllm fire does what the smllm tool does: it fires one event, prints the reply that the agent reads, and exits with a code that says whether smllm accepted the event.
Before you begin
jqis installed, for the shell snippets that read the JSON.
Steps
The steps use this config and the machine of Your first state machine:
.smllm/config.toml
[machines]
files = ["fix.smllm.yaml"]
.smllm/fix.smllm.yaml
id: fix
description: Fix a bug, from a test that reproduces it to the fix.
initial: TRIAGE
meta:
smllm: 1
instance:
kind: bug
ref: { param: bugId, pattern: "^BUG-[0-9]+$", description: "The bug's id, such as BUG-7." }
events:
reproduced:
description: Fire after a test reproduces the bug.
fixed:
description: Fire after the test passes.
states:
TRIAGE:
meta: { entryPoint: true }
entry: { type: prompt, params: { text: "Write a test that reproduces the bug, then fire reproduced with the bug's id." } }
on:
reproduced: { target: FIX, actions: setRef }
FIX:
entry: { type: prompt, params: { text: "Change the code until the test passes, then fire fixed." } }
on:
fixed: DONE
DONE:
type: final
entry: { type: prompt, params: { text: "Summarise the cause of the bug and the fix." } }
-
Start a session: fire
enterwithout--session. With--json, smllm prints one JSON object. Its fieldoksays whether smllm accepted the event, andsessionholds the session’s key.locationsays where the session is, andtextholds the reply that the agent reads:$ smllm fire enter --param stateMachine=fix --json {"ok":true,"session":"sm-rpnmhe","location":{"machine":"fix","state":"TRIAGE","instance":"i-bzj1bm","ref":null},"text":"<smllm>\nsession sm-rpnmhe · fix › TRIAGE · bug i-bzj1bm\nArrived by: enter (new bug)\nParams: stateMachine = \"fix\"\n<instructions>\nWrite a test that reproduces the bug, then fire reproduced with the bug's id.\n</instructions>\n</smllm>\n"}In a script, keep the key and hand the text to the agent:
reply=$(smllm fire enter --param stateMachine=fix --json) key=$(jq -r .session <<<"$reply") jq -r .text <<<"$reply" -
Fire each event with the key. Each
--paramgives one param, asNAME=VALUE:$ smllm fire --session sm-rpnmhe reproduced --param bugId=BUG-7 --json {"ok":true,"session":"sm-rpnmhe","location":{"machine":"fix","state":"FIX","instance":"i-bzj1bm","ref":"BUG-7"},"text":"<smllm>\nsession sm-rpnmhe · fix › FIX · bug BUG-7\nArrived by: reproduced from TRIAGE\nParams: bugId = \"BUG-7\"\n<instructions>\nChange the code until the test passes, then fire fixed.\n</instructions>\n</smllm>\n"} -
Check the exit code of each run. smllm exits with
1when it refuses the event, and the reply says why and lists the events that the state offers:$ smllm fire --session sm-rpnmhe reproduced --param bugId=BUG-8 <smllm> session sm-rpnmhe · fix › FIX · bug BUG-7 error: reproduced is not offered here (offered: fixed, yield, pause, unmatched) Fire one event: smllm({ session: "sm-rpnmhe", event, params }) <events> - fixed: Fire after the test passes. - yield: Stop for now and stay in FIX. note (optional): What you are waiting for. - pause: Put bug BUG-7 aside and return to idle. - unmatched: The request fits none of these; handle it from idle, then resume. </events> </smllm> [exit 1]smllm exits with
2on a usage error, such as a key that names no session. It prints the error to standard error, and prints no reply:$ smllm fire --session sm-zzzzzz fixed error: no smllm session sm-zzzzzz: pass the key shown in the latest <smllm> header, or fire enter with no session to start a new one [exit 2] -
To see where a session is without firing an event, show it. The reply is the one that the smllm tool gives when the agent calls it with no event:
$ smllm session show sm-rpnmhe <smllm> session sm-rpnmhe · fix › FIX · bug BUG-7 <instructions> Change the code until the test passes, then fire fixed. </instructions> Fire one event: smllm({ session: "sm-rpnmhe", event, params }) <events> - fixed: Fire after the test passes. - yield: Stop for now and stay in FIX. note (optional): What you are waiting for. - pause: Put bug BUG-7 aside and return to idle. - unmatched: The request fits none of these; handle it from idle, then resume. </events> </smllm>smllm session show --jsonprints the same object assmllm fire --json. -
Put the steps together. This bash script shows each reply and fires the event that you type, with its params, until the session is idle again:
reply=$(smllm fire enter --param stateMachine=fix --json) key=$(jq -r .session <<<"$reply") while [ "$(jq -r '.location.state // empty' <<<"$reply")" != "" ]; do jq -r .text <<<"$reply" read -r -p 'event and params: ' event params args=() for p in $params; do args+=(--param "$p"); done reply=$(smllm fire --session "$key" "$event" "${args[@]}" --json) done jq -r .text <<<"$reply"location.stateisnullwhen the session is idle. A run that smllm refuses exits with1and still prints the reply, so the loop shows why and asks again. In a harness, the agent’s reply takes the place ofread.
Troubleshooting
- smllm refuses
firewithno session given: onlyenterstarts a session. Pass--sessionwith the key of an earlier reply for every other event. - The agent of a harness doesn’t see the instructions:
smllm fireprints the reply, and the harness has to show it to the agent. Give the agent thetextof the JSON, or the whole output without--json.
See also
Read the audit of a session
When you want to know what an agent did in a session, such as why it skipped a step or how often smllm refused an event, read the session’s audit. smllm writes one line for each call that drove the session: each event that the agent fired, each stop that the harness asked about, and the session’s start, with the outcome of each.
Steps
The steps use this config and the machine of Your first state machine:
.smllm/config.toml
[machines]
files = ["fix.smllm.yaml"]
.smllm/fix.smllm.yaml
id: fix
description: Fix a bug, from a test that reproduces it to the fix.
initial: TRIAGE
meta:
smllm: 1
instance:
kind: bug
ref: { param: bugId, pattern: "^BUG-[0-9]+$", description: "The bug's id, such as BUG-7." }
events:
reproduced:
description: Fire after a test reproduces the bug.
fixed:
description: Fire after the test passes.
states:
TRIAGE:
meta: { entryPoint: true }
entry: { type: prompt, params: { text: "Write a test that reproduces the bug, then fire reproduced with the bug's id." } }
on:
reproduced: { target: FIX, actions: setRef }
FIX:
entry: { type: prompt, params: { text: "Change the code until the test passes, then fire fixed." } }
on:
fixed: DONE
DONE:
type: final
entry: { type: prompt, params: { text: "Summarise the cause of the bug and the fix." } }
-
Let the agent work in a session. In this session, the agent enters the machine, fires
fixedtoo early, tries to stop, and then reproduces the bug:$ smllm fire --session sm-nqvfvw enter --param stateMachine=fix --json {"ok":true,"session":"sm-nqvfvw","location":{"machine":"fix","state":"TRIAGE","instance":"i-419405","ref":null},"text":"<smllm>\nsession sm-nqvfvw · fix › TRIAGE · bug i-419405\nArrived by: enter (new bug)\nParams: stateMachine = \"fix\"\n<instructions>\nWrite a test that reproduces the bug, then fire reproduced with the bug's id.\n</instructions>\n</smllm>\n"} $ smllm fire --session sm-nqvfvw fixed --json {"ok":false,"session":"sm-nqvfvw","location":{"machine":"fix","state":"TRIAGE","instance":"i-419405","ref":null},"text":"<smllm>\nsession sm-nqvfvw · fix › TRIAGE · bug i-419405\nerror: fixed is not offered here (offered: reproduced, yield, pause, unmatched)\nFire one event: smllm({ session: \"sm-nqvfvw\", event, params })\n<events>\n- reproduced: Fire after a test reproduces the bug.\n bugId (required, pattern: ^BUG-[0-9]+$): The bug's id, such as BUG-7.\n- yield: Stop for now and stay in TRIAGE.\n note (optional): What you are waiting for.\n- pause: Put bug i-419405 aside and return to idle.\n- unmatched: The request fits none of these; handle it from idle, then resume.\n</events>\n</smllm>\n"} [exit 1]When the agent tries to stop, Claude Code’s stop hook asks smllm, and smllm refuses the stop with this text:
<smllm> session sm-nqvfvw · fix › TRIAGE · bug i-419405 Fire one event: smllm({ session: "sm-nqvfvw", event, params }) <events> - reproduced: Fire after a test reproduces the bug. bugId (required, pattern: ^BUG-[0-9]+$): The bug's id, such as BUG-7. - yield: Stop for now and stay in TRIAGE. note (optional): What you are waiting for. - pause: Put bug i-419405 aside and return to idle. - unmatched: The request fits none of these; handle it from idle, then resume. </events> </smllm>$ smllm fire --session sm-nqvfvw reproduced --param bugId=BUG-7 --json {"ok":true,"session":"sm-nqvfvw","location":{"machine":"fix","state":"FIX","instance":"i-419405","ref":"BUG-7"},"text":"<smllm>\nsession sm-nqvfvw · fix › FIX · bug BUG-7\nArrived by: reproduced from TRIAGE\nParams: bugId = \"BUG-7\"\n<instructions>\nChange the code until the test passes, then fire fixed.\n</instructions>\n</smllm>\n"} -
Find the key of the session. Each
<smllm>header shows it, aftersession, andsmllm session listlists every session, the most recently active first, with its harness, where it is, when it was last active, and its folder. smllm prints the full path of each folder, and this guide shortens your home folder to~:$ smllm session list sm-nqvfvw claude fix i-419405 last active 2026-01-05 09:04 UTC ~/project -
Print the audit of the session:
$ smllm session show sm-nqvfvw --audit 2026-01-05 09:00 UTC agent bind idle accepted 2026-01-05 09:01 UTC agent fire enter fix i-419405 idle → TRIAGE accepted 2026-01-05 09:02 UTC agent fire fixed fix i-419405 TRIAGE refused: fixed is not offered here (offered: reproduced, yield, pause, unmatched) 2026-01-05 09:03 UTC agent stop fix i-419405 TRIAGE block 2026-01-05 09:04 UTC agent fire reproduced fix i-419405 TRIAGE → FIX acceptedEach line starts with the time of the call, in UTC, and its source:
agent, orhostfor an event that only the host that embeds smllm can fire. The call follows:bindwhen a harness binds its session to smllm,fireand its event, orstopwhen the harness asks whether the agent can stop. Then come the machine, the instance, and the state before the call. When the call moved the session,→and the state after the call follow.The outcome ends the line:
accepted,refusedwith the reason,blockfor a stop that smllm refused, orrunawayfor a stop that smllm allowed because the agent tried to stop again without firing an event. -
To filter the audit, or to keep it with a report, print it as JSON lines, one object for each call:
$ smllm session show sm-nqvfvw --audit --json {"at":1767603600000,"session":"sm-nqvfvw","source":"agent","call":"bind","event":null,"machine":null,"instance":null,"from":null,"to":null,"params":{},"offered":["enter"],"outcome":"accepted","reason":null,"trace":[],"binding":{"harness":"claude","hostSession":"cc-1","cwd":"~/project","configs":["~/project/.smllm/config.toml"]},"reply":"sha256:e32bba214c735287af4ff7a2590c135f3c5c643ebd26df19f906428b1dc4cc23"} {"at":1767603660000,"session":"sm-nqvfvw","source":"agent","call":"fire","event":"enter","machine":"fix","instance":"i-419405","from":null,"to":"TRIAGE","params":{"stateMachine":"fix"},"offered":[],"outcome":"accepted","reason":null,"trace":[],"binding":null,"reply":"sha256:ea7f7859c16348a748d815d7479e0a07af296be7572d634718dfc4ca8f1f1255"} {"at":1767603720000,"session":"sm-nqvfvw","source":"agent","call":"fire","event":"fixed","machine":"fix","instance":"i-419405","from":"TRIAGE","to":"TRIAGE","params":{},"offered":["reproduced","yield","pause","unmatched"],"outcome":"refused","reason":"fixed is not offered here (offered: reproduced, yield, pause, unmatched)","trace":[],"binding":null,"reply":"sha256:4f74289a359e206952a50a14a262ac1d163cbb48796beadda42644f890f8d96b"} {"at":1767603780000,"session":"sm-nqvfvw","source":"agent","call":"stop","event":null,"machine":"fix","instance":"i-419405","from":"TRIAGE","to":"TRIAGE","params":{},"offered":["reproduced","yield","pause","unmatched"],"outcome":"block","reason":null,"trace":[],"binding":null,"reply":"sha256:03ef41907cf05fd767716289eed9eb53feeb34c86ad415595a17dc74bfaf40cf"} {"at":1767603840000,"session":"sm-nqvfvw","source":"agent","call":"fire","event":"reproduced","machine":"fix","instance":"i-419405","from":"TRIAGE","to":"FIX","params":{"bugId":"BUG-7"},"offered":[],"outcome":"accepted","reason":null,"trace":[],"binding":null,"reply":"sha256:2b22dc5d256dfc28505f76bbfb423df2232dced33f0096a21fa4c86a30508aaa"}Each object has the fields of the line, and more.
atholds the time in Unix milliseconds, andparamsthe params of the call.offeredlists the events that the reply offered.tracerecords what the call ran, such as the result of each guard.The call that created the session also has
binding: the harness, its session, the folder, and the configs. That call is abind, or afire enterthat started a session without a key.This command lists only the calls that smllm didn’t accept:
smllm session show sm-nqvfvw --audit --json | jq -c 'select(.outcome != "accepted") | {call, event, from, outcome, reason}'For the session of step 1, it prints:
{"call":"fire","event":"fixed","from":"TRIAGE","outcome":"refused","reason":"fixed is not offered here (offered: reproduced, yield, pause, unmatched)"} {"call":"stop","event":null,"from":"TRIAGE","outcome":"block","reason":null}
Troubleshooting
- The audit has no line for a reply’s text: each line holds
reply, the hash of the reply that the agent was shown, assha256:and the hex of its SHA-256, not the text. To check that the agent was shown a text, compare the SHA-256 of the text withreply. - The audit takes space: smllm keeps the file of each session,
audit/KEY.jsonlin the folder of the sessions, until you remove it. On Linux and macOS the folder issmllm/in$XDG_STATE_HOME, which is~/.local/state/by default. On Windows it issmllm\in%APPDATA%.
See also
The command
This page lists the commands of smllm, then the forms of their output and their exit codes, then the files that smllm reads and writes. Commands and options lists the options of each command, as smllm COMMAND --help prints them.
Commands
smllm has these commands:
smllm init [--user]creates.smllm/config.tomlin the project, or with--userthe user config~/.config/smllm/config.toml.smllm new ID [--write]prints a template state machine with the IDID. With--write, it writesID.smllm.yamlbeside the config and lists it in the config.smllm validate [PATHS]…checks the configs, their state machines, and the saved instances, or only the files thatPATHSnames. It prints each finding asfile:line: level: message (CODE), whereCODEis the rule that the finding breaks, as The rules of smllm validate lists.--warningsand--infolist the warnings and the info findings too.smllm graph [ID]prints the states and transitions of a state machine, or of all of them, as text.--jsonprints JSON, and--mermaida Mermaid state diagram.--openopens the machine as a diagram in your web browser, where you select a state or a transition to read its prompts, commands, guards, and events.--html FILEwrites the same page to a file, and--png FILEwrites the diagram as a picture. With several machines,--png m.pngwritesm-ID.pngfor each machineID.smllm fire --session KEY EVENT [--param k=v]…fires an event from a shell, as the agent does through the smllm tool.smllm statusline [--json]prints where a session is, as The status line row describes.smllm session listandsmllm session show KEYlist the sessions, or show where one is, as the smllm tool does with no event.smllm session show KEY --audit [--json]prints the session’s audit: one line for each call that drove the session, accepted or refused, with its time, its source (agentorhost), the call, the state, and the outcome.smllm instance listandsmllm instance show IDlist the instances of the configured state machines, or show one, by its ID or its ref, with its history.smllm harness install HARNESS…andsmllm harness status HARNESS…install smllm into each harness, or show what is installed, as Harnesses describes.smllm info schemaprints the JSON Schema of the state machine format, for completion in your editor, andsmllm info rulesprints the rules page.smllm compile FILEwrites a config or a state machine as JSON for@six5536/smllm-wasm.smllm completions SHELLprints a completion script forbash,elvish,fish,powershell, orzsh.
In these commands, KEY is a session key, EVENT is the name of an event, and k=v gives the param k the value v. A JSON param reads v as JSON text, such as plan={"steps":[]}.
Output and exit codes
smllm exits with one of three codes:
0on success.1when it finds errors: an error in a config, a rejected event, or a failed hook.2on a usage error or an internal error.
smllm statusline always exits with 0, so that it never blanks a status line. Errors go to standard error with the prefix error: . With --json, a command prints exactly one JSON object, except for an audit, which it prints as JSON lines.
Files
smllm reads and writes these files:
- Configs:
.smllm/config.tomlin the project and~/.config/smllm/config.toml, which smllm combines. When both define a state machine with the same ID, the project’s wins. - Instances:
.smllm/state/in the project, which git ignores. - Sessions:
~/.local/state/smllm/. - Audits:
audit/KEY.jsonlin the folder of the sessions, one for each session, which smllm keeps until you remove it. Each line holds the hash of the reply that the agent was shown, assha256:and the hex of the SHA-256 of the reply’s UTF-8 text, not the text itself. - Cache:
~/.cache/smllm/, a copy of each load of your machines, which is safe to delete.
Commands and options
This page lists each command of smllm, with its arguments and its options, as smllm COMMAND --help prints them.
The commands:
smllmsmllm initsmllm newsmllm validatesmllm firesmllm sessionsmllm session listsmllm session showsmllm instancesmllm instance listsmllm instance showsmllm harnesssmllm harness installsmllm harness statussmllm graphsmllm infosmllm info schemasmllm info rulessmllm compilesmllm statuslinesmllm completions
smllm
State machines for LLM agents: declarative XState-style machines that drive an agent’s turn loop through its hooks and one MCP tool.
Usage: smllm [OPTIONS] [COMMAND]
Exit codes: 0 success, 1 errors found (a config error, a rejected event, or a failed hook), 2 a usage or internal error.
Documentation: https://github.com/six5536/smllm
Subcommands
init— Create .smllm/config.toml in this directory, or the user config.new— Print a template state machine, or write it to<ID>.smllm.yaml.validate— Check the configs, their state machines, and the saved instances.fire— Fire an event from the shell, as the smllm tool does.session— List sessions, or show where one is.instance— List instances, or show one with its history.harness— Install or check a harness integration (Claude Code, Codex, Cursor, and others).graph— Print the states and transitions of the state machines.info— Print reference information.compile— Compile a config or state machine to compact JSON, for smllm-wasm hosts.statusline— Print where a session is, as a status line row or as JSON.completions— Print a shell completion script to stdout.
Options
--config <FILE>— Use only this config file (SMLLM_CONFIG also sets it).-V,--version— Print the version and exit.
smllm init
Create .smllm/config.toml in this directory, or the user config
Usage: smllm init [OPTIONS]
Options
--user— Create the user config (~/.config/smllm/config.toml) instead.--json— Print JSON.
smllm new
Print a template state machine, or write it to <ID>.smllm.yaml
Usage: smllm new [OPTIONS] <ID>
Arguments
<ID>— The state machine id.
Options
--dir <DIR>— The directory to write it to (default: the directory of the config).--write— Write the file and list it in the config, instead of printing it.--json— Print JSON.
smllm validate
Check the configs, their state machines, and the saved instances
Usage: smllm validate [OPTIONS] [PATHS]...
Arguments
<PATHS>— Config (.toml) or state machine (.yaml) files (default: the configs found).
Options
--json— Print JSON.--warnings— List the warnings too.--info— List the info findings too.
smllm fire
Fire an event from the shell, as the smllm tool does
Usage: smllm fire [OPTIONS] <EVENT>
Arguments
<EVENT>— The event to fire.
Options
--session <KEY>— The session key (omit it only to start a session withenter).--param <KEY=VALUE>— A param of the event (repeatable).--json— Print JSON.
smllm session
List sessions, or show where one is
Usage: smllm session <COMMAND>
Subcommands
list— List sessions, the most recently active first.show— Show where a session is, as the smllm tool does with no event, or with –audit every call that drove it.
smllm session list
List sessions, the most recently active first
Usage: smllm session list [OPTIONS]
Options
--json— Print JSON.
smllm session show
Show where a session is, as the smllm tool does with no event, or with –audit every call that drove it
Usage: smllm session show [OPTIONS] <KEY>
Arguments
<KEY>— The session key.
Options
--audit— Print the session’s audit: one line for each call, or with –json the audit file’s JSON lines.--json— Print JSON.
smllm instance
List instances, or show one with its history
Usage: smllm instance <COMMAND>
Subcommands
list— List instances of the configured state machines.show— Show an instance, by id or ref, with its history.
smllm instance list
List instances of the configured state machines
Usage: smllm instance list [OPTIONS]
Options
--json— Print JSON.
smllm instance show
Show an instance, by id or ref, with its history
Usage: smllm instance show [OPTIONS] <KEY>
Arguments
<KEY>— The session key, or instance id or ref.
Options
--json— Print JSON.
smllm harness
Install or check a harness integration (Claude Code, Codex, Cursor, and others)
Usage: smllm harness <COMMAND>
Subcommands
install— Install or update a harness integration.status— Show the state of each part of a harness integration.
smllm harness install
Install or update a harness integration
Usage: smllm harness install [OPTIONS] <HARNESS>...
Arguments
<HARNESS>— The harnesses: claude, codex, factory, gemini, copilot, cursor, pi, opencode, kilo, qwen, devin, agents, or all.
Options
-
--scope <S>— The scope: project, user, or local (local for Claude Code and Copilot only).Default value:
project -
--without <PART>— Leave out a part, such as instructions, hooks, mcp, permissions, or statusline (repeatable). -
--force— Overwrite parts edited by hand. -
--json— Print JSON.
smllm harness status
Show the state of each part of a harness integration
Usage: smllm harness status [OPTIONS] <HARNESS>...
Arguments
<HARNESS>— The harnesses: claude, codex, factory, gemini, copilot, cursor, pi, opencode, kilo, qwen, devin, agents, or all.
Options
-
--scope <S>— The scope: project, user, or local (local for Claude Code and Copilot only).Default value:
project -
--without <PART>— Leave out a part, such as instructions, hooks, mcp, permissions, or statusline (repeatable). -
--force— Overwrite parts edited by hand. -
--json— Print JSON.
smllm graph
Print the states and transitions of the state machines
Usage: smllm graph [OPTIONS] [ID]
Arguments
<ID>— A state machine id (default: all of them).
Options
--json— Print JSON.--mermaid— Print a Mermaid stateDiagram-v2.--open— Open the interactive diagram in the default web browser (BROWSER sets the program).--html <FILE>— Write the interactive diagram to FILE, an HTML page that needs only a browser.--png <FILE>— Write the diagram as a PNG picture to FILE, or for several machines to STEM-ID.EXT beside it.
smllm info
Print reference information
Usage: smllm info <COMMAND>
Subcommands
schema— Print the JSON Schema of the state machine format.rules— Print the rules that validate checks, with the code that each finding ends with, as Markdown.
smllm info schema
Print the JSON Schema of the state machine format
Usage: smllm info schema
smllm info rules
Print the rules that validate checks, with the code that each finding ends with, as Markdown
Usage: smllm info rules
smllm compile
Compile a config or state machine to compact JSON, for smllm-wasm hosts
Usage: smllm compile [OPTIONS] <FILE>
Arguments
<FILE>— A config.toml or a state machine file.
Options
-o,--out <OUT>— The file to write to, instead of stdout.
smllm statusline
Print where a session is, as a status line row or as JSON.
The statusline command reads Claude Code’s status JSON on stdin and shows the session bound to its session_id. It prints nothing when there is nothing to show. It always exits 0, because a non-zero exit blanks Claude Code’s whole status line.
Usage: smllm statusline [OPTIONS]
Options
-
--session <KEY>— The session key (default: the session bound to thesession_idof the status JSON on stdin). -
--json— Print JSON ({}when there is nothing to show). -
--color <WHEN>— When to colour the row (default: auto).Possible values:
auto: Colour, unless NO_COLOR is set and not emptyalways: Colournever: No colour
smllm completions
Print a shell completion script to stdout
Usage: smllm completions <SHELL>
Arguments
-
<SHELL>— The shell to print a completion script for.Possible values:
bash,elvish,fish,powershell,zsh
The machine file
This page lists each key of a machine file, *.smllm.yaml, as the JSON Schema that smllm info schema prints describes it. Each section is one object of the file, named by its path from the top of the file, in the order that you meet them. In a path, <STATE> stands for the name of a state, <EVENT> for the name of an event, <PARAM> for the name of a param, and [] for an item of a list. Actions, guards, and transitions, which several keys take, have sections of their own. A key without yes under Required can be left out.
The machine
| Key | Type | Required | Description |
|---|---|---|---|
id | string | yes | The machine’s id, unique within the config: letters, digits, _, and -. |
description | string | The machine’s description, which the idle list shows. | |
initial | string | yes | The state in which a new instance starts. |
meta | object | yes | smllm’s data for the machine. |
states | map of objects | yes | The machine’s states. smllm does not support nested states. |
meta
smllm’s data for the machine.
| Key | Type | Required | Description |
|---|---|---|---|
smllm | integer | yes | The smllm format version, which is 1. |
instance | object | What the machine calls an instance, and the instance’s ref. | |
events | map of objects | The machine’s events, each declared once. A built-in event can set only its description. | |
sharedActions | list of objects | Actions added to the entry or exit actions of several states. |
states.<STATE>
One state.
| Key | Type | Required | Description |
|---|---|---|---|
description | string | The state’s description. | |
type | atomic or final | The state’s type. A final state completes the instance. atomic: An ordinary state, the default. final: A state that completes the instance. | |
meta | object | smllm’s data for the state. | |
entry | action, or a list of them | The actions that run when the state is entered. Without a prompt among them, smllm shows the prompt file enter-<STATE>.md, if it exists. The action setRef can be written as its bare name. | |
exit | action, or a list of them | The actions that run when the state is left. The action setRef can be written as its bare name. | |
on | map of (transition, or a list of them) | The transitions for each event. A transition can be written as the name of its target state. | |
always | transition, or a list of them | Transitions without an event. smllm leaves a state with always at once. A transition can be written as the name of its target state. | |
invoke | object, or a list of them | The host’s work while an instance is in this state. The state handles an event of source: host, through which the host fires the work’s result. |
meta.instance
| Key | Type | Required | Description |
|---|---|---|---|
kind | string | The word for an instance in the text that smllm writes, such as issue. The default is instance. | |
ref | object | The instance’s ref: its id outside smllm, such as an issue number. |
meta.events.<EVENT>
| Key | Type | Required | Description |
|---|---|---|---|
description | string | The event’s guidance for the agent, unless a transition gives its own. | |
params | object | The event’s params: a JSON Schema object of string and JSON properties. | |
source | agent or host | Who fires the event: agent, the default, or host. Only the host that embeds the engine fires a host event, and the agent never sees it. |
meta.sharedActions[]
One meta.sharedActions entry.
| Key | Type | Required | Description |
|---|---|---|---|
states | list of strings | yes | The states that the actions are added to. |
position | before or after | yes | Whether the actions run before or after the state’s own entry or exit actions. before: Before the state’s own actions. after: After the state’s own actions. |
entry | action, or a list of them | Actions added to each state’s entry. The action setRef can be written as its bare name. | |
exit | action, or a list of them | Actions added to each state’s exit. The action setRef can be written as its bare name. |
states.<STATE>.meta
smllm’s data for a state.
| Key | Type | Required | Description |
|---|---|---|---|
entryPoint | boolean | Whether the agent can enter this state directly from idle. Default: false. | |
fallback | boolean | Whether the unmatched event enters this state. At most one state of a machine can be the fallback. Default: false. | |
paramDescriptions | map of map of strings | Prompts for params in this state, by event and then by param. |
Actions
These keys take actions: states.<STATE>.entry, states.<STATE>.exit, meta.sharedActions[].entry, meta.sharedActions[].exit, <transition>.actions.
An action, written as {type, params}.
type: prompt
Shows text to the agent.
It needs params, an object with these keys:
| Key | Type | Required | Description |
|---|---|---|---|
text | string | The text itself. | |
file | string | A file of text, relative to the machine file, which smllm reads each time it shows the prompt. |
type: command
Runs a command.
It needs params, an object with these keys:
| Key | Type | Required | Description |
|---|---|---|---|
run | string or list of strings | yes | A string that a shell runs, or a list of a program and its arguments, run without a shell. String: A string that sh -c runs, or cmd /C on Windows. List of strings: A program and its arguments, run without a shell. |
timeoutSecs | integer | The time limit in seconds. The default is 60. | |
cwd | string | The working directory, relative to the machine file. The default is the session’s working directory. |
type: setRef
Sets the instance’s ref from the event’s ref param.
It takes no params.
Transitions
These keys take transitions: states.<STATE>.on.<EVENT>, states.<STATE>.always.
A transition object.
| Key | Type | Required | Description |
|---|---|---|---|
target | string | The target state. A transition without a target stays in the state without re-entering it. | |
guard | guard | A condition that must pass for the transition to be taken. | |
actions | action, or a list of them | The actions that run when the transition is taken. The action setRef can be written as its bare name. | |
reenter | boolean | Whether a transition to its own state leaves and re-enters it, as in XState. | |
description | string | Guidance for the event in this state. |
states.<STATE>.invoke
The host’s work while an instance is in its state: XState’s invoke, with only src and input. The host runs the work and fires its result as an event of source: host.
| Key | Type | Required | Description |
|---|---|---|---|
src | string | yes | The name of the work, by which the host finds its code. |
input | object | Fixed values that the host passes to the work. |
meta.instance.ref
| Key | Type | Required | Description |
|---|---|---|---|
param | string | The name of the ref’s param, which enter and setRef use. The default is ref. | |
description | string | The prompt that asks for the ref. | |
pattern | string | A pattern that the ref must match, in the syntax of a param’s pattern. |
meta.events.<EVENT>.params
A subset of a JSON Schema of type: object.
| Key | Type | Required | Description |
|---|---|---|---|
type | string | yes | Always object. |
properties | map of objects | The params, by name. | |
required | list of strings | The names of the required params. |
Guards
These keys take guards: <transition>.guard.
A guard, written as {type, params}.
type: command
Passes when the command exits with status 0.
It needs params, an object with these keys:
| Key | Type | Required | Description |
|---|---|---|---|
run | string or list of strings | yes | A string that a shell runs, or a list of a program and its arguments, run without a shell. String: A string that sh -c runs, or cmd /C on Windows. List of strings: A program and its arguments, run without a shell. |
timeoutSecs | integer | The time limit in seconds. The default is 60. | |
cwd | string | The working directory, relative to the machine file. The default is the session’s working directory. |
type: visits
Passes when state has been entered at least atLeast times.
It needs params, an object with these keys:
| Key | Type | Required | Description |
|---|---|---|---|
state | string | yes | The state whose entries are counted. |
atLeast | integer | yes | The least number of entries, counting the current one. |
meta.events.<EVENT>.params.properties.<PARAM>
One param’s schema.
| Key | Type | Required | Description |
|---|---|---|---|
type | string or json | string, the default, or json, for a param that takes any JSON value. | |
description | string | The prompt for the param. | |
enum | list of strings | The allowed values of a string param. | |
pattern | string | A pattern that a string param’s value must match, in Rust regex syntax: the common subset of JSON Schema’s ECMA-262 patterns, without lookaround or backreferences. In smllm, \d, \w, and \s match Unicode, and in a JavaScript host they match only ASCII. To match ASCII digits in both, write [0-9]. | |
schema | string or object | The JSON Schema of a json param’s value, draft-07 or 2020-12: a path relative to the machine file, or the schema itself as a map. String: A JSON file, relative to the machine file. The path can lead outside the machine’s folder. Object: The schema itself. |
The agent’s text
This page lists what smllm says to the agent: first the parts of a <smllm> block in the order in which they appear, then the header, the idle list, the entry block, and the events list, then the built-in events, and last the text of a reply, a refused event, and a blocked stop. The agent reads this text in the hooks of its harness and in the replies of the smllm tool, and smllm fire prints the same text in a shell.
- The example machine
- The block
- The header
- The idle list
- The entry block
- The events list
- The built-in events
- A refused event
- A blocked stop
- The replies to a fired event
The example machine
The examples of this page come from one session, in this order, with this config and this machine. The machine guides the agent through an issue: in PLAN the agent writes a plan, and a command guard checks that the plan exists. In WORK the agent makes the change, and DONE ends the work.
.smllm/config.toml
[machines]
files = ["dev.smllm.yaml"]
.smllm/dev.smllm.yaml
id: dev
description: Work on an issue, from a plan to a change.
initial: PLAN
meta:
smllm: 1
instance:
kind: issue
ref: { param: issueId, pattern: "^GH-[0-9]+$", description: "The issue's id, such as GH-12." }
events:
planned:
description: Fire after you write the plan in PLAN.md.
params:
type: object
properties:
size: { description: "How large the change is.", enum: [small, large] }
required: [size]
done:
description: Fire after you make the change.
states:
PLAN:
meta: { entryPoint: true }
entry: { type: prompt, params: { text: "Write a plan for the issue in PLAN.md, then fire planned." } }
on:
planned:
- { target: WORK, guard: { type: command, params: { run: "test -f PLAN.md" } } }
- { target: PLAN, reenter: true }
WORK:
entry: { type: prompt, params: { text: "Make the change that PLAN.md describes, then fire done." } }
on:
done: DONE
DONE:
type: final
entry: { type: prompt, params: { text: "Summarise the change for the issue." } }
The block
Each text that smllm gives the agent is one block, fenced in <smllm> and </smllm>. A block holds these parts, in this order, and leaves out each part that it doesn’t need:
- The header.
Arrived by: HOW, which says how the instance reached its state, as the entry block lists.Params: NAME = "VALUE", …, the params of the event that the agent fired.- The notes, one on each line, such as
Paused issue GH-12 at WORK. - The trace of the guards that smllm checked, and the failures of actions and prompts.
error: MESSAGE, when smllm refused the event, as a refused event lists.<instructions>, which holds the text of the prompts that the state’s actions gave, in the order in which they ran, with a blank line between two prompts.- After a final state,
Completed KIND LABEL. You are now in idle., then the idle list. - The events list.
The text of a machine’s author goes into <instructions> as it is. Every other line goes into the block on one line of its own: a line break shows as \n, and a < that starts the tag smllm, instructions, or events shows as <. A ref or a param value therefore can’t close the block. smllm validate warns about author text that contains a closing tag, as rule AGT-1 says.
The header
The first line of every block says where the session is. It takes one of three forms:
| Where | Header |
|---|---|
| In a machine | session KEY · MACHINE › STATE[ (visit N)] · KIND LABEL |
| In idle | session KEY · idle |
| No session was made | no session · idle |
The parts mean this:
KEYis the session key, such assm-nqvfvw, which the agent passes on every call of the smllm tool.MACHINEis the state machine, andSTATEis the instance’s current state.(visit N)appears from the second visit to a state, andNis the number of the visit. Each time the instance enters the state counts as a visit, also whenenterorresumebrings it there.KINDis what the machine calls an instance, frommeta.instance.kind, andinstancewhen the machine doesn’t say.LABELis the instance’s ref, such asGH-12, after the ref is set, and its ID, such asi-k0xzjt, before.no session · idleheads the refusal of anenterthat the agent fired without a session key, because smllm made no session for it.
The idle list
A session that holds no instance is in idle. The idle list is the block of idle, which the agent sees when its session starts:
<smllm>
session sm-nqvfvw · idle
State machines:
- dev: Work on an issue, from a plan to a change.
issue id param issueId (pattern: ^GH-[0-9]+$): The issue's id, such as GH-12.
starts at: PLAN; entry points: PLAN
Fire one event: smllm({ session: "sm-nqvfvw", event, params })
<events>
- enter: Start a new instance of a state machine, or continue one.
stateMachine (required, one of: dev): The state machine to enter.
issueId (optional): The id param that the list of state machines names for the machine: an existing id or ref continues that instance, and a new ref starts one with it. Omit it to start a new instance.
state (optional): An entry point to enter instead, or any state for an instance whose saved state no longer exists. Omit it to enter a new instance at the initial state, or an existing one at its saved state.
</events>
</smllm>
After the header, the notes, and any error, the idle list has these lines:
<instructions>holds the text that the config’s[idle]table gives, when it gives one.State machines:starts the list of the machines of the config, orNo state machines are configured.replaces it.- ID: DESCRIPTIONnames a machine.KIND id param PARAM (pattern: PATTERN): DESCRIPTIONnames the param that holds the machine’s ref.starts at: INITIAL; entry points: STATE, …names the initial state and the states thatentercan start in.Interrupted: KIND LABEL (MACHINE) at STATEnames the instance thatunmatchedput aside, when there is one.Paused:starts the list of paused instances, with one- KIND LABEL (MACHINE) at STATEline for each. The list shows at most 10, the most recently updated first.…and N more paused: fire listPaused to list them all.follows a list that leaves some out.- The events list of idle comes last.
The entry block
When an event moves the instance to a state, the reply is the state’s entry block: the header, Arrived by, the params, the notes and the trace, and <instructions>. The entry block has no events list:
$ smllm fire --session sm-nqvfvw enter --param stateMachine=dev --param issueId=GH-12
<smllm>
session sm-nqvfvw · dev › PLAN · issue GH-12
Arrived by: enter (new issue)
Params: stateMachine = "dev", issueId = "GH-12"
<instructions>
Write a plan for the issue in PLAN.md, then fire planned.
</instructions>
</smllm>
Arrived by takes one of these forms:
EVENT from STATE: the eventEVENTmoved the instance fromSTATE.enter (new KIND):enterstarted a new instance.enter:entercontinued an instance at its saved state.enter (jump from STATE):enterwithstatemoved an instance fromSTATEto an entry point.enter (reopened from STATE):enterwithstatereopened a completed instance.enter (saved state STATE no longer exists):enterwithstatemoved an instance whose saved state the machine no longer has.resume:resumereturned the session to the instance that it put aside.unmatched from STATE:unmatchedmoved the instance to the machine’s fallback state.
A form ends with via STATE, … when the instance passed through states with always transitions on its way.
The notes and the trace take these forms:
Took over from session KEY (last active TIME): the instance belonged to another session, which loses it.Reopened KIND LABEL at STATE.Guard: command `RUN` → true, orGuard: command `RUN` → false (DETAIL): the result of acommandguard, whereDETAILis the exit status and the end of the command’s output.Guard: visits STATE ≥ N → BOOL (visits: COUNT): the result of avisitsguard.Action failed: LIST[INDEX] KIND: DETAIL: an action failed, and smllm skipped the commands after it in the same list.Prompt failed: FILE: REASON: smllm couldn’t read the file of a prompt.No always transition of STATE matched; stopped thereandStopped after 32 always transitions; check for a loop: thealwaystransitions couldn’t leave a state.
The events list
The events list names each event that the agent can fire in the session’s current state, with its params. The agent sees it when it calls the smllm tool with no event, when its session starts again, after a refused event, and when smllm blocks a stop. smllm session show prints the same block:
$ smllm session show sm-nqvfvw
<smllm>
session sm-nqvfvw · dev › PLAN · issue GH-12
<instructions>
Write a plan for the issue in PLAN.md, then fire planned.
</instructions>
Fire one event: smllm({ session: "sm-nqvfvw", event, params })
<events>
- planned: Fire after you write the plan in PLAN.md.
size (required, one of: small, large): How large the change is.
- yield: Stop for now and stay in PLAN.
note (optional): What you are waiting for.
- pause: Put issue GH-12 aside and return to idle.
- unmatched: The request fits none of these; handle it from idle, then resume.
</events>
</smllm>
The list has these forms:
Fire one event: smllm({ session: "KEY", event, params })shows the call that fires an event. Without a session, it readsFire one event: smllm({ event, params }).<events>holds one item for each event:- NAME: DESCRIPTION.- Each param of the event follows its event on a line of its own, indented by four spaces:
NAME (required|optional[, one of: VALUE, …][, pattern: PATTERN][, a JSON value[, schema: SCHEMA]]): DESCRIPTION.
In a machine state, the list names the state’s own events in the order of its on, then the built-in events. An event’s description comes from its transition’s description, or else from meta.events. A param’s description comes from the state’s meta.paramDescriptions, or else from the param’s description. The list leaves out each event that the machine marks with source: host, because only the host fires it.
The built-in events
smllm adds its built-in events to every machine:
| Event | Offered | Params | What it does |
|---|---|---|---|
enter | In idle | stateMachine, the ref param, and state | Starts a new instance, or continues, jumps, or reopens one. |
resume | In idle with an interrupted instance, and in the fallback state after unmatched | None | Returns to the state that unmatched left. |
listPaused | In idle, when the idle list leaves paused instances out | None | Lists every paused instance, and changes nothing. |
yield | In each state that isn’t final | note, optional | Keeps the instance in its state and lets the agent stop. |
pause | In each state | None | Puts the instance aside as paused, and returns the session to idle. |
unmatched | In each state | None | Moves the instance to the fallback state, or puts it aside as interrupted and returns the session to idle. |
A machine can change the description of a built-in event in meta.events, but not its params or its source, as rule AGT-2 says. A state can’t handle a built-in event under on.
A refused event
When smllm refuses an event, nothing changes, and the reply is the header, error: MESSAGE, and the events list. smllm fire exits with 1:
$ smllm fire --session sm-nqvfvw done
<smllm>
session sm-nqvfvw · dev › PLAN · issue GH-12
error: done is not offered here (offered: planned, yield, pause, unmatched)
Fire one event: smllm({ session: "sm-nqvfvw", event, params })
<events>
- planned: Fire after you write the plan in PLAN.md.
size (required, one of: small, large): How large the change is.
- yield: Stop for now and stay in PLAN.
note (optional): What you are waiting for.
- pause: Put issue GH-12 aside and return to idle.
- unmatched: The request fits none of these; handle it from idle, then resume.
</events>
</smllm>
[exit 1]
$ smllm fire --session sm-nqvfvw planned --param size=huge
<smllm>
session sm-nqvfvw · dev › PLAN · issue GH-12
error: planned param size must be one of: small, large (got "huge")
Fire one event: smllm({ session: "sm-nqvfvw", event, params })
<events>
- planned: Fire after you write the plan in PLAN.md.
size (required, one of: small, large): How large the change is.
- yield: Stop for now and stay in PLAN.
note (optional): What you are waiting for.
- pause: Put issue GH-12 aside and return to idle.
- unmatched: The request fits none of these; handle it from idle, then resume.
</events>
</smllm>
[exit 1]
In a machine state, the message takes one of these forms:
EVENT is not offered here (offered: EVENT, …)EVENT is an event that only the host fires (offered: EVENT, …)EVENT needs param NAMEEVENT has no param NAME (params: NAME, …)EVENT takes no params, but got NAMEEVENT param NAME must be one of: VALUE, … (got "VALUE")EVENT param NAME must match PATTERN (got "VALUE")EVENT param NAME must be a stringEVENT param NAME must be a JSON value: REASONEVENT param NAME doesn't match its schema. At PATH: ERROR, with one sentence for each part of the value that fails,the rootas the PATH of the whole value, and at most ten sentences, thenAnd N moreKIND ID already has ref REF, and smllm sets a ref only onceanother KIND of MACHINE already has ref REFthe interrupted state STATE no longer exists; leave with one of this state's events, or pause and enter with a state: STATE, …
In idle, the message takes one of these forms:
EVENT is not offered in idle (offered: EVENT, …)enter needs param stateMachineenter param stateMachine must be one of: MACHINE, … (got "VALUE")enter has no param NAME for MACHINE (params: stateMachine, PARAM, state)enter got param NAME more than onceenter param PARAM must match PATTERN (got "VALUE")STATE is not an entry point of MACHINE (entry points: STATE, …)KIND LABEL is completed. To reopen it, fire enter with state: STATE, orone of STATE, …when the machine has several entry pointsKIND LABEL is completed, and no state is an entry point to reopen it atstate machine MACHINE needs a host to drive it: only the host fires its events EVENT, …MACHINE has no state STATEsaved state STATE of KIND LABEL no longer exists. Fire enter with state: one of STATE, …listPaused takes no params, but got NAMEresume takes no params, but got NAMEstate machine MACHINE is not configured (its file might have errors); fix the config, then resumeLABEL moved to session KEYthe interrupted instance is no longer interruptedsaved state STATE no longer exists. Fire enter with stateMachine MACHINE, PARAM LABEL, state: one of STATE, …
When the session’s instance has left it, smllm returns the session to idle, and the reply is the idle list with one of these messages:
KIND LABEL moved to session KEY; you are in idle: another session took the instance over.KIND LABEL is STATUS; you are in idle: the instance is no longer active.another session changed KIND LABEL; you are in idle: another session wrote the instance first.instance ID no longer exists; you are in idle
A blocked stop
When the agent tries to end its turn in a state that isn’t final, the stop hook of the harness asks smllm, and smllm blocks the stop. The harness gives the agent this block, which is the header and the events list, without the instructions:
<smllm>
session sm-nqvfvw · dev › PLAN · issue GH-12
Fire one event: smllm({ session: "sm-nqvfvw", event, params })
<events>
- planned: Fire after you write the plan in PLAN.md.
size (required, one of: small, large): How large the change is.
- yield: Stop for now and stay in PLAN.
note (optional): What you are waiting for.
- pause: Put issue GH-12 aside and return to idle.
- unmatched: The request fits none of these; handle it from idle, then resume.
</events>
</smllm>
smllm lets a stop through without a block in these cases:
- The session is in idle.
- The agent fired
yieldafter the user’s last prompt, and no transition has happened since. - The agent tries to stop a second time in a row without firing an event. smllm records this stop as
runawayin the session’s audit, and writes the block to standard error instead. - The harness can’t block a stop, as Harnesses lists. In GitHub Copilot the session start binds no session, so the stop hook allows every stop without a block. The
agentsintegration runs no hooks. Inopencode runthe run ends with the agent’s turn, so the agent never reads the block.
What happens when the agent stops explains these rules.
The replies to a fired event
A fired event that smllm accepts gets one of these replies:
- A transition gets the entry block of the state that the instance reaches.
- A transition to a final state gets the state’s block,
Completed KIND LABEL. You are now in idle., and the idle list. yieldgets the header andYielded: staying in STATE. You can end your turn.pauseandunmatchedwithout a fallback state get the idle list, with a note that names the instance that the event put aside.listPausedgets the header of idle, the list of every paused instance, and the events list of idle.
A guard that fails moves the instance along the event’s next transition. Here the plan doesn’t exist yet, so planned re-enters PLAN, and the trace shows the guard:
$ smllm fire --session sm-nqvfvw planned --param size=small
<smllm>
session sm-nqvfvw · dev › PLAN (visit 2) · issue GH-12
Arrived by: planned from PLAN
Params: size = "small"
Guard: command `test -f PLAN.md` → false (exited 1)
<instructions>
Write a plan for the issue in PLAN.md, then fire planned.
</instructions>
</smllm>
After the agent writes the plan, the guard passes:
PLAN.md
Rename the option `--colour` to `--color`.
$ smllm fire --session sm-nqvfvw planned --param size=small
<smllm>
session sm-nqvfvw · dev › WORK · issue GH-12
Arrived by: planned from PLAN
Params: size = "small"
Guard: command `test -f PLAN.md` → true
<instructions>
Make the change that PLAN.md describes, then fire done.
</instructions>
</smllm>
These are the replies to yield, to pause, and to an enter that continues the paused issue:
$ smllm fire --session sm-nqvfvw yield --param "note=Waiting for the user to approve the plan."
<smllm>
session sm-nqvfvw · dev › WORK · issue GH-12
Yielded: staying in WORK. You can end your turn.
</smllm>
$ smllm fire --session sm-nqvfvw pause
<smllm>
session sm-nqvfvw · idle
Paused issue GH-12 at WORK.
State machines:
- dev: Work on an issue, from a plan to a change.
issue id param issueId (pattern: ^GH-[0-9]+$): The issue's id, such as GH-12.
starts at: PLAN; entry points: PLAN
Paused:
- issue GH-12 (dev) at WORK
Fire one event: smllm({ session: "sm-nqvfvw", event, params })
<events>
- enter: Start a new instance of a state machine, or continue one.
stateMachine (required, one of: dev): The state machine to enter.
issueId (optional): The id param that the list of state machines names for the machine: an existing id or ref continues that instance, and a new ref starts one with it. Omit it to start a new instance.
state (optional): An entry point to enter instead, or any state for an instance whose saved state no longer exists. Omit it to enter a new instance at the initial state, or an existing one at its saved state.
</events>
</smllm>
$ smllm fire --session sm-nqvfvw enter --param stateMachine=dev --param issueId=GH-12
<smllm>
session sm-nqvfvw · dev › WORK (visit 2) · issue GH-12
Arrived by: enter
Params: stateMachine = "dev", issueId = "GH-12"
<instructions>
Make the change that PLAN.md describes, then fire done.
</instructions>
</smllm>
This is the reply to the event that reaches the final state:
$ smllm fire --session sm-nqvfvw done
<smllm>
session sm-nqvfvw · dev › DONE · issue GH-12
Arrived by: done from WORK
<instructions>
Summarise the change for the issue.
</instructions>
Completed issue GH-12. You are now in idle.
State machines:
- dev: Work on an issue, from a plan to a change.
issue id param issueId (pattern: ^GH-[0-9]+$): The issue's id, such as GH-12.
starts at: PLAN; entry points: PLAN
Fire one event: smllm({ session: "sm-nqvfvw", event, params })
<events>
- enter: Start a new instance of a state machine, or continue one.
stateMachine (required, one of: dev): The state machine to enter.
issueId (optional): The id param that the list of state machines names for the machine: an existing id or ref continues that instance, and a new ref starts one with it. Omit it to start a new instance.
state (optional): An entry point to enter instead, or any state for an instance whose saved state no longer exists. Omit it to enter a new instance at the initial state, or an existing one at its saved state.
</events>
</smllm>
Harnesses
This page lists what smllm harness install writes, then what each harness supports, then the Claude Code plugin. A harness is the program that runs the agent, such as Claude Code or Codex.
Install
smllm harness install connects smllm to one or more harnesses:
smllm harness install claude # CLAUDE.md/AGENTS.md block, .mcp.json, hooks, permission,
# status line skill
smllm harness install claude codex # several at once; AGENTS.md and .mcp.json written once
smllm harness install all # every harness: claude codex factory gemini copilot cursor pi opencode kilo qwen devin agents
smllm harness status all # what is installed, part by part
--scope says where the integration goes: the project (--scope project, the default), your user (--scope user), or git-ignored local settings (--scope local, for Claude Code and Copilot only).
Each harness gets as much as it can take of three parts: the instructions block, the hooks (smllm harness hook HARNESS EVENT), and the smllm MCP server. smllm writes each part where the harness reads it. The report lists what a harness can’t take (unsupported) and what you still have to do (note), for example trust the folder in Codex or Pi.
What each harness supports
The hooks differ by harness:
- Claude Code, Codex, Factory, Gemini CLI, Qwen Code, Devin, and Cursor: smllm uses their own hooks. The agent gets the session when it starts, and smllm blocks a stop.
- Pi, OpenCode, and Kilo Code: these harnesses have no hooks config, so smllm installs a Pi extension or an OpenCode or Kilo plugin that runs the same hook command. The agent gets the session with the first prompt. A blocked stop works in OpenCode’s terminal user interface (TUI) and server, but not in
opencode run. - GitHub Copilot: Copilot gets the tool and the instructions. smllm can’t show Copilot the session, so its hooks stay quiet: the agent starts with
enter, and smllm doesn’t block a stop. agents: any other agent that readsAGENTS.mdgets only the instructions block, with no hooks and no MCP server. The agent fires events withsmllm firefrom the shell.
The Claude Code plugin
Claude Code can also take smllm as a plugin:
claude plugin marketplace add six5536/smllm && claude plugin install smllm@smllm
Use either the plugin or smllm harness install claude, not both, because with both the hooks and the MCP server run twice. With the plugin, the tool is named mcp__plugin_smllm_smllm__smllm. harness install approves the tool in advance in the project settings, and with the plugin, Claude Code asks for permission the first time instead.
The status line row
This page lists the forms of the row that smllm statusline prints, then its options, its JSON fields, and its use from harnesses other than Claude Code.
smllm statusline prints where the current session is, as one row for your harness’s status line. The command reads Claude Code’s status JSON on stdin and finds the smllm session bound to its session_id. When there is nothing to show, because there is no smllm config or smllm hasn’t bound a session yet, it prints nothing. The command only reads, takes a few milliseconds, and always exits with 0, so it can’t blank the rest of your status line.
The default row
The row takes one of two forms:
| Where | Row |
|---|---|
| In a machine | smllm MACHINE › STATE[ (visit N)] · KIND LABEL[ · yielded] |
| In idle | smllm idle[ · N interrupted][ · N paused] |
These rows show each form:
smllm dev › WORK (visit 3) · issue GH-123
smllm dev › REVIEW · issue GH-123 · yielded
smllm idle · 1 interrupted · 2 paused
The parts mean this:
MACHINEis the state machine, andSTATEis its current state.(visit N)appears from the second visit to a state, andNis the number of the visit.KIND LABELis the instance, for exampleissue GH-123. The label is the instance’s ref after the ref is set, and its ID before.N interruptedandN pausedgive the number of interrupted and paused instances.yieldedmeans that the agent firedyield, so it can stop until your next prompt.
The row has these colours: smllm dim, the machine cyan, the state bold, the instance magenta,
the notes dim, and yielded yellow. Colour is on by default, because a status line command never
runs in a terminal that it could check. To turn colour off, set NO_COLOR=1 or pass
--color never.
Options
The command takes these options:
smllm statusline [--session KEY] [--json] [--color auto|always|never]
--session KEYshows the session with the keyKEYinstead of reading stdin.--jsonprints the fields described in the JSON fields instead of the row.--colortakesalways,never, orauto.automeans always, unlessNO_COLORis set.
The JSON fields
smllm statusline --json prints one object, or {} when there is nothing to show. Every key is
always present, and its value is null when it doesn’t apply.
| Field | Type | Meaning |
|---|---|---|
session | string | The smllm session key (sm-nqvfvw) |
idle | bool | True when the session holds no instance |
machine | string | null | The state machine of the held instance |
state | string | null | The held instance’s current state |
visit | number | null | The number of times the state has been entered |
yielded | bool | True when the agent fired yield after your last prompt |
instance | object | null | The held instance |
interrupted | object | null | The instance that unmatched put aside |
paused | number | The number of paused instances of the configured state machines |
instance and interrupted are each an object {machine, kind, id, ref, label, status}:
kindis what the machine calls an instance (meta.instance.kind).refisnulluntil it is set.labelis the ref, or the ID when there is no ref.statusisactive,interrupted,paused, orcompleted.
A later release can add fields. Before version 1.0, a field is renamed or removed only in a minor release.
The sample in this section comes from a project with this config and this machine:
.smllm/config.toml
[machines]
files = ["dev.smllm.yaml"]
.smllm/dev.smllm.yaml
id: dev
description: Work on an issue, then review the change.
initial: WORK
meta:
smllm: 1
instance:
kind: issue
ref: { param: issueId, pattern: "^GH-[0-9]+$", description: "The issue's id, such as GH-123." }
events:
done:
description: Fire after the change is made.
changes:
description: Fire when the review asks for changes.
approved:
description: Fire when the review approves the change.
states:
WORK:
meta: { entryPoint: true }
entry: { type: prompt, params: { text: "Make the change, then fire done." } }
on:
done: REVIEW
REVIEW:
entry: { type: prompt, params: { text: "Review the change, then fire approved or changes." } }
on:
changes: WORK
approved: DONE
DONE:
type: final
The session paused the issues GH-121 and GH-122. Then it entered GH-123, and the review
sent the issue back to WORK twice, so the session is at its third visit to WORK, and
smllm statusline --json prints this object for it:
{
"session": "sm-nqvfvw",
"idle": false,
"machine": "dev",
"state": "WORK",
"visit": 3,
"yielded": false,
"instance": { "machine": "dev", "kind": "issue", "id": "i-n7d6tx", "ref": "GH-123", "label": "GH-123", "status": "active" },
"interrupted": null,
"paused": 2
}
sm-nqvfvw is the key of the session, and i-n7d6tx is the instance’s ID. Your key and your
instance’s ID differ.
Other harnesses
The command needs only a session key. smllm statusline --session KEY works from any status bar,
prompt, or tmux line that knows the key. Without --session, the command reads stdin as JSON with
a session_id that the Claude Code hooks have bound. A host that uses @six5536/smllm-wasm gets
the same fields from engine.status(key).
The rules of smllm validate
This page lists the rules that smllm validate checks. Each finding ends with the code of the rule that it breaks, such as (FMT-4), and this page says what each rule checks. smllm info rules prints the same page, so it always matches the smllm that you run.
| Code | Rule | What validate checks |
|---|---|---|
FMT-1 | The machine file | The file is YAML that parses, sets meta.smllm: 1, and has at least one state. Its keys are the known ones, each required key is present, and each value has the expected type. The machine’s ID and its state names have the allowed characters, and no other machine in the config has the same ID or one that differs only in case. The file uses only the part of XState v5 that smllm supports: flat atomic and final states, without nested or parallel states, delays, or context, and with an invoke of only src and input. |
FMT-2 | States and transitions | A final state has no on or always, isn’t an entry point, and can be reached. A machine without a final state gets an info finding, because its instances never complete. At most one state is the fallback, and the fallback isn’t final. A state doesn’t handle a built-in event under on, a list of guarded transitions ends with one without a guard, and neither the ref param nor any other param has the name of a param of enter. An event with the ref param sets the ref. A state with an invoke handles an event of source: host, which carries the work’s result, and each invoke names its src. |
FMT-3 | Actions and guards | Each action is a {type, params} object of a known type, with the params that its type needs. A prompt has exactly one of text and file, a file path is relative to the file that gives it, and setRef appears only where an event carries the ref param. Each guard is a {type, params} object of a known type, with valid params. A visits guard counts a state that exists, and its atLeast is more than 0. |
FMT-4 | Event and instance declarations | The ref param’s name, and each declared event and param name, contain only letters, digits, _, and -. An event’s params are an object of properties, each required param is in properties, and each enum and pattern is valid and allows at least one value. A state rewords only the events that it has a transition on, and only params that the event declares in meta.events. Each declared event is used by a state, and each ref pattern can match a ref. An event’s source is agent or host, and a machine with an event of source: host gets an info finding, because only a host can fire the event, so the machine needs a host to drive it. |
FMT-10 | JSON params | A json param takes no enum or pattern, a string param takes no schema, and a param that sets the ref is a string. A json param’s schema, and each file that a relative $ref in it names, is a map or a JSON file that smllm can read, declares draft-07, 2020-12, or no draft, and is valid against its draft. A schema without a draft is 2020-12, or the draft of the schema that refers to it. Each $ref to another file is a path relative to the schema that holds it, because smllm doesn’t fetch a schema from the network. |
FMT-6 | Files that smllm reads | smllm can read each file that the config names. |
FMT-7 | Combining configs | A config with errors doesn’t replace the machines of the configs before it, and validate says which machines it leaves out and which machine replaces which. |
AGT-1 | Author text | Author text, such as a prompt or an event’s description, contains none of the closing tags </smllm>, </instructions>, and </events> that fence smllm’s own text, because the agent couldn’t then tell smllm’s text from the author’s. A finding under this rule is a warning. |
AGT-2 | Events list | A built-in event changes only its description, never its params or its source. |
AGT-5 | Transitions | The initial state and each transition’s target exist, each state can be reached, and a transition that should re-enter its state names it as the target. A state with always has no on events, because smllm leaves it at once, and no chain of always transitions loops back to its start. |
AGT-6 | Guards and actions | meta.sharedActions lists only states that exist. |
MCP-1 | Param types | Each param has type: string, the default, or type: json. |
CLI-3 | Configs and saved instances | smllm can read and parse each config file, and each saved instance is in a state that still exists. |
CLI-11 | Command guards and actions | A command’s run is a non-empty string, run by a shell, or a non-empty list, run without one. A list-form run doesn’t rely on $SMLLM_* variables, because no shell expands them. A command’s timeoutSecs is at least 1. |
STORE-1 | The instance files | smllm can read each instance file and each ref marker of the config’s machines. |
FMT-1: The machine file
A finding of FMT-1 means that the machine file isn’t YAML that smllm reads, or that one of its keys or values isn’t one that smllm knows. The rules page says what the rule checks.
Messages
<file>:<line>: error: <YAML error> (FMT-1)
<file>: error: missing key `<key>` (FMT-1)
<file>:<line>: error: <path>: missing key `<key>` (FMT-1)
<file>:<line>: error: <path>: unknown key `<key>` (expected one of: <keys>) (FMT-1)
<file>:<line>: error: <path>: expected <type>, found <type> (FMT-1)
<file>:<line>: error: <path>: expected <type>, found nothing (FMT-1)
<file>:<line>: error: meta.smllm: unsupported smllm format version <n>. Set meta.smllm: 1, the only version that this smllm reads (FMT-1)
<file>:<line>: error: states: a state machine needs at least one state (FMT-1)
<file>:<line>: error: id: id "<id>" must contain only letters, digits, _, and - (FMT-1)
<file>:<line>: error: states.<state>: state name "<state>" must not be empty or contain spaces (FMT-1)
<file>: error: two machines in this config have id <id> (FMT-1)
<file>: error: machine id <id> differs from <id> only in case, so they would share a state directory on macOS and Windows (FMT-1)
<file>:<line>: error: states.<state>.type: `type: <type>` is not supported. smllm supports only flat atomic and final states (FMT-1)
<file>:<line>: error: <path>: `<key>` is not supported. smllm supports only part of XState: flat atomic and final states, without delays or context, and an invoke of only src and input (FMT-1)
Cause
The first missing key form is a key of the whole file, such as id, and the second is a key of a part of it, such as smllm under meta. A finding of a wrong type ends with found nothing when the key has no value, such as a state written as TRIAGE: alone.
Each message has one of these causes:
- The YAML doesn’t parse.
- A required key is missing, such as
id, orsmllmundermeta. - A key has a name that smllm doesn’t know, or a value of a type that smllm doesn’t expect, or no value.
meta.smllmisn’t 1.statesis empty.- The machine’s ID has a character other than a letter, a digit,
_, and-. - A state’s name is empty or has a space.
- Two machines of a config have the same ID, or IDs that differ only in case.
- A state sets
typetoparallel,history, orcompound, which smllm leaves out. - The file uses a key of XState that smllm leaves out:
statesorinitialinside a state, for a nested state,after,context,output,tags,history,onDone,types, orversion.
Fix
Correct the key or the value that the message names. Set meta.smllm to 1, give the machine at least one state, and give each machine of a config an ID of its own. Keep each state flat: a state that needs steps of its own becomes several states of the machine.
Example
This machine sets a format version that smllm doesn’t read:
.smllm/fix.smllm.yaml
id: fix
initial: TRIAGE
meta:
smllm: 2
states:
TRIAGE:
on:
reproduced: DONE
DONE:
type: final
smllm validate reports the version:
$ smllm validate .smllm/fix.smllm.yaml
.smllm/fix.smllm.yaml:4: error: meta.smllm: unsupported smllm format version 2. Set meta.smllm: 1, the only version that this smllm reads (FMT-1)
1 error, 0 warnings, 0 info
[exit 1]
The fixed machine sets version 1:
.smllm/fix.smllm.yaml
id: fix
initial: TRIAGE
meta:
smllm: 1
states:
TRIAGE:
on:
reproduced: DONE
DONE:
type: final
smllm validate reports no finding:
$ smllm validate .smllm/fix.smllm.yaml
0 errors, 0 warnings, 0 info
FMT-2: States and transitions
A finding of FMT-2 means that a state or a transition breaks a rule of how the machine moves from state to state, or that a param takes a name that smllm keeps for itself. The rules page says what the rule checks.
Messages
<file>:<line>: error: states.<state>.type: `<type>` is not one of: atomic, final (FMT-2)
<file>:<line>: error: states.<state>: a final state cannot have on or always (FMT-2)
<file>:<line>: warning: states.<state>: a final state that is an entry point completes the instance as soon as it is entered. Remove meta.entryPoint, or make the state non-final (FMT-2)
<file>:<line>: warning: states.<state>: state <state> is unreachable. Add a transition to it, or set meta.entryPoint: true (FMT-2)
<file>:<line>: info: states: no final state: instances of this machine never complete. If instances should complete, add a state with type: final (FMT-2)
<file>:<line>: error: states.<state>: the fallback state cannot be final (FMT-2)
<file>:<line>: error: states.<state>: only one state can be the fallback, and <state> already is (FMT-2)
<file>:<line>: error: states.<state>.on.<event>: <event> is a built-in event, so a state cannot handle it under on (FMT-2)
<file>:<line>: error: <path>: the last transition has a guard, so nothing happens when every guard fails. End the list with a transition without a guard, such as one back to this state (FMT-2)
<file>:<line>: error: meta.instance.ref.param: ref param <param> has the name of one of enter's own params. Choose another name, such as issueId (FMT-2)
<file>:<line>: warning: meta.events.<event>.params.properties.<param>: param <param> has the name of one of enter's own params (FMT-2)
<file>:<line>: warning: meta.events.<event>: <event> has a param with the ref param's name <param>, but never sets the ref. Rename the param, or add a setRef action (FMT-2)
<file>:<line>: error: states.<state>.invoke: the state invokes the host's work, but handles no event of source: host to carry its result. Declare the result as an event of source: host in meta.events, and handle it in the state's on (FMT-2)
<file>:<line>: error: states.<state>.invoke: a final state invokes no work, because the instance completes when it enters the state. Remove the invoke, or make the state atomic (FMT-2)
<file>:<line>: error: <path>.src: src is empty. Give src the name of the host's work (FMT-2)
Cause
smllm validate lists a warning only with --warnings, and an info finding only with --info.
Each message has its own cause:
- A state’s
typeis a value other thanatomicandfinal. - A final state has
onoralways. A final state completes the instance, so no transition leaves it. - A final state sets
meta.entryPoint: true, so an agent who enters it completes the instance at once. - No transition leads to a final state, and the state isn’t an entry point.
- The machine has no final state, so its instances never complete. This finding is only information, because a machine can run for ever on purpose.
- A final state sets
meta.fallback: true. - More than one state sets
meta.fallback: true. - A state handles a built-in event, such as
yieldorpause, underon. smllm handles the built-in eventsenter,resume,pause,unmatched,yield, andlistPauseditself. - The last transition of a list has a
guard. When every guard fails, the event matches no transition. meta.instance.ref.paramisstateMachineorstate, the names of the params ofenter.- An event declares a param named
stateMachineorstate. - An event declares a param with the name of the ref param, but no transition on the event has a
setRefaction, so the param never sets the ref. - A state has an
invoke, but handles no event ofsource: hostunderon, so the host can’t fire the result of the work. - A final state has an
invoke. - An
invokehas an emptysrc.
Fix
Make the change that the message names:
- Set
typetoatomicorfinal, or leave it out for an atomic state. - Remove
onandalwaysfrom the final state, or removetype: final. - Remove
meta.entryPointfrom the final state, or make the state atomic. - Add a transition to the final state, or remove the state.
- Add a state with
type: finalif the instances complete. - Move
meta.fallbackto an atomic state. - Keep
meta.fallback: trueon one state only. - Rename the event, or remove it from
on. - End the list with a transition that has no guard, such as one back to the same state.
- Give the ref param another name, such as
issueId. - Rename the param.
- Rename the param, or add a
setRefaction to a transition on the event. - Declare an event with
source: hostfor the work’s result, and handle it in the state’son. - Remove the
invokefrom the final state, or make the state atomic. - Give
srcthe name of the work.
Example
This machine runs the tests before it completes, but the only transition on fixed has a guard:
.smllm/fix.smllm.yaml
id: fix
initial: TRIAGE
meta:
smllm: 1
states:
TRIAGE:
on:
reproduced: FIX
FIX:
on:
fixed:
- target: DONE
guard:
type: command
params:
run: cargo test
DONE:
type: final
smllm validate reports the guard:
$ smllm validate .smllm/fix.smllm.yaml
.smllm/fix.smllm.yaml:11: error: states.FIX.on.fixed: the last transition has a guard, so nothing happens when every guard fails. End the list with a transition without a guard, such as one back to this state (FMT-2)
1 error, 0 warnings, 0 info
[exit 1]
The fixed machine stays in FIX when the tests fail:
.smllm/fix.smllm.yaml
id: fix
initial: TRIAGE
meta:
smllm: 1
states:
TRIAGE:
on:
reproduced: FIX
FIX:
on:
fixed:
- target: DONE
guard:
type: command
params:
run: cargo test
- target: FIX
DONE:
type: final
smllm validate reports no finding:
$ smllm validate .smllm/fix.smllm.yaml
0 errors, 0 warnings, 0 info
FMT-3: Actions and guards
A finding of FMT-3 means that an action or a guard of the machine isn’t one that smllm knows, or that its params are wrong. The rules page says what the rule checks.
Messages
<file>:<line>: error: <path>: unknown action `<name>`. Write an action as {type, params}. Only setRef can be a bare name (FMT-3)
<file>:<line>: error: <path>.type: unknown action type `<type>` (expected one of: prompt, command, setRef) (FMT-3)
<file>:<line>: error: <path>: a prompt needs exactly one of: text, file (FMT-3)
<file>:<line>: error: <path>: prompt file <file>: <error>. The path is relative to the file that gives it (FMT-3)
<file>:<line>: error: <path>: setRef runs only in a transition's actions under `on`. Entry, exit, and always have no event to read the ref param from (FMT-3)
<file>:<line>: error: <path>: a guard is a map {type, params}, not a name. Guard types: command { run }, visits { state, atLeast } (FMT-3)
<file>:<line>: error: <path>.type: unknown guard type `<type>` (expected one of: command, visits) (FMT-3)
<file>:<line>: error: <path>: visits counts unknown state <state> (FMT-3)
<file>:<line>: warning: <path>: visits atLeast 0 is always true (FMT-3)
Cause
smllm validate lists a warning only with --warnings.
Each message has its own cause:
- An action is a bare name other than
setRef. - An action’s
typeis a value other thanprompt,command, andsetRef. - A prompt’s params give both
textandfile, or neither. - smllm can’t read the file that a prompt names. smllm reads the path from the folder of the file that gives it, which is the machine file, or the config for
[idle] on-enter. - A
setRefaction is underentry,exit, oralways. Only a transition underonhas an event that carries the ref param. - A guard is a name, not a map.
- A guard’s
typeis a value other thancommandandvisits. - A
visitsguard counts a state that the machine doesn’t have. - A
visitsguard hasatLeast: 0, which every instance passes.
Fix
Make the change that the message names:
- Write the action as a map with
typeandparams. - Set
typetoprompt,command, orsetRef. - Give the prompt exactly one of
textandfile. - Correct the path, or create the file. The path is relative to the folder of the file that gives it: the machine file, or the config for
[idle] on-enter. - Move
setRefto the actions of a transition underon. - Write the guard as a map with
typeandparams. - Set
typetocommandorvisits. - Name a state of the machine in
state. - Set
atLeastto 1 or more, or remove the guard.
Example
This machine gives a prompt file with a path that starts at the project, not at the machine file:
.smllm/prompts/triage.md
Reproduce the bug, then fire `reproduced`.
.smllm/fix.smllm.yaml
id: fix
initial: TRIAGE
meta:
smllm: 1
states:
TRIAGE:
entry:
type: prompt
params:
file: .smllm/prompts/triage.md
on:
reproduced: DONE
DONE:
type: final
smllm validate reports the prompt file:
$ smllm validate .smllm/fix.smllm.yaml
.smllm/fix.smllm.yaml:7: error: states.TRIAGE.entry: prompt file .smllm/prompts/triage.md: no such file. The path is relative to the file that gives it (FMT-3)
1 error, 0 warnings, 0 info
[exit 1]
The fixed machine gives the path from the folder of the machine file:
.smllm/fix.smllm.yaml
id: fix
initial: TRIAGE
meta:
smllm: 1
states:
TRIAGE:
entry:
type: prompt
params:
file: prompts/triage.md
on:
reproduced: DONE
DONE:
type: final
smllm validate reports no finding:
$ smllm validate .smllm/fix.smllm.yaml
0 errors, 0 warnings, 0 info
FMT-4: Event and instance declarations
A finding of FMT-4 means that a declaration under meta.events or meta.instance, or a state’s rewording of a param, has a bad name, a bad pattern, or a part that nothing uses. The rules page says what the rule checks.
Messages
<file>:<line>: error: meta.instance.ref.param: ref param "<param>" must contain only letters, digits, _, and - (FMT-4)
<file>:<line>: error: <path>: event name "<event>" must contain only letters, digits, _, and - (FMT-4)
<file>:<line>: error: meta.events.<event>.params.properties.<param>: param name "<param>" must contain only letters, digits, _, and - (FMT-4)
<file>:<line>: error: meta.events.<event>.params: params type must be object, not <type> (FMT-4)
<file>:<line>: error: meta.events.<event>.params.required: required param <param> is not in properties (FMT-4)
<file>:<line>: error: meta.events.<event>.params.properties.<param>: enum is empty, so it allows no value (FMT-4)
<file>:<line>: error: <path>.pattern: invalid pattern "<pattern>": <reason> (FMT-4)
<file>:<line>: warning: meta.events.<event>.params.properties.<param>: enum value "<value>" does not match the pattern, so the param can never take it (FMT-4)
<file>:<line>: warning: meta.events.<event>.params.properties.<param>: <event> sets the ref, so meta.instance.ref.pattern <pattern> applies instead of this pattern. Remove this pattern, or change meta.instance.ref.pattern (FMT-4)
<file>:<line>: warning: states.<state>.meta.paramDescriptions.<event>: <state> has no transition on <event>, so smllm never shows these descriptions (FMT-4)
<file>:<line>: error: states.<state>.meta.paramDescriptions.<event>.<param>: <event> declares no param <param>. Declare params in meta.events, because paramDescriptions only rewords their prompts (FMT-4)
<file>:<line>: warning: meta.events.<event>: event <event> is declared but no state uses it (FMT-4)
<file>:<line>: error: meta.events.<event>.source: `<value>` is not one of: agent, host (FMT-4)
<file>:<line>: info: meta.events: only the host fires the events <events>, so the machine needs a host to drive it. A harness can't start an instance of it, and a host that embeds the engine fires these events (FMT-4)
Cause
smllm validate lists a warning only with --warnings, and an info finding only with --info.
Each message has its own cause:
- The ref param’s name has a character other than a letter, a digit,
_, and-. - An event’s name, under
meta.eventsor under a state’son, has such a character. - A param’s name has such a character.
- An event’s
paramshas atypeother thanobject. requirednames a param thatpropertiesdoesn’t declare.- A param’s
enumis an empty list. - A
patternisn’t a valid regular expression. - A value of a param’s
enumdoesn’t match the param’spattern. - An event that sets the ref gives the ref param a
patternof its own, andmeta.instance.ref.patterngives another. The pattern ofmeta.instance.refapplies. - A state’s
meta.paramDescriptionsrewords the params of an event that the state has no transition on. - A state’s
meta.paramDescriptionsrewords a param that the event doesn’t declare. meta.eventsdeclares an event that no state has a transition on.- An event’s
sourceis a value other thanagentandhost. - An event has
source: host. This finding is only information: only a host program can fire the event, so the machine needs a host to drive it.
Fix
Make the change that the message names:
- Rename the ref param with letters, digits,
_, and-. - Rename the event in the same way.
- Rename the param in the same way.
- Set the
typeofparamstoobject. - Correct the name in
required, or declare the param underproperties. - Add a value to
enum, or removeenum. - Correct the pattern.
- Correct the value or the pattern.
- Remove the param’s
pattern, or changemeta.instance.ref.pattern. - Add a transition on the event to the state, or remove the event from
meta.paramDescriptions. - Declare the param under the event in
meta.events, or remove it frommeta.paramDescriptions. - Add a transition on the event to a state, or remove the event.
- Set
sourcetoagentorhost. - Nothing needs to change if a host program drives the machine. If the agent should fire the event, set
sourcetoagent, or removesource.
Example
This machine requires a param issue, but the param under properties is issueId:
.smllm/fix.smllm.yaml
id: fix
initial: TRIAGE
meta:
smllm: 1
events:
reproduced:
params:
type: object
required: [issue]
properties:
issueId:
type: string
states:
TRIAGE:
on:
reproduced: DONE
DONE:
type: final
smllm validate reports the required param:
$ smllm validate .smllm/fix.smllm.yaml
.smllm/fix.smllm.yaml:9: error: meta.events.reproduced.params.required: required param issue is not in properties (FMT-4)
1 error, 0 warnings, 0 info
[exit 1]
The fixed machine requires issueId:
.smllm/fix.smllm.yaml
id: fix
initial: TRIAGE
meta:
smllm: 1
events:
reproduced:
params:
type: object
required: [issueId]
properties:
issueId:
type: string
states:
TRIAGE:
on:
reproduced: DONE
DONE:
type: final
smllm validate reports no finding:
$ smllm validate .smllm/fix.smllm.yaml
0 errors, 0 warnings, 0 info
FMT-10: JSON params
A finding of FMT-10 means that a param with type: json has a constraint that only a string param takes, or that smllm can’t read or use the param’s JSON Schema. The rules page says what the rule checks.
Messages
<file>:<line>: error: <path>.schema: param <param> is a string, so it takes no schema. Set type: json for a param that takes a JSON value with a schema (FMT-10)
<file>:<line>: error: <path>.enum: param <param> takes a JSON value, so it takes no enum. Give the constraint in the param's schema (FMT-10)
<file>:<line>: error: <path>.pattern: param <param> takes a JSON value, so it takes no pattern. Give the constraint in the param's schema (FMT-10)
<file>:<line>: error: <path>: <event> sets the ref, so its param <param> must be a string. Remove type: json, or give the ref another param (FMT-10)
<file>:<line>: error: <path>.schema: the schema file <schema file> does not exist (FMT-10)
<file>:<line>: error: <path>.schema: <schema> can't be read: <error> (FMT-10)
<file>:<line>: error: <path>.schema: <schema> is not JSON: <error> (FMT-10)
<file>:<line>: error: <path>.schema: <schema> declares the draft <uri>, but smllm reads only draft-07 and 2020-12. Set $schema to http://json-schema.org/draft-07/schema# or https://json-schema.org/draft/2020-12/schema (FMT-10)
<file>:<line>: error: <path>.schema: <schema> has a $schema of <value>, but $schema must be a string (FMT-10)
<file>:<line>: error: <path>.schema: <schema> is not a valid <draft> schema: <error> (FMT-10)
<file>:<line>: error: <path>.schema: ref <ref> in <schema> names a file that does not exist (FMT-10)
<file>:<line>: error: <path>.schema: ref <ref> in <schema> resolves to no bundled file. smllm doesn't fetch a schema from the network: write the ref as a path relative to the schema that holds it (FMT-10)
Cause
In each message, <path> is the param, such as meta.events.planned.params.properties.plan. <schema> is schema and the name of a schema file, or the inline schema for a schema written in the machine file.
Each message has its own cause:
- A string param has a
schema. - A JSON param has an
enum. - A JSON param has a
pattern. - An event that sets the ref gives the ref param
type: json. A ref is a string. - The schema file that
schemanames doesn’t exist. smllm reads the path from the folder of the machine file. - smllm can’t read the schema file, for example because the path names a folder.
- The schema file isn’t JSON.
- The schema’s
$schemadeclares a draft other than draft-07 and 2020-12. - The schema’s
$schemaisn’t a string. - The schema isn’t valid against its draft. A schema without a
$schemais 2020-12, or the draft of the schema that refers to it. - A
$refnames a file that doesn’t exist. smllm reads the path from the folder of the schema that holds the$ref. - A
$refis a URL that names no schema file of the param. smllm doesn’t fetch a schema from the network.
Fix
Make the change that the message names:
- Set
type: jsonon the param, or removeschema. - Remove
enum, and give the values in the schema. - Remove
pattern, and give the pattern in the schema. - Remove
type: jsonfrom the ref param, or set the ref from another param. - Correct the path, relative to the folder of the machine file, or create the file.
- Correct the path, so that it names a file that smllm can read.
- Correct the JSON.
- Set
$schematohttp://json-schema.org/draft-07/schema#orhttps://json-schema.org/draft/2020-12/schema, or remove it. - Write
$schemaas a string. - Correct the schema at the place that the message names.
- Correct the path of the
$ref, or create the file. - Save the schema as a file next to the schema that refers to it, and write the
$refas a relative path.
Example
This machine’s schema file declares draft-04:
.smllm/schemas/plan.json
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"required": ["steps"],
"properties": {
"steps": { "type": "array", "items": { "type": "string" } }
}
}
.smllm/plan.smllm.yaml
id: plan
initial: PLAN
meta:
smllm: 1
events:
planned:
params:
type: object
required: [plan]
properties:
plan:
type: json
schema: schemas/plan.json
states:
PLAN:
on:
planned: DONE
DONE:
type: final
smllm validate reports the draft:
$ smllm validate .smllm/plan.smllm.yaml
.smllm/plan.smllm.yaml:13: error: meta.events.planned.params.properties.plan.schema: schema schemas/plan.json declares the draft http://json-schema.org/draft-04/schema#, but smllm reads only draft-07 and 2020-12. Set $schema to http://json-schema.org/draft-07/schema# or https://json-schema.org/draft/2020-12/schema (FMT-10)
1 error, 0 warnings, 0 info
[exit 1]
The fixed schema file declares draft-07:
.smllm/schemas/plan.json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["steps"],
"properties": {
"steps": { "type": "array", "items": { "type": "string" } }
}
}
smllm validate reports no finding:
$ smllm validate .smllm/plan.smllm.yaml
0 errors, 0 warnings, 0 info
FMT-6: Files that smllm reads
A finding of FMT-6 means that smllm can’t read a machine file that the config or the command line names. The rules page says what the rule checks.
Messages
<file>: error: can't read: no such file (FMT-6)
<file>: error: can't read: <reason> (FMT-6)
Cause
The file doesn’t exist, or smllm can’t read it. A path in the config’s files is relative to the folder of the config, so a path that starts at the project names a file that doesn’t exist. smllm can’t read a path that names a folder, or a file that the user isn’t allowed to read.
Fix
Correct the path in the config’s files, or in the command, so that it names the machine file. Write a path in files from the folder of the config. Where the message names another error, correct the path so that it names a file, or let your user read the file.
Example
This config names a machine file that doesn’t exist, because the machine’s file is fix.smllm.yaml:
.smllm/fix.smllm.yaml
id: fix
initial: TRIAGE
meta:
smllm: 1
states:
TRIAGE:
on:
reproduced: DONE
DONE:
type: final
.smllm/config.toml
[machines]
files = ["fixes.smllm.yaml"]
smllm validate reports the file that it can’t read:
$ smllm validate
.smllm/fixes.smllm.yaml: error: can't read: no such file (FMT-6)
1 error, 0 warnings, 0 info
[exit 1]
The fixed config names the machine’s file:
.smllm/config.toml
[machines]
files = ["fix.smllm.yaml"]
smllm validate reports no finding:
$ smllm validate
0 errors, 0 warnings, 0 info
FMT-7: Combining configs
A finding of FMT-7 means that smllm combined the user config, ~/.config/smllm/config.toml, with the project config, and that a machine of the project replaces a machine of the user config, or might. The rules page says what the rule checks.
Messages
<file>: info: machine <id> here replaces the one in <file> (FMT-7)
<file>: warning: smllm does not use machine <id> in <file> while this file, which replaces it, has errors (FMT-7)
<file>: warning: smllm doesn't load the machine <id> from earlier configs while this file has errors, because this file might replace it (FMT-7)
Cause
smllm validate lists a warning only with --warnings, and an info finding only with --info.
smllm reads the user config first, then the project config. When both configs give a machine with the same ID, the project’s machine replaces the user’s. Each message has its own cause:
- A machine of the project config has the ID of a machine of the user config, and replaces it. This finding is only information.
- A machine file of the project config has errors, and its
idis the ID of a machine of the user config. smllm uses neither machine until the file is fixed, because the user’s machine isn’t the one that the project asks for. - The project config itself has errors, so smllm can’t tell which machines it gives. smllm uses none of the user config’s machines until the config is fixed.
Fix
Fix the errors that smllm validate reports in the project’s machine file or config. smllm then uses the project’s machine in place of the user’s. To use the user’s machine in the project, remove the project’s machine from the project config, or give the project’s machine another ID.
Example
smllm prints the full path of a file in the user config, and this page shortens your home folder to ~. The user config gives a machine fix:
~/.config/smllm/config.toml
[machines]
files = ["fix.smllm.yaml"]
~/.config/smllm/fix.smllm.yaml
id: fix
initial: TRIAGE
meta:
smllm: 1
states:
TRIAGE:
on:
reproduced: DONE
DONE:
type: final
The project gives its own machine fix, with a review step, but misspells final:
.smllm/config.toml
[machines]
files = ["fix.smllm.yaml"]
.smllm/fix.smllm.yaml
id: fix
initial: TRIAGE
meta:
smllm: 1
states:
TRIAGE:
on:
reproduced: REVIEW
REVIEW:
on:
approved: DONE
DONE:
type: fnal
smllm validate --warnings reports the error, and that smllm doesn’t use the user’s machine in its place:
$ smllm validate --warnings
.smllm/fix.smllm.yaml: warning: smllm does not use machine fix in ~/.config/smllm/fix.smllm.yaml while this file, which replaces it, has errors (FMT-7)
.smllm/fix.smllm.yaml:13: error: states.DONE.type: `fnal` is not one of: atomic, final (FMT-2)
1 error, 1 warning, 0 info
[exit 1]
The fixed machine spells final:
.smllm/fix.smllm.yaml
id: fix
initial: TRIAGE
meta:
smllm: 1
states:
TRIAGE:
on:
reproduced: REVIEW
REVIEW:
on:
approved: DONE
DONE:
type: final
A run that names the machine file checks the file alone, without the configs, and reports no finding:
$ smllm validate .smllm/fix.smllm.yaml
0 errors, 0 warnings, 0 info
smllm validate --info combines the configs, reports no error, and says that the project’s machine replaces the user’s:
$ smllm validate --info
.smllm/fix.smllm.yaml: info: machine fix here replaces the one in ~/.config/smllm/fix.smllm.yaml (FMT-7)
0 errors, 0 warnings, 1 info
AGT-1: Author text
A finding of AGT-1 means that text of the machine, such as a prompt or an event’s description, contains one of the closing tags </smllm>, </instructions>, and </events>. smllm fences the text that it gives the agent with these tags, so a tag in your text hides from the agent where smllm’s text ends. The finding is a warning. The rules page says what the rule checks.
Messages
<file>:<line>: warning: <path>: text contains `<tag>`, which smllm uses to fence agent text. Reword it so the agent can tell smllm's text from yours (AGT-1)
Cause
A prompt, an event’s description, a transition’s description, or another text that smllm passes to the agent contains </smllm>, </instructions>, or </events>. The usual cause is a prompt that quotes smllm’s own text. smllm validate lists the warning only with --warnings.
Fix
Reword the text without the tag. To refer to smllm’s text, name it in words, such as “the instructions”.
Example
The prompt of TRIAGE quotes the tag that ends the instructions:
.smllm/fix.smllm.yaml
id: fix
initial: TRIAGE
meta:
smllm: 1
states:
TRIAGE:
entry: { type: prompt, params: { text: "Write a test that reproduces the bug. Fire reproduced when you reach </instructions>." } }
on:
reproduced: DONE
DONE:
type: final
smllm validate --warnings reports the tag:
$ smllm validate --warnings .smllm/fix.smllm.yaml
.smllm/fix.smllm.yaml:7: warning: states.TRIAGE.entry: text contains `</instructions>`, which smllm uses to fence agent text. Reword it so the agent can tell smllm's text from yours (AGT-1)
0 errors, 1 warning, 0 info
The fixed prompt says what to do in words:
.smllm/fix.smllm.yaml
id: fix
initial: TRIAGE
meta:
smllm: 1
states:
TRIAGE:
entry: { type: prompt, params: { text: "Write a test that reproduces the bug, then fire reproduced." } }
on:
reproduced: DONE
DONE:
type: final
smllm validate reports no finding:
$ smllm validate --warnings .smllm/fix.smllm.yaml
0 errors, 0 warnings, 0 info
AGT-2: Events list
A finding of AGT-2 means that the machine changes a built-in event, such as yield or pause, in a way that smllm doesn’t allow. A machine can reword the description of a built-in event, but smllm sets its params and who fires it. The rules page says what the rule checks.
Messages
<file>:<line>: error: meta.events.<event>: built-in event <event> can change only its description, not its params (AGT-2)
<file>:<line>: error: meta.events.<event>: built-in event <event> can change only its description, not its source (AGT-2)
Cause
An entry of meta.events has the name of a built-in event, and sets params or source.
Fix
Remove params and source from the entry, and keep its description. Where the agent needs an event with other params, declare an event of your own, with a name of its own.
Example
This machine gives the built-in event yield a param:
.smllm/fix.smllm.yaml
id: fix
initial: TRIAGE
meta:
smllm: 1
events:
yield:
description: Stop and wait for the bug's reporter to answer.
params:
type: object
properties:
question: { type: string, description: What you asked the reporter. }
states:
TRIAGE:
on:
reproduced: DONE
DONE:
type: final
smllm validate reports the params:
$ smllm validate .smllm/fix.smllm.yaml
.smllm/fix.smllm.yaml:6: error: meta.events.yield: built-in event yield can change only its description, not its params (AGT-2)
1 error, 0 warnings, 0 info
[exit 1]
The fixed machine rewords only the description of yield:
.smllm/fix.smllm.yaml
id: fix
initial: TRIAGE
meta:
smllm: 1
events:
yield:
description: Stop and wait for the bug's reporter to answer.
states:
TRIAGE:
on:
reproduced: DONE
DONE:
type: final
smllm validate reports no finding:
$ smllm validate .smllm/fix.smllm.yaml
0 errors, 0 warnings, 0 info
AGT-5: Transitions
A finding of AGT-5 means that a transition of the machine leads nowhere, that a state can’t be reached, or that a state with always can’t settle. The rules page says what the rule checks.
Messages
<file>:<line>: error: initial: initial state <state> does not exist (AGT-5)
<file>:<line>: error: <path>: target state <state> does not exist (AGT-5)
<file>:<line>: error: states.<state>.always: always transitions loop: <states>. smllm leaves a state with always at once, so the loop might never stop. Break it with an event (AGT-5)
<file>:<line>: warning: states.<state>: state <state> is unreachable. Add a transition to it, or set meta.entryPoint: true (AGT-5)
<file>:<line>: warning: <path>: reenter has no effect without a target, because a transition without one never re-enters. To re-enter it, add target: <state> (AGT-5)
<file>:<line>: warning: states.<state>: smllm leaves a state with always at once, so it never offers the state's on events (AGT-5)
Cause
Each message has its own cause, and smllm validate lists the last three, the warnings, only with --warnings:
initialnames a state that the machine doesn’t have.- A transition’s
targetnames a state that the machine doesn’t have. A misspelt or renamed state is the usual cause. - States with
alwaystransitions lead back to the state where they started, so smllm would pass through them forever. - No transition leads to a state, and the state isn’t an entry point, so no instance can ever be in it.
- A transition sets
reenter: truebut has notarget. A transition without a target leaves the instance where it is, and never re-enters the state. - A state has both
alwaysandon. smllm leaves the state at once, so the agent never gets the chance to fire its events.
Fix
Fix each message in the order of the causes:
- Set
initialto the name of a state. - Correct the target to the name of a state, or add the state.
- Give one of the states of the loop an event that the agent fires, in place of its
alwaystransition. - Add a transition to the state, or mark the state with
meta: { entryPoint: true }if the agent enters it directly. Delete a state that nothing uses. - Set
targetto the state itself, so that the transition re-enters it and runs its exit and entry actions again. - Move the events of
onto a state that the agent waits in, or remove thealwaystransitions.
Example
The event reproduced of this machine leads to FIXING, but the state is called FIX:
.smllm/fix.smllm.yaml
id: fix
initial: TRIAGE
meta:
smllm: 1
states:
TRIAGE:
on:
reproduced: FIXING
FIX:
on:
fixed: DONE
DONE:
type: final
smllm validate --warnings reports the missing state. It also warns that nothing leads to FIX, and so to DONE:
$ smllm validate --warnings .smllm/fix.smllm.yaml
.smllm/fix.smllm.yaml:8: error: states.TRIAGE.on.reproduced: target state FIXING does not exist (AGT-5)
.smllm/fix.smllm.yaml:9: warning: states.FIX: state FIX is unreachable. Add a transition to it, or set meta.entryPoint: true (AGT-5)
.smllm/fix.smllm.yaml:12: warning: states.DONE: state DONE is unreachable. Add a transition to it, or set meta.entryPoint: true (FMT-2)
1 error, 2 warnings, 0 info
[exit 1]
The warning for DONE ends with (FMT-2), not (AGT-5), because DONE is a final state. A final state that no instance can reach breaks the rule for final states, so smllm reports it under FMT-2.
The fixed machine leads to FIX:
.smllm/fix.smllm.yaml
id: fix
initial: TRIAGE
meta:
smllm: 1
states:
TRIAGE:
on:
reproduced: FIX
FIX:
on:
fixed: DONE
DONE:
type: final
smllm validate reports no finding:
$ smllm validate --warnings .smllm/fix.smllm.yaml
0 errors, 0 warnings, 0 info
AGT-6: Guards and actions
A finding of AGT-6 means that an entry of meta.sharedActions names a state that the machine doesn’t have, or a position other than before and after. The rules page says what the rule checks.
Messages
<file>:<line>: error: meta.sharedActions[<n>]: sharedActions lists unknown state <state> (AGT-6)
<file>:<line>: error: meta.sharedActions[<n>].position: `<value>` is not one of: before, after (AGT-6)
Cause
Each message has its own cause:
- The
stateslist of a shared action names a state that the machine doesn’t have. A misspelt or renamed state is the usual cause. - The
positionof a shared action is neitherbeforenorafter.
Fix
Fix each message in the order of the causes:
- Correct the name to the name of a state, or remove it from the list.
- Set
positiontobefore, to run the shared actions before the state’s own actions, or toafter, to run them after.
Example
The shared action of this machine still names FIXING, the old name of the state FIX:
.smllm/fix.smllm.yaml
id: fix
initial: TRIAGE
meta:
smllm: 1
sharedActions:
- states: [TRIAGE, FIXING]
position: after
entry: { type: prompt, params: { text: "Record what you try in the bug's notes." } }
states:
TRIAGE:
on:
reproduced: FIX
FIX:
on:
fixed: DONE
DONE:
type: final
smllm validate reports the unknown state:
$ smllm validate .smllm/fix.smllm.yaml
.smllm/fix.smllm.yaml:6: error: meta.sharedActions[0]: sharedActions lists unknown state FIXING (AGT-6)
1 error, 0 warnings, 0 info
[exit 1]
The fixed shared action names FIX:
.smllm/fix.smllm.yaml
id: fix
initial: TRIAGE
meta:
smllm: 1
sharedActions:
- states: [TRIAGE, FIX]
position: after
entry: { type: prompt, params: { text: "Record what you try in the bug's notes." } }
states:
TRIAGE:
on:
reproduced: FIX
FIX:
on:
fixed: DONE
DONE:
type: final
smllm validate reports no finding:
$ smllm validate .smllm/fix.smllm.yaml
0 errors, 0 warnings, 0 info
MCP-1: Param types
A finding of MCP-1 means that a param of an event has a type that smllm doesn’t take. A param is a string, the default, or a JSON value, with type: json. The rules page says what the rule checks.
Messages
<file>:<line>: error: <path>: param <param> has type <type>, but a param is a string or JSON. Use type: string, with enum or pattern to limit its values, or type: json for a JSON value (MCP-1)
Cause
A param of meta.events.<EVENT>.params.properties sets type to a value other than string and json, such as number, integer, or boolean.
Fix
Choose the type by what the param holds:
- For a value that the agent gives as text, such as a number or a name, set
type: string, or removetype. To limit its values, addpatternorenum. - For a structured value, set
type: json, and addschemato check it.
Example
The param bugId of this machine is a number:
.smllm/fix.smllm.yaml
id: fix
initial: TRIAGE
meta:
smllm: 1
events:
reproduced:
description: Fire after a test reproduces the bug.
params:
type: object
properties:
bugId: { type: number, description: The number of the bug. }
required: [bugId]
states:
TRIAGE:
on:
reproduced: DONE
DONE:
type: final
smllm validate reports the type:
$ smllm validate .smllm/fix.smllm.yaml
.smllm/fix.smllm.yaml:11: error: meta.events.reproduced.params.properties.bugId: param bugId has type number, but a param is a string or JSON. Use type: string, with enum or pattern to limit its values, or type: json for a JSON value (MCP-1)
1 error, 0 warnings, 0 info
[exit 1]
The fixed param is a string of digits:
.smllm/fix.smllm.yaml
id: fix
initial: TRIAGE
meta:
smllm: 1
events:
reproduced:
description: Fire after a test reproduces the bug.
params:
type: object
properties:
bugId: { type: string, pattern: "^[0-9]+$", description: The number of the bug. }
required: [bugId]
states:
TRIAGE:
on:
reproduced: DONE
DONE:
type: final
smllm validate reports no finding:
$ smllm validate .smllm/fix.smllm.yaml
0 errors, 0 warnings, 0 info
CLI-3: Configs and saved instances
A finding of CLI-3 means that a saved instance is in a state that its machine no longer has, or that smllm can’t read a config file. The rules page says what the rule checks.
Messages
<file>: warning: saved instance <instance> is in state <state>, which no longer exists. To repair it, fire enter with its ref and a state (CLI-3)
<file>: error: can't read: <reason> (CLI-3)
<file>:<line>: error: <TOML error> (CLI-3)
Cause
Each message has its own cause, and smllm validate lists the first, a warning, only with --warnings:
- smllm saved an instance in a state, and an edit of the machine file then renamed or removed that state. The message names the machine file.
- smllm can’t read a config file, such as a file named with
--configthat doesn’t exist. - A config file isn’t TOML that smllm reads, or it has a key or a value that smllm doesn’t know.
Fix
Fix each message in the order of the causes:
- Enter the instance again with the event
enter, and give its ref and the state to continue in, asstate. The state doesn’t have to be an entry point. Where you renamed the state by mistake, rename it back instead. - Correct the path of the config file, or make the file readable.
- Correct the line that the message names.
Example
This machine has the states TRIAGE, FIX, and DONE:
.smllm/config.toml
[machines]
files = ["fix.smllm.yaml"]
.smllm/fix.smllm.yaml
id: fix
initial: TRIAGE
meta:
smllm: 1
states:
TRIAGE:
on:
reproduced: FIX
FIX:
on:
fixed: DONE
DONE:
type: final
An agent enters the machine and fires reproduced, so the instance is in FIX, then puts the instance aside:
$ smllm fire --session sm-nqvfvw enter --param stateMachine=fix
<smllm>
session sm-nqvfvw · fix › TRIAGE · instance i-5z4x7n
Arrived by: enter (new instance)
Params: stateMachine = "fix"
</smllm>
$ smllm fire --session sm-nqvfvw reproduced
<smllm>
session sm-nqvfvw · fix › FIX · instance i-5z4x7n
Arrived by: reproduced from TRIAGE
</smllm>
$ smllm fire --session sm-nqvfvw pause
<smllm>
session sm-nqvfvw · idle
Paused instance i-5z4x7n at FIX.
State machines:
- fix
instance id param ref
starts at: TRIAGE
Paused:
- instance i-5z4x7n (fix) at FIX
Fire one event: smllm({ session: "sm-nqvfvw", event, params })
<events>
- enter: Start a new instance of a state machine, or continue one.
stateMachine (required, one of: fix): The state machine to enter.
ref (optional): The id param that the list of state machines names for the machine: an existing id or ref continues that instance, and a new ref starts one with it. Omit it to start a new instance.
state (optional): An entry point to enter instead, or any state for an instance whose saved state no longer exists. Omit it to enter a new instance at the initial state, or an existing one at its saved state.
</events>
</smllm>
The machine then renames FIX to REPAIR:
.smllm/fix.smllm.yaml
id: fix
initial: TRIAGE
meta:
smllm: 1
states:
TRIAGE:
on:
reproduced: REPAIR
REPAIR:
on:
fixed: DONE
DONE:
type: final
smllm validate --warnings reports the saved instance:
$ smllm validate --warnings
.smllm/fix.smllm.yaml: warning: saved instance i-5z4x7n is in state FIX, which no longer exists. To repair it, fire enter with its ref and a state (CLI-3)
0 errors, 1 warning, 0 info
Entering the instance with state=REPAIR moves it to the new state:
$ smllm fire --session sm-nqvfvw enter --param stateMachine=fix --param ref=i-5z4x7n --param state=REPAIR
<smllm>
session sm-nqvfvw · fix › REPAIR · instance i-5z4x7n
Arrived by: enter (saved state FIX no longer exists)
Params: stateMachine = "fix", ref = "i-5z4x7n", state = "REPAIR"
</smllm>
smllm validate reports no finding:
$ smllm validate --warnings
0 errors, 0 warnings, 0 info
CLI-11: Command guards and actions
A finding of CLI-11 means that a guard or an action of type: command can’t run as written. The rules page says what the rule checks.
Messages
<file>:<line>: error: <path>.params.run: run must be a string (shell) or a list of strings (no shell) (CLI-11)
<file>:<line>: error: <path>: command run is empty (CLI-11)
<file>:<line>: error: <path>: command run is an empty list (CLI-11)
<file>:<line>: error: <path>: timeoutSecs must be at least 1 (CLI-11)
<file>:<line>: warning: <path>: a list-form run gets no shell, so nothing expands $SMLLM_*. Use the string form, or read the environment in a script (CLI-11)
Cause
Each message has its own cause, and smllm validate lists the last, a warning, only with --warnings:
runis neither a string nor a list of strings, such as a number or a list that holds a number.runis an empty string, or a string of spaces.runis an empty list.timeoutSecsis 0.runis a list, and one of its items contains$SMLLM_. smllm runs a list without a shell, so the program gets the text$SMLLM_REFitself, not the value of the variable.
Fix
Fix each message in the order of the causes:
- Write
runas a string, which a shell runs, or as a list of strings: the program, then its arguments. - Give
runthe command to run. - Give
runat least the program to run. - Set
timeoutSecsto 1 or more, or remove it for the default of 60 seconds. - Write
runas a string, so that the shell expands the variables, or run a script that reads them from its environment. On Windows,cmdwrites a variable as%SMLLM_REF%.
Example
The command of DONE passes the ref of the instance in a list, where no shell expands $SMLLM_REF:
.smllm/fix.smllm.yaml
id: fix
initial: TRIAGE
meta:
smllm: 1
states:
TRIAGE:
on:
reproduced: DONE
DONE:
type: final
entry: { type: command, params: { run: [git, commit, -am, "Fix $SMLLM_REF"] } }
smllm validate --warnings reports the list:
$ smllm validate --warnings .smllm/fix.smllm.yaml
.smllm/fix.smllm.yaml:11: warning: states.DONE.entry: a list-form run gets no shell, so nothing expands $SMLLM_*. Use the string form, or read the environment in a script (CLI-11)
0 errors, 1 warning, 0 info
The fixed command is a string, which a shell runs:
.smllm/fix.smllm.yaml
id: fix
initial: TRIAGE
meta:
smllm: 1
states:
TRIAGE:
on:
reproduced: DONE
DONE:
type: final
entry: { type: command, params: { run: 'git commit -am "Fix $SMLLM_REF"' } }
smllm validate reports no finding:
$ smllm validate --warnings .smllm/fix.smllm.yaml
0 errors, 0 warnings, 0 info
STORE-1: The instance files
A finding of STORE-1 means that smllm can’t read a file in which it saves the instances of a machine. smllm skips such a file everywhere else, so smllm validate is where you learn of it. The findings are warnings. The rules page says what the rule checks.
Messages
<file>: warning: can't read the instance file of <machine>: <reason>. smllm skips it, so fix or delete the file (STORE-1)
<file>: warning: can't read the ref marker of <machine>: <reason>. smllm finds these refs more slowly without the marker, and rewrites the marker when it next sets one of them. Don't delete the marker, because smllm reads a missing marker as no such ref (STORE-1)
Cause
smllm keeps each instance of a machine in a JSON file in the machine’s state folder, which is .smllm/state/<MACHINE> by default. The ref markers, in the folder’s refs folder, map the ref of each instance to its ID. smllm validate lists the warnings only with --warnings. Each message has its own cause:
- An instance file isn’t JSON, or isn’t an instance that smllm reads. An edit by hand that went wrong is the usual cause, or a file from a version of smllm that saves instances in another form.
- A ref marker isn’t JSON, or smllm can’t read it.
Fix
Fix each message in the order of the causes:
- Correct the file, or delete it. Deleting it deletes the instance.
- Correct the marker, but don’t delete it. smllm rewrites the marker the next time it sets a ref that the marker holds.
Example
An edit by hand cut this paused instance of the machine fix short:
.smllm/config.toml
[machines]
files = ["fix.smllm.yaml"]
.smllm/fix.smllm.yaml
id: fix
initial: TRIAGE
meta:
smllm: 1
states:
TRIAGE:
on:
reproduced: FIX
FIX:
on:
fixed: DONE
DONE:
type: final
.smllm/state/fix/paused/i-5z4x7n.json
{
"id": "i-5z4x7n",
"machine": "fix",
"ref": null,
"state": "FIX",
"status": "paused",
"holder": null,
"version": 3,
"visits": {
"FIX": 1,
"TRIAGE": 1
},
smllm validate --warnings reports the file:
$ smllm validate --warnings
.smllm/state/fix/paused/i-5z4x7n.json: warning: can't read the instance file of fix: it isn't valid JSON: EOF while parsing a value at line 13 column 0. smllm skips it, so fix or delete the file (STORE-1)
0 errors, 1 warning, 0 info
The fixed file is whole again:
.smllm/state/fix/paused/i-5z4x7n.json
{
"id": "i-5z4x7n",
"machine": "fix",
"ref": null,
"state": "FIX",
"status": "paused",
"holder": null,
"version": 3,
"visits": {
"FIX": 1,
"TRIAGE": 1
},
"resumeState": null,
"created": 1793928806977,
"updated": 1793928816175
}
smllm validate reports no finding:
$ smllm validate --warnings
0 errors, 0 warnings, 0 info
Why a state machine
An agent that follows a long prompt can skip a step, and nothing tells it, or you, that it did. smllm gives the agent one step at a time from a state machine, and when the agent tries to stop before it has fired one of the state’s events, smllm shows it those events instead.
- One step at a time
- A stop that asks for a decision
- Checks that the agent doesn’t run
- A machine that you can check
One step at a time
A prompt gives the agent the whole workflow at once, and the agent decides which part applies. A state machine gives the agent only the instructions of its current state. When the agent fires an event, smllm moves it to the next state and shows it that state’s instructions, so the agent never holds a step that it isn’t on.
Each state lists the events that it accepts, and smllm refuses any other. An agent in TRIAGE can’t fire fixed, because TRIAGE offers only reproduced, so the agent can’t skip from the report to the end.
A stop that asks for a decision
An agent ends its turn when it decides that the work is done. In a state that isn’t final, the harness’s stop hook asks smllm, and smllm refuses the first stop and shows the agent the events of its state: an event of the machine, yield to wait for you, or pause to put the work aside. A stop then becomes a decision that the session records, not a step that the agent forgot. If the agent tries to stop again without firing an event, smllm lets it stop, so that an agent that can’t go on doesn’t loop, and the session’s audit shows the stop.
Checks that the agent doesn’t run
A transition can have a guard. A command guard runs a command, such as the project’s tests, and the transition happens only when the command succeeds. Otherwise smllm takes the next transition of the event, which can keep the agent in its state. The agent can say that the tests pass, but the machine moves on only when they do.
A machine that you can check
A machine is a file in the project, reviewed like the code. smllm validate checks it before an agent runs it: each target state exists, each state can be reached, and each event that the machine declares is used by a state. A prompt can’t be checked in this way, because its steps are prose.
The machine is a strict subset of an XState v5 config, so the tools of XState can draw it, and smllm graph draws it too.
See also
Sessions and instances
What is the difference between a session and an instance, and why does smllm keep the two apart? A session is one conversation of the agent, and an instance is one piece of work, such as an issue, that moves through a state machine. smllm keeps them apart so that the work outlasts the conversation: a later conversation continues the issue in the state where the last one left it.
- A session is a conversation
- An instance is a piece of work
- A ref names the work
- Work that outlasts a session
- Work put aside
- Several sessions
A session is a conversation
When a conversation of the harness starts, the session start hook gives it an smllm session with a key, such as sm-nqvfvw. smllm records which conversation of the harness the key belongs to, so every later hook of the same conversation finds the same session. A conversation that the harness resumes or compacts keeps its session, and the hook shows the agent where the session is.
Pi, OpenCode, and Kilo Code take text only with a prompt, so their agent gets the session with the first prompt. GitHub Copilot and the agents integration give the agent no key: the agent fires enter without one, and smllm then makes the session.
The key appears in the header of every block, and the agent passes it on every call of the smllm tool. Only the main agent calls the tool, and it never passes the key to a subagent, because the session belongs to the main agent’s conversation.
A session records its harness, its working folder, its configs, the instance that it holds, and whether the agent yielded. It holds at most one instance at a time. A session that holds no instance is in idle, where the agent sees the machines that it can enter. smllm keeps the sessions in your user’s state folder, with an audit of every call that drove each session. On Linux and macOS the folder is ~/.local/state/smllm/ unless you set $XDG_STATE_HOME, and on Windows it is %APPDATA%\smllm\.
An instance is a piece of work
An instance is one run of a state machine over one piece of work. enter starts it, with a generated ID such as i-k0xzjt, in the machine’s initial state or in an entry point. Each event moves it, and a final state completes it.
An instance has a status: active while a session holds it, paused or interrupted after the agent puts it aside, and completed after it reaches a final state. It counts the visits to each state, and its history records each event, with the session that fired it, the states before and after, the params, and the results of the guards.
smllm keeps the instances in a state/ folder beside the config of their machine, which is .smllm/state/ for the project’s config, and git ignores the folder. The instances belong to the config, and the sessions belong to you. smllm instance list lists the instances, and smllm instance show shows one with its history.
A ref names the work
A ref is the work’s own name outside smllm, such as the issue GH-12. The machine names the param that carries the ref in meta.instance.ref, and it can give the ref a pattern. The ref is set once, by an enter that names a new ref, or by a setRef action of a transition. No two instances of a machine have the same ref.
After the ref is set, smllm shows the instance by its ref instead of its ID: in the header, in the idle list, and in the status line. The agent and you know the issue by its number, so enter with the number finds the instance in any session, and smllm instance show finds it by the number too.
Work that outlasts a session
A conversation ends, but its instance stays in its state, on disk. A new conversation gets a new session, which starts in idle. The agent continues the work by firing enter with the ref or the ID, and smllm returns the instance to its saved state, with the state’s instructions.
The idle list names the paused instances, so the agent sees the work that it paused in each new session. An instance that another session still holds isn’t in that list, and the agent continues it with its ref. A completed instance stays on disk too, and enter with an entry point as its state reopens it, with its visits and its history.
Work put aside
The agent puts an instance aside in two ways:
pausesets the instance aside to continue later, and returns the session to idle. The idle list shows the 10 most recently updated paused instances, andlistPausedlists them all.enterwith the ref continues one.unmatchedhandles a request that fits none of the state’s events. When the machine has a fallback state, the instance moves there and stays active, andresumereturns it to the state that it left. Otherwise the instance becomes interrupted, the session goes to idle, andresumefrom idle returns to it.
A session has one interrupted instance at most. When unmatched interrupts a second instance, the first one becomes paused, so that no work is lost.
Several sessions
You can run several conversations at once, in one project or in several. Each has its own session and its own key, and each holds its own instance.
Two sessions never hold the same instance. When the agent of one session fires enter for an instance that another session holds, the instance moves to the new session, and the new session’s block notes Took over from session KEY. The next event or stop of the old session returns it to idle, with the error that the instance moved to the new session.
Each write of an instance carries a version, and smllm refuses a write that another session made stale. When two sessions change one instance at the same moment, the second change is refused, and its session returns to idle. Neither session overwrites the other’s work.
smllm session list lists the sessions, with the instance that each holds.
See also
What happens when the agent stops
What does smllm do when the agent tries to end its turn? In a state that isn’t final, smllm blocks the stop and shows the agent the events of its state, so that the agent fires one that says what happens to the work: it goes on, waits for you, or puts the work aside. smllm lets the stop through in idle, after yield, and when the agent tries to stop a second time in a row without firing an event.
- The stop hook
- A blocked stop asks for a decision
- Yield and pause
- Idle stops freely
- A second stop goes through
- Harnesses that can’t block a stop
The stop hook
The harness runs its stop hook each time the agent tries to end its turn. The hook asks smllm about the agent’s session, and smllm answers in one of two ways. It lets the stop through, and the turn ends. Or it blocks the stop with a <smllm> block, and the harness gives the block to the agent and keeps the agent going.
The block of a blocked stop is the header and the events list of the state, without the state’s instructions. The agent’s text shows one.
A blocked stop asks for a decision
An agent ends its turn when its model stops calling tools. That point often comes too early: the agent leaves out a step, or it stops to report what it did before the work is finished. smllm blocks the stop because the machine’s state, not the agent’s text, records whether the work is finished.
The block names the events that the agent can fire. They are the state’s own events, which move the work on, and the built-in events yield, pause, and unmatched. Each one is a decision about the work, and smllm records each in the instance’s history and in the session’s audit. A stop is then a choice that you can read later, and not a step that the agent skipped without a trace.
Yield and pause
Two of the built-in events let the agent stop:
yieldkeeps the instance in its state, and lets the agent stop until your next prompt. The agent fires it to ask you a question, or to wait for something that only you can do. Its optionalnotesays what the agent waits for. The status line showsyielded, so that you see that the agent waits for you. Your next prompt, or the next transition, ends the yield, and smllm then blocks a stop again.pauseputs the instance aside and returns the session to idle, where the agent stops freely. The agent fires it when the work has to wait longer, for example for a review. Any later session continues the work withenter, as Sessions and instances explains.
unmatched also returns the session to idle when the machine has no fallback state, so that the agent can handle a request that fits none of the events.
Idle stops freely
A session in idle holds no instance, so it has no work to finish, and smllm lets every stop through. A session also reaches idle after its instance enters a final state. The final state’s instructions, such as a request for a summary, are the last text that the agent gets for the work, and the agent then stops when it likes.
smllm also lets the agent stop when the machine of the session’s instance is no longer in the config, for example while its file has an error. The instance keeps its saved state, and the agent goes on after the file is fixed.
A second stop goes through
When the agent tries to stop again straight after a blocked stop, without firing an event, smllm lets the stop through. The harness tells the hook that the agent is going on only because of the earlier block, and the session records that no event was fired since. An agent that can’t go on, for example because it is stuck, would otherwise loop between the stop and the block without end.
smllm records this stop as runaway in the session’s audit, which smllm session show KEY --audit prints, and writes the events list to standard error. The instance stays in its state. Your next prompt clears the record of the block, so that the next stop is blocked again.
Harnesses that can’t block a stop
Some harnesses can’t block a stop, as Harnesses lists, and each lets every stop through for its own reason:
- GitHub Copilot runs the hooks, but its session start hook can’t give the agent a session, so smllm binds none. The stop hook finds no session, and allows the stop without a word.
- The
agentsintegration installs no hooks, so no hook runs at a stop. - In
opencode run, smllm blocks the stop as it does in OpenCode’s TUI, but the run ends with the agent’s turn, so the agent never reads the events list.
The machine still holds the agent to its states and its events. The agent learns to fire yield before it stops only from the instructions block that smllm harness install writes.
See also
Agent, host, and human steps
Who moves a state machine from state to state? The agent fires most events, but not every step of the work is the agent’s to take. A machine can run commands that the agent can’t skip, leave an event to the program that hosts smllm, and wait for a person through yield.
- Steps that the agent takes
- Steps that smllm runs
- Steps that the host takes
- Steps that a person takes
- Machines that mix them
Steps that the agent takes
An event belongs to the agent unless the machine says otherwise: its source in meta.events is agent, the default. The agent fires it with the smllm tool, or with smllm fire in a harness without the tool. smllm accepts the event only when the state offers it and its params are valid, and the session’s audit records the call with the source agent.
You can fire the same events yourself with smllm fire from a shell, as Your first state machine does. smllm treats your call as the agent’s, because smllm fire makes the same call as the smllm tool.
Steps that smllm runs
A guard or an action of the type command runs a command, such as the project’s tests. smllm runs it as part of the transition, so the agent can’t skip it, and the agent can’t change its result.
- A
commandguard chooses the transition. When the agent fires the event, smllm takes the first transition whose guard passes, and a guard passes when its command exits with0. The agent can say that the tests pass, but the machine moves on only when they do. The block that the agent gets shows each guard’s result. - A
commandaction runs when the instance enters or leaves a state, or takes a transition. An action never stops a transition. A failed action appears in the block asAction failed, and smllm skips the commands after it in the same list. - A state with
alwaystransitions is left as soon as the instance enters it, by the first transition whose guard passes. A check can then run with no event at all.
Each command gets the session, the machine, the state, the event, the instance, its ref, and the event’s params in its environment, in variables such as SMLLM_REF and SMLLM_PARAM_SIZE. Rule CLI-11 says what smllm validate checks in a command.
Steps that the host takes
A host is a program that embeds smllm’s engine, such as a web application that runs @six5536/smllm-wasm. A machine marks an event with source: host when only that program can decide it, for example from the result of its own checks. The host fires the event with fireAsHost.
The agent never sees a host event: the events list leaves it out, and smllm refuses it when the agent fires it. Under a harness such as Claude Code, no program fires host events, so an instance of the machine would wait for one that never comes. smllm therefore leaves such a machine out of the idle list under a harness, refuses to start an instance of it there, and smllm validate gives the machine an info finding under rule FMT-4.
A state can also name the host’s work with XState’s invoke, such as invoke: { src: runChecks, input: { strict: true } }. smllm runs nothing: the host’s view of the state lists each invoke, and the host runs the work that src names and fires its result as a host event. A host that runs several machines then finds its code by the name of the work, not by the names of the states. A state with an invoke must handle an event of source: host, which rule FMT-2 checks, so that the work’s result always has a way back into the machine.
Steps that a person takes
A person doesn’t fire an event through the tool. Instead, the agent waits for the person with yield, and the person answers with their next prompt. The agent then fires the event that the answer calls for.
A machine models a decision of a person, such as an approval, as a state of its own. The state’s instructions ask the agent to show the work, fire yield, and fire approved after the person approves. The decision is the person’s, and the event is the agent’s. The status line shows yielded while the agent waits, so that you see that the next step is yours. What happens when the agent stops explains how yield lets the agent stop.
Machines that mix them
One machine can give each step to the one who should take it. In this machine, the agent plans, a person approves the plan, the tests decide whether the change is done, and the host reviews it:
.smllm/config.toml
[machines]
files = ["change.smllm.yaml"]
.smllm/change.smllm.yaml
id: change
description: Make a change that the user approves and the host checks.
initial: PLAN
meta:
smllm: 1
events:
planned: { description: Fire after you write the plan. }
approved: { description: Fire after the user approves the plan. }
done: { description: Fire after you make the change. }
checked: { description: "The host fires this after the change passes its review.", source: host }
states:
PLAN:
entry: { type: prompt, params: { text: "Write a plan for the change, then fire planned." } }
on:
planned: APPROVE
APPROVE:
entry: { type: prompt, params: { text: "Show the plan to the user, then fire yield and wait. Fire approved after the user approves the plan." } }
on:
approved: WORK
WORK:
entry: { type: prompt, params: { text: "Make the change, then fire done." } }
on:
done:
- { target: REVIEW, guard: { type: command, params: { run: "cargo test" } } }
- { target: WORK, reenter: true }
REVIEW:
entry: { type: prompt, params: { text: "Wait while the host reviews the change." } }
on:
checked: DONE
DONE:
type: final
smllm validate says that the machine needs a host:
$ smllm validate --info
.smllm/change.smllm.yaml:6: info: meta.events: only the host fires the events checked, so the machine needs a host to drive it. A harness can't start an instance of it, and a host that embeds the engine fires these events (FMT-4)
0 errors, 0 warnings, 1 info
Under a harness, and in smllm fire, smllm refuses to start it:
$ smllm fire --session sm-nqvfvw enter --param stateMachine=change
<smllm>
session sm-nqvfvw · idle
error: state machine change needs a host to drive it: only the host fires its events checked
No state machines are configured.
Fire one event: smllm({ session: "sm-nqvfvw", event, params })
<events>
- enter: Start a new instance of a state machine, or continue one.
stateMachine (required): The state machine to enter.
state (optional): An entry point to enter instead, or any state for an instance whose saved state no longer exists. Omit it to enter a new instance at the initial state, or an existing one at its saved state.
</events>
</smllm>
[exit 1]
Without REVIEW and its host event, with done leading straight to DONE, the same machine runs under any harness, and the agent, the tests, and the person share the work.