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.