Page
Skill System
Skills are deterministic orchestration state machines. They do NOT own locks, commit tools, or mutate the workspace directly – they only propose. The arbiter validates and enforces all constraints.
SkillSpec Schema
{
"name": "build_feature",
"description": "Implement a feature end-to-end.",
"initial_state": "understand",
"input_schema": "<JSONSchema>",
"output_schema": "<JSONSchema>",
"states": {
"understand": {
"objective": "Clarify and restate the requirement.",
"allowed_tools": ["choir.memory.query"],
"transitions": [
{ "on": "complete", "to": "plan" }
]
},
"plan": {
"objective": "Produce an implementation plan.",
"allowed_tools": ["choir.memory.query"],
"transitions": [
{ "on": "complete", "to": "modify" },
{ "on": "revise", "to": "understand" }
]
},
"modify": {
"objective": "Apply changes to files.",
"allowed_tools": ["choir.fs.read", "choir.fs.write"],
"transitions": [
{ "on": "complete", "to": "validate" }
]
},
"validate": {
"objective": "Verify correctness.",
"allowed_tools": ["choir.fs.read"],
"transitions": [
{ "on": "complete", "to": "done" },
{ "on": "fail", "to": "modify" }
]
},
"done": { "terminal": true }
},
"max_steps": 20,
"interruptible": true
}
Schema Fields
| Field | Type | Description |
|---|---|---|
name |
string | Unique skill identifier |
description |
string | Human-readable purpose (included in LLM system prompt summary) |
initial_state |
string | Name of the starting state |
input_schema |
JSONSchema | Schema for skill input parameters |
output_schema |
JSONSchema | Schema for skill output |
states |
object | Map of state name to state definition |
max_steps |
integer | Maximum number of LLM steps before forced termination |
interruptible |
boolean | Whether the skill can be cancelled mid-execution |
State Fields
| Field | Type | Description |
|---|---|---|
objective |
string | What the LLM should accomplish in this state |
allowed_tools |
string[] | Tools the LLM may call in this state |
transitions |
object[] | Valid exit transitions from this state |
terminal |
boolean | If true, this is an end state (no transitions) |
Transition Fields
| Field | Type | Description |
|---|---|---|
on |
string | Event name that triggers the transition |
to |
string | Target state name |
State Machine Execution
Each LLM call during a skill receives:
- Global context + compacted memory
- Current skill step objective
- Step-specific context
The LLM does NOT see the whole skill definition or all steps – just the current phase.
The model proposes one of:
- A tool call (must be in
allowed_toolsfor the current state) - A transition event (must match a valid transition from the current state)
- Finish (only valid in a terminal state)
The arbiter validates the proposal:
- Check that the tool is in
allowed_tools(if tool call) - Check that the transition is valid for the current state (if transition)
- Apply state change and log event
Key Constraints
- One active skill per lane at a time.
- Skills must not spawn other skills recursively.
- Step context must be structured (not free text accumulation).
- LLM cannot jump to arbitrary states, call forbidden tools, or skip transitions.
Per-State Tool Allowlists
Each state defines exactly which tools the LLM may call. This is a hard constraint, not a suggestion:
"understand": {
"allowed_tools": ["choir.memory.query"],
...
}
In the understand state, the LLM can only call choir.memory.query. Calls to any other tool are rejected by the arbiter.
This provides defense-in-depth: even if the LLM attempts to write files during a “planning” phase, the state machine prevents it.
Constraint Violation Handling
When the LLM proposes an invalid action:
- The arbiter rejects the proposal.
- A structured error is returned to the LLM with:
- The list of allowed tools for the current state
- The list of valid transitions from the current state
- The LLM retries with corrected output.
- A per-step retry budget (default: 2 retries) prevents infinite correction loops.
- Exceeding the retry budget triggers a skill-level error transition.
Built-In Skills
tool-builder
Guides the agent through proposing a new tool:
understand -> design -> validate -> propose -> done
^ | |
| v v
+-- revise --+ fail -> design
| State | Objective | Allowed Tools |
|---|---|---|
understand |
Clarify what the tool should do, inputs/outputs, side effects | choir.memory.query, choir.fs.read |
design |
Draft the tool manifest (LLM + runtime views) | choir.memory.query |
validate |
Review manifest for correctness, check conflicts | choir.memory.query |
propose |
Submit via choir.propose.tool for human approval |
choir.propose.tool |
done |
Terminal state | – |
skill-builder
Guides the agent through proposing a new skill:
understand -> design -> validate -> propose -> done
^ | |
| v v
+-- revise --+ fail -> design
| State | Objective | Allowed Tools |
|---|---|---|
understand |
Clarify the workflow, phases, expected outcomes | choir.memory.query, choir.fs.read |
design |
Draft the SkillSpec (states, transitions, tools, schemas) | choir.memory.query |
validate |
Verify state machine is well-formed (valid transitions, terminal states, registered tools, no unreachable states) | choir.memory.query |
propose |
Submit via choir.propose.skill for human approval |
choir.propose.skill |
done |
Terminal state | – |
Both skills produce proposals that go through the approval pipeline. The human reviews and approves (triggers image rebuild) or rejects. The agent never directly installs tools or skills.
Hierarchical State Machine Composition
Choir nests state machines at four levels:
Session SM
+-- Lane SM (edge / core)
+-- Skill SM
+-- LLM step (stochastic proposal)
- Session SM: Manages the overall session lifecycle (start, heartbeat, stop, crash recovery).
- Lane SM: Edge and core lanes each have their own state machines (IDLE, REASONING, WAITING_TOOL, etc.). See Architecture.
- Skill SM: The skill state machine runs within a lane, constraining which tools are available at each phase.
- LLM step: The stochastic part. The LLM proposes actions; the skill SM and lane SM enforce deterministic constraints.
Skill Loading
Skills are loaded at agent startup:
- Scan
/choir/skills/directory. - Parse each
.jsonfile against the SkillSpec schema. - Validate state machines:
- All transitions reference valid states.
- Terminal states exist.
- No unreachable states.
- Validate
allowed_toolsreferences against the tool registry. - Build skill registry.
- If any JSON is invalid or a state machine is ill-formed, fail startup (fail fast).
Like tools, skill registries are immutable at runtime. Changes require a rebuild and redeploy (choirctl agent update).
Creating Custom Skills
- Write a SkillSpec JSON file following the schema above.
- Place it in the global repo (
choir-global/skills/) for shared skills, or the per-agent repo (choir-agent-<id>/skills/) for agent-specific skills. - Agent-specific skills with the same name as global skills override them (resolved at build time).
- Run
choirctl agent build <agent-id>andchoirctl agent update <agent-id>.
Alternatively, the agent can use the skill-builder skill to propose new skills through the approval pipeline.