Package Exports
- @srk0102/plexa
- @srk0102/plexa/bridges/ollama
- @srk0102/plexa/core
- @srk0102/plexa/core/aggregator
- @srk0102/plexa/core/body-adapter
- @srk0102/plexa/core/brain
- @srk0102/plexa/core/space
- @srk0102/plexa/core/translator
- @srk0102/plexa/introspection
Readme
@srk0102/plexa
Orchestration framework for embodied AI. One LLM brain, many bodies, single-process tick loop.
Plexa is built on scp-protocol. Use scp-protocol directly when you have one body. Use Plexa when you have several bodies and want one LLM to coordinate them.
npm install @srk0102/plexaNode >= 18. One production dependency: scp-protocol. Plexa's own core has no other runtime packages; everything else is node:* built-ins.
What it does
- Runs a 60Hz reactor loop that ticks every registered body each frame.
- Aggregates each body's state and events into a prompt that fits a configurable token budget (default 2000), trimming by event priority.
- Calls a brain (OllamaBrain, or a subclass you write) at most every
brainIntervalMs. - Validates the brain's tool intent against the body's declared schema, then dispatches as a direct async method call on the body.
- Exposes an HTTP introspection server on port 4747 for the
plexaCLI (status, bodies, logs).
It is a sequencer and a prompt packer, not a planner or a safety layer. The brain chooses the tool; Plexa only validates and dispatches it.
Install
npm install @srk0102/plexaThis pulls in scp-protocol as its sole dependency. No AWS, no API keys, no external services required to run the examples.
To use a real LLM, install and run Ollama:
ollama pull llama3.2If Ollama is not running, the bundled examples fall back to a stub brain so npm run hello still works end-to-end.
Quick start
const { Space, BodyAdapter, Brain } = require("@srk0102/plexa")
const { OllamaBrain } = require("@srk0102/plexa/bridges/ollama")
class CartpoleBody extends BodyAdapter {
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 },
},
},
hold: { description: "apply no force this frame", parameters: {} },
}
async apply_force({ direction, magnitude }) { /* physics */ }
async hold() { /* no-op */ }
async tick() {
await super.tick()
// sensor read; populate body state visible to the brain prompt
this.setState({ pole_angle: readAngle() })
}
}
const space = new Space("balancer", { tickHz: 60, brainIntervalMs: 1500 })
space.addBody(new CartpoleBody())
space.setBrain(new OllamaBrain({ model: "llama3.2" }))
space.setGoal("balance the pole upright")
await space.run()Tools are methods. Bodies default to in-process; there is no port and no HTTP call between Plexa and a body in the same process. To run a body in another process, mark it static transport = "http" and give it a port.
Run the bundled examples
git clone https://github.com/srk0102/plexa.git
cd plexa
npm install
node examples/hello-world/index.js # one body, one tool loop
node examples/two-bodies/index.js # cartpole + light, one brain
node examples/inprocess-demo/index.js # cartpole physics, full statsEach example prints brain calls, tools dispatched, and final stats. All three run without Ollama by falling back to the stub brain.
How the reactor works
brainIntervalMs
|
body.tick() --> aggregator --> brain --> translator --> body.invokeTool()
body.tick() (state + | (schema (direct
body.tick() events) | check) async call)
^ |
+------------------------------+
tickHz loopSingle thread. setTimeout-based tick. No locks. The brain call is async and non-blocking; body ticks continue while the brain is in flight. If the brain is still running when the next brain window opens, it is skipped.
Managed mode
When a body is added to a Space it is in managed mode. Managed does not mean the body is dumb.
| Mode | Who calls the LLM | Body's pattern store | Body's reflexes |
|---|---|---|---|
| standalone | the body | used | fire |
| managed | Plexa | still used | still fire |
In managed mode the body continues to resolve decisions locally via its own scp-protocol pattern store and fires a body_decision event to Plexa for each one. Plexa observes; it does not override.
Tool intent contract
A brain response must be valid JSON matching:
{
"target_body": "cartpole",
"tool": "apply_force",
"parameters": { "direction": "left", "magnitude": 0.4 },
"priority": 3,
"fallback": "hold"
}The translator rejects intents for seven reasons: unknown body, unknown tool, missing required parameter, wrong type, value out of range, value not in enum, or malformed response. Each rejection is counted in space.getStats().translator.byReason.
Event priority
Bodies emit events with one of four priorities: CRITICAL, HIGH, NORMAL, LOW. The aggregator walks a seven-step reduction cascade when the prompt approaches the token budget: float compaction, string truncation, drop stale body fields, drop LOW events, drop NORMAL events, drop HIGH events, and finally drop bodies. CRITICAL events are preserved at every step.
CLI
Installed globally or via npx:
npx @srk0102/plexa version # version string
npx @srk0102/plexa status # running space, tick, brain stats
npx @srk0102/plexa bodies # connected bodies and tool lists
npx @srk0102/plexa logs # live tail of body events and tool calls
npx @srk0102/plexa start ./space.jsThe CLI reads from an HTTP server on port 4747. To expose it from your app:
const { Space, attachIntrospection } = require("@srk0102/plexa")
const space = new Space("robot")
attachIntrospection(space) // localhost:4747/plexa/{status,bodies,logs,health}
await space.run()No dependencies beyond node:http. No bearer-token auth yet: treat the port as localhost-only.
Tests
npm test92 tests across 14 suites. Built-in node:test, no test framework dependency.
| File | Tests |
|---|---|
| plexa.test.js | 82 |
| managed-mode.test.js | 10 |
The test suite covers Space lifecycle, addBody transport validation, reactor loop, Aggregator priority trimming, Translator rejection reasons, Brain base class, and managed-mode decision handoff. It does not cover: CLI, introspection server, or the bundled examples. Those are exercised by running them manually.
Honest state of the code
What works:
- In-process bodies.
examples/inprocess-demoand both updatedhello-world/two-bodiesexamples run end to end. - Aggregator with CRITICAL-preserving trim cascade.
- Translator with per-parameter type / enum / min / max / required validation.
- Ollama bridge over raw
node:http. - CLI with colored output, spinner, and panel rendering using only
node:*.
What exists but has a caveat:
NetworkBodyAdapterproxies tool calls to a remote body over HTTP, butSpace.addBodydoes not yet auto-wrap HTTP-transport bodies in it. You have to instantiate the proxy yourself.- Introspection server is unauthenticated. Bind to localhost only.
- The bundled
scp-protocoldependency is pinned to^0.3.0; the two packages are developed together.
What is not implemented yet:
- CRDT or shared state between bodies.
- Body-to-body lateral events.
- Persistent vertical memory across sessions. The current
body_decisionhistory is in-memory and bounded to the last 10 entries. - Safety gate that validates LLM intents against policy before dispatch.
- Process isolation per body. Bodies share the Node process.
- Retry and cost tracking in the Brain base class.
Links
- Source: https://github.com/srk0102/plexa
- npm: https://npmjs.com/package/@srk0102/plexa
- SCP protocol: https://npmjs.com/package/scp-protocol
License
MIT