Documentation Index
Fetch the complete documentation index at: https://srk-e37e8aa3.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
Adapter guide
An SCP adapter is three files. Nothing else is required.
adapters/your-body/
embodiment.json describe the body
muscle.js physics + sensors + tools
system-prompt.md tell the brain what entities mean
embodiment.json
A static manifest the brain reads once. Plain JSON, no schema validation needed.
{
"name": "cartpole",
"version": "1.0.0",
"actuators": [
{ "name": "force", "type": "continuous", "range": [-15, 15], "unit": "newton" }
],
"sensors": [
{ "name": "pole_angle", "type": "scalar", "range": [-1.5, 1.5], "unit": "rad" },
{ "name": "cart_pos", "type": "scalar", "range": [-2.5, 2.5], "unit": "meter" }
],
"constraints": [
"pole must stay within +/- 0.7 rad",
"cart must stay within +/- 2 m"
]
}
Keep it short. The brain prompt only needs enough to reason about constraints.
muscle.js
A subclass of SCPBody with one async method per tool. Tools are declared in static tools.
const { SCPBody, PatternStore } = require("scp-protocol")
class CartpoleBody extends SCPBody {
static bodyName = "cartpole"
static tools = {
apply_force: {
description: "push the cart to balance the pole",
parameters: {
direction: { type: "string", enum: ["left", "right"], required: true },
magnitude: { type: "number", min: 0, max: 1, required: true },
},
},
reset: { description: "recenter the cart and pick a small tilt", parameters: {} },
hold: { description: "apply no force this frame", parameters: {} },
}
constructor(opts = {}) {
super({
...opts,
patternStore: new PatternStore({
featureExtractor: (e) => ({
tilt_bucket: Math.abs(e.pole_angle) > 0.4 ? "tilted" : "upright",
drift: e.cart_pos > 0.5 ? "right" : e.cart_pos < -0.5 ? "left" : "center",
}),
}),
})
this.physics = new CartpolePhysics()
}
// -- Tools (one async method per static tool name) --
async apply_force({ direction, magnitude }) {
this.physics.apply(direction, magnitude)
return { applied: direction, magnitude }
}
async reset() { this.physics.reset(); return { ok: true } }
async hold() { this.physics.force = 0; return { ok: true } }
// -- Sensor loop (called by Plexa or by your own runner) --
async tick() {
await super.tick()
this.physics.step()
this.setState({
pole_angle: this.physics.theta,
cart_pos: this.physics.x,
})
// Reflex: emergency damping if the pole is past the danger threshold.
if (Math.abs(this.physics.theta) > 0.5) {
this.physics.apply(this.physics.theta > 0 ? "left" : "right", 0.6)
this.emit("pole_critical", { angle: this.physics.theta }, "CRITICAL")
}
}
// -- Outcome reporting (optional but recommended) --
evaluateOutcome(state) {
if (Math.abs(state.pole_angle) > 0.6) return false
if (Math.abs(state.pole_angle) < 0.1) return true
return null // unknown, skip
}
}
module.exports = { CartpoleBody }
A few rules:
- Static
tools is the brain’s contract. The names must match the async method names exactly.
tick() runs every frame. Keep it cheap.
setState(patch) updates the data the aggregator sends to the brain.
emit(type, payload, priority) queues an event. Priorities are CRITICAL, HIGH, NORMAL, LOW.
evaluateOutcome(state) is optional. Return true, false, or null (skip). When you return a boolean, the body auto-reports to the pattern store and adaptive memory.
system-prompt.md
Plain prose telling the brain what your tools mean and when to use which.
You control a cart-pole. Your job is to keep the pole upright and the cart near the center.
Tools:
- apply_force(direction, magnitude): push the cart left or right with a force in [0, 1].
- reset(): only if the pole has fallen over and you want to start over.
- hold(): apply no force this frame.
Rules:
- Push opposite to the tilt direction.
- Magnitude scales with how far off-balance the pole is.
- If the pole_critical event has fired in the last second, the body has already self-corrected; prefer hold().
The brain sees this as the system prompt, plus the live world state from the aggregator.
Common mistakes
Forgetting to declare a tool in static tools. A method named apply_force will not be callable unless static tools.apply_force is also declared. The brain only sees what is in static tools.
Heavy work inside tick(). A 60Hz loop has a 16 ms budget. Move physics to a worker if it gets close.
Not calling super.tick(). Stats and counters live in the base class.
Treating the pattern store as global. Each body owns its own. Cross-body memory belongs in Plexa’s VerticalMemory.
Returning structured objects from a tool that the brain has to parse. Tools return values for your own logging. The brain only sees the world state on the next tick.