docs: add simulation controller SDK spec, replace spatial topology
The mesh is the communication fabric, not the simulation engine. SimController pattern: external controller drives tick loop, computes visibility, sends observations to peers, collects actions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
419
docs/simulation-sdk-spec.md
Normal file
419
docs/simulation-sdk-spec.md
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
# @claudemesh/sim — Simulation Controller SDK
|
||||||
|
|
||||||
|
**Date:** 2026-04-08
|
||||||
|
**Status:** Design spec — not yet implemented
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The mesh is the communication fabric, not the simulation engine. Simulation logic lives in **controllers** — external processes that connect to the mesh as peers and drive the simulation loop.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ 3D Space / UI │ Renders world state,
|
||||||
|
│ (Three.js, Unity, │ provides human input
|
||||||
|
│ dashboard, Python) │
|
||||||
|
└───────────┬─────────────┘
|
||||||
|
│ reads state, sends commands
|
||||||
|
┌───────────┴─────────────┐
|
||||||
|
│ Simulation Controller │ Owns world model,
|
||||||
|
│ (@claudemesh/sim) │ computes observations,
|
||||||
|
│ │ collects actions
|
||||||
|
└───────────┬─────────────┘
|
||||||
|
│ mesh messages + state
|
||||||
|
┌───────────┴─────────────┐
|
||||||
|
│ Mesh (broker) │ Delivers messages,
|
||||||
|
│ │ holds shared state,
|
||||||
|
│ │ drives clock ticks
|
||||||
|
└───────────┬─────────────┘
|
||||||
|
┌─────┼─────┐
|
||||||
|
┌──┴──┐ ┌┴───┐ ┌┴───┐
|
||||||
|
│Peer │ │Peer│ │Peer│ AI agents with
|
||||||
|
│"Rep"│ │"C1"│ │"Mgr"│ personas, react
|
||||||
|
└─────┘ └────┘ └────┘ to observations
|
||||||
|
```
|
||||||
|
|
||||||
|
**Separation of concerns:**
|
||||||
|
- **Mesh:** messages, state, peers, clock ticks
|
||||||
|
- **Controller:** world model, physics, visibility, rules, tick loop
|
||||||
|
- **3D/UI:** rendering, human input (optional)
|
||||||
|
- **AI peers:** persona behavior, actions, communication
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SimController class
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { SimController } from "@claudemesh/sim";
|
||||||
|
|
||||||
|
const sim = new SimController({
|
||||||
|
// Mesh connection (uses @claudemesh/sdk under the hood)
|
||||||
|
brokerUrl: "wss://ic.claudemesh.com/ws",
|
||||||
|
meshId: "sim-store-001",
|
||||||
|
memberId: "controller-floor",
|
||||||
|
pubkey: keys.publicKey,
|
||||||
|
secretKey: keys.secretKey,
|
||||||
|
displayName: "Floor Controller",
|
||||||
|
|
||||||
|
// Controller config
|
||||||
|
role: "orchestrator", // "orchestrator" | "domain"
|
||||||
|
domain: "floor", // state namespace prefix: "sim:floor:*"
|
||||||
|
tickSource: "clock", // "clock" (follow mesh clock) | "self" (drive own ticks)
|
||||||
|
tickInterval: 1000, // ms, only used when tickSource: "self"
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lifecycle
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await sim.connect();
|
||||||
|
|
||||||
|
// Define the world
|
||||||
|
sim.defineZones({
|
||||||
|
"store-floor": { connects: ["checkout", "office"], capacity: 20 },
|
||||||
|
"checkout": { connects: ["store-floor", "warehouse"], capacity: 5 },
|
||||||
|
"warehouse": { connects: ["checkout"], capacity: 10 },
|
||||||
|
"office": { connects: ["store-floor"], capacity: 4 },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Place peers in zones
|
||||||
|
sim.placePeer("Sales-Rep-1", "store-floor");
|
||||||
|
sim.placePeer("Customer-1", "store-floor");
|
||||||
|
sim.placePeer("Manager", "office");
|
||||||
|
|
||||||
|
// Define obstructions (optional — for geometric mode)
|
||||||
|
sim.addObstruction("wall-1", { from: {x:0, y:5}, to: {x:10, y:5} });
|
||||||
|
|
||||||
|
// Define visibility rules
|
||||||
|
sim.setVisibilityMode("zone"); // "zone" | "radius" | "custom"
|
||||||
|
// zone: peers in same zone + connected zones can see each other
|
||||||
|
// radius: distance check with optional obstructions (line-of-sight)
|
||||||
|
// custom: provide your own function
|
||||||
|
|
||||||
|
// Custom visibility (most flexible)
|
||||||
|
sim.setVisibilityFn((peerA, peerB, world) => {
|
||||||
|
// Your logic: zones, distance, line-of-sight, roles, anything
|
||||||
|
return peerA.zone === peerB.zone;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the simulation
|
||||||
|
sim.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tick loop
|
||||||
|
|
||||||
|
The controller runs a loop — either driven by mesh clock ticks or self-driven:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
sim.onTick(async (tick) => {
|
||||||
|
// 1. Read world state
|
||||||
|
const world = sim.getWorldState();
|
||||||
|
|
||||||
|
// 2. Compute visibility for each peer
|
||||||
|
const visibility = sim.computeVisibility();
|
||||||
|
// Returns: { "Sales-Rep-1": ["Customer-1"], "Customer-1": ["Sales-Rep-1"], "Manager": [] }
|
||||||
|
|
||||||
|
// 3. Build observations per peer
|
||||||
|
for (const [peerName, visiblePeers] of Object.entries(visibility)) {
|
||||||
|
const peer = world.peers[peerName];
|
||||||
|
const observation = {
|
||||||
|
tick: tick.number,
|
||||||
|
simTime: tick.simTime,
|
||||||
|
you: { zone: peer.zone, role: peer.role },
|
||||||
|
visible_peers: visiblePeers.map(name => ({
|
||||||
|
name,
|
||||||
|
zone: world.peers[name].zone,
|
||||||
|
role: world.peers[name].role,
|
||||||
|
status: world.peers[name].lastAction,
|
||||||
|
})),
|
||||||
|
environment: sim.getZoneState(peer.zone),
|
||||||
|
events: sim.getEventsFor(peerName),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. Send observation to the peer
|
||||||
|
await sim.sendObservation(peerName, observation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Collect actions from previous tick
|
||||||
|
const actions = sim.collectActions();
|
||||||
|
// Returns: [{ peer: "Sales-Rep-1", action: "greet", target: "Customer-1" }, ...]
|
||||||
|
|
||||||
|
// 6. Apply actions to world state
|
||||||
|
for (const action of actions) {
|
||||||
|
sim.applyAction(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Publish world state (for dashboard/3D renderer)
|
||||||
|
await sim.publishWorldState();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Observations and actions protocol
|
||||||
|
|
||||||
|
**Observation (controller → peer):** Sent as a mesh message with a structured format. The peer's MCP server receives it as a channel notification.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "sim_observation",
|
||||||
|
"tick": 42,
|
||||||
|
"simTime": "2026-04-08T14:30:00Z",
|
||||||
|
"you": {
|
||||||
|
"zone": "store-floor",
|
||||||
|
"position": { "x": 10, "y": 5 },
|
||||||
|
"role": "sales-rep",
|
||||||
|
"inventory": ["catalog", "tablet"]
|
||||||
|
},
|
||||||
|
"visible_peers": [
|
||||||
|
{ "name": "Customer-1", "zone": "store-floor", "status": "browsing" }
|
||||||
|
],
|
||||||
|
"environment": {
|
||||||
|
"zone": "store-floor",
|
||||||
|
"customers_present": 3,
|
||||||
|
"noise_level": "moderate",
|
||||||
|
"time_of_day": "afternoon"
|
||||||
|
},
|
||||||
|
"events": [
|
||||||
|
"Customer-1 entered your zone",
|
||||||
|
"New shipment arrived in warehouse"
|
||||||
|
],
|
||||||
|
"available_actions": [
|
||||||
|
"greet <peer>",
|
||||||
|
"show_product <sku> <peer>",
|
||||||
|
"check_inventory <sku>",
|
||||||
|
"move_to <zone>",
|
||||||
|
"radio <message>"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Action (peer → controller):** Peer sends a structured message back to the controller.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "sim_action",
|
||||||
|
"tick": 42,
|
||||||
|
"actions": [
|
||||||
|
{ "action": "greet", "target": "Customer-1" },
|
||||||
|
{ "action": "show_product", "sku": "ABC-123", "target": "Customer-1" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The controller validates actions (can this peer do this? are they in the right zone?) and applies valid ones to the world state.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Multi-controller pattern
|
||||||
|
|
||||||
|
### Federated controllers
|
||||||
|
|
||||||
|
Each controller owns a domain — its slice of the world. They coordinate through mesh state.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Floor controller — owns positions, zones, visibility
|
||||||
|
const floor = new SimController({
|
||||||
|
role: "domain",
|
||||||
|
domain: "floor",
|
||||||
|
tickSource: "clock",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inventory controller — owns stock, pricing
|
||||||
|
const inventory = new SimController({
|
||||||
|
role: "domain",
|
||||||
|
domain: "inventory",
|
||||||
|
tickSource: "clock",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Orchestrator — merges domains into unified observations
|
||||||
|
const orchestrator = new SimController({
|
||||||
|
role: "orchestrator",
|
||||||
|
domains: ["floor", "inventory", "queue"],
|
||||||
|
tickSource: "clock",
|
||||||
|
});
|
||||||
|
|
||||||
|
orchestrator.onTick(async (tick) => {
|
||||||
|
// Read all domain states
|
||||||
|
const floor = await orchestrator.getDomainState("floor");
|
||||||
|
const inventory = await orchestrator.getDomainState("inventory");
|
||||||
|
|
||||||
|
// Merge into unified observation per peer
|
||||||
|
for (const peer of orchestrator.getPeers()) {
|
||||||
|
const obs = mergeObservation(peer, floor, inventory);
|
||||||
|
await orchestrator.sendObservation(peer.name, obs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### State namespacing
|
||||||
|
|
||||||
|
Each domain writes to its own namespace:
|
||||||
|
|
||||||
|
```
|
||||||
|
sim:floor:positions → {"Alice": {"x":10,"y":5}, "Bob": {"x":3,"y":8}}
|
||||||
|
sim:floor:zones → {"store-floor": {"peers": ["Alice","Bob"]}}
|
||||||
|
sim:inventory:stock → {"ABC-123": 42, "DEF-456": 0}
|
||||||
|
sim:queue:lines → [{"id": 1, "length": 3, "cashier": "Carol"}]
|
||||||
|
sim:orchestrator:tick → 42
|
||||||
|
```
|
||||||
|
|
||||||
|
Any peer, controller, or dashboard reads state to render or react.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## World state management
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Define entity types
|
||||||
|
sim.defineEntityType("product", {
|
||||||
|
sku: "string",
|
||||||
|
name: "string",
|
||||||
|
price: "number",
|
||||||
|
stock: "number",
|
||||||
|
zone: "string",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create entities
|
||||||
|
sim.createEntity("product", { sku: "ABC-123", name: "Widget", price: 29.99, stock: 42, zone: "store-floor" });
|
||||||
|
|
||||||
|
// Query entities
|
||||||
|
const products = sim.queryEntities("product", { zone: "store-floor" });
|
||||||
|
|
||||||
|
// Peer actions modify entities
|
||||||
|
sim.defineAction("purchase", {
|
||||||
|
validate: (peer, args, world) => {
|
||||||
|
const product = world.getEntity("product", args.sku);
|
||||||
|
return product && product.stock > 0 && peer.zone === product.zone;
|
||||||
|
},
|
||||||
|
apply: (peer, args, world) => {
|
||||||
|
const product = world.getEntity("product", args.sku);
|
||||||
|
product.stock -= 1;
|
||||||
|
peer.inventory.push(args.sku);
|
||||||
|
return { event: `${peer.name} purchased ${product.name}` };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dashboard integration
|
||||||
|
|
||||||
|
The 3D/UI reads world state from the mesh and renders it. No special protocol — just `get_state`.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Three.js dashboard
|
||||||
|
const positions = JSON.parse(await mesh.getState("sim:floor:positions"));
|
||||||
|
for (const [name, pos] of Object.entries(positions)) {
|
||||||
|
updatePeerMarker(name, pos.x, pos.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Or subscribe to a stream
|
||||||
|
mesh.subscribe("sim:world");
|
||||||
|
mesh.on("stream_data", (data) => {
|
||||||
|
renderFrame(data);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Human input flows back through the mesh:
|
||||||
|
```javascript
|
||||||
|
// User clicks to move a peer
|
||||||
|
canvas.on("click", (pos) => {
|
||||||
|
mesh.send("Floor Controller", JSON.stringify({
|
||||||
|
type: "sim_command",
|
||||||
|
command: "move_peer",
|
||||||
|
peer: selectedPeer,
|
||||||
|
to: { x: pos.x, y: pos.y },
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Package structure
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/sim/
|
||||||
|
package.json @claudemesh/sim
|
||||||
|
src/
|
||||||
|
index.ts Main exports
|
||||||
|
controller.ts SimController class
|
||||||
|
world.ts WorldState manager
|
||||||
|
visibility.ts Zone-based + radius + custom visibility
|
||||||
|
actions.ts Action validation + application
|
||||||
|
entities.ts Entity type system
|
||||||
|
observation.ts Observation builder
|
||||||
|
protocol.ts Message format constants
|
||||||
|
types.ts TypeScript types
|
||||||
|
README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dependencies:**
|
||||||
|
- `@claudemesh/sdk` — mesh connectivity
|
||||||
|
- No physics engine — keep it lightweight. Users bring their own for geometric mode.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example: retail store simulation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { SimController } from "@claudemesh/sim";
|
||||||
|
|
||||||
|
const sim = new SimController({ /* mesh config */ });
|
||||||
|
await sim.connect();
|
||||||
|
|
||||||
|
// World setup
|
||||||
|
sim.defineZones({
|
||||||
|
"entrance": { connects: ["store-floor"] },
|
||||||
|
"store-floor": { connects: ["entrance", "checkout", "fitting-room"] },
|
||||||
|
"fitting-room": { connects: ["store-floor"] },
|
||||||
|
"checkout": { connects: ["store-floor", "back-office"] },
|
||||||
|
"back-office": { connects: ["checkout"] },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Peer personas (mesh skills)
|
||||||
|
await sim.shareSkill("sales-rep", {
|
||||||
|
instructions: "You are a retail sales rep. Greet customers who enter your zone. Show products when asked. Guide to fitting room or checkout. Be helpful but not pushy.",
|
||||||
|
});
|
||||||
|
await sim.shareSkill("customer", {
|
||||||
|
instructions: "You are a customer shopping for clothes. Browse, ask questions, try things on. You have a budget of $200. Make realistic decisions.",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Place peers
|
||||||
|
sim.placePeer("Rep-1", "store-floor", { role: "sales-rep", skill: "sales-rep" });
|
||||||
|
sim.placePeer("Rep-2", "store-floor", { role: "sales-rep", skill: "sales-rep" });
|
||||||
|
sim.placePeer("Cust-1", "entrance", { role: "customer", skill: "customer" });
|
||||||
|
sim.placePeer("Cust-2", "entrance", { role: "customer", skill: "customer" });
|
||||||
|
sim.placePeer("Manager", "back-office", { role: "manager" });
|
||||||
|
|
||||||
|
// Set clock to 10x
|
||||||
|
await sim.setClock(10);
|
||||||
|
|
||||||
|
// Run
|
||||||
|
sim.start();
|
||||||
|
|
||||||
|
// After N ticks, generate report
|
||||||
|
sim.onComplete(async (report) => {
|
||||||
|
console.log(`Simulation complete: ${report.ticks} ticks`);
|
||||||
|
console.log(`Actions: ${report.totalActions}`);
|
||||||
|
console.log(`Sales: ${report.entities.product.filter(p => p.stock === 0).length} sold out`);
|
||||||
|
console.log(`Customer satisfaction: ${report.metrics.satisfaction}`);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Effort estimate
|
||||||
|
|
||||||
|
| Component | Effort |
|
||||||
|
|-----------|--------|
|
||||||
|
| SimController core (tick loop, observations, actions) | 2 days |
|
||||||
|
| World state + entity system | 1 day |
|
||||||
|
| Visibility (zone + radius + custom) | 1 day |
|
||||||
|
| Protocol (observation/action message format) | Half day |
|
||||||
|
| Dashboard integration examples | Half day |
|
||||||
|
| Retail store example | Half day |
|
||||||
|
| **Total** | **5-6 days** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*This replaces the "spatial topology" vision item. Instead of baking simulation features into the broker, we build a simulation framework on top of the mesh.*
|
||||||
@@ -69,10 +69,19 @@ Per-mesh compute sandboxes. Peers request: `execute_code(lang: "python", code: "
|
|||||||
|
|
||||||
**Effort:** 2-3 days (E2B), 1-2 weeks (self-hosted).
|
**Effort:** 2-3 days (E2B), 1-2 weeks (self-hosted).
|
||||||
|
|
||||||
### Spatial topology (proximity-based visibility)
|
### Simulation controller SDK (`@claudemesh/sim`)
|
||||||
Extend visibility with `(x, y)` coordinates and visibility radius. Peers only see others within range. Combined with sim clock, enables spatial simulations (customer walks into store zone, sees sales reps).
|
Replaces the "spatial topology" item. Instead of baking simulation features into the broker, build a framework on top of the mesh.
|
||||||
|
|
||||||
**Effort:** 1 day.
|
- **SimController class** — connects as a peer, drives tick loop, computes visibility, sends observations, collects actions
|
||||||
|
- **World state** — entity system, zone graph, state namespacing (`sim:domain:key`)
|
||||||
|
- **Visibility modes** — zone-based, radius, custom function (bring your own physics)
|
||||||
|
- **Multi-controller** — federated domains (floor, inventory, queue) with orchestrator merge
|
||||||
|
- **Dashboard integration** — reads world state from mesh, human input flows back via messages
|
||||||
|
- **The mesh stays simple** — messaging + state. All simulation logic lives in the controller.
|
||||||
|
|
||||||
|
Full spec: [`simulation-sdk-spec.md`](./simulation-sdk-spec.md)
|
||||||
|
|
||||||
|
**Effort:** 5-6 days.
|
||||||
|
|
||||||
### Semantic peer search
|
### Semantic peer search
|
||||||
`search_peers(query, filters?)` — multi-field matching across names, groups, roles, summaries, profile capabilities, skills. Ranked results. For meshes with 50+ peers.
|
`search_peers(query, filters?)` — multi-field matching across names, groups, roles, summaries, profile capabilities, skills. Ranked results. For meshes with 50+ peers.
|
||||||
|
|||||||
Reference in New Issue
Block a user