The model
Skep is built from one abstraction — the bee — and a handful of nouns that fall out of treating space, not time, as the workspace. This page is the whole mental model.
Bee
A bee is the only actor. A queen, a scout, a worker — all bees. A bee carries:
- a goal — the self-contained brief it exists to satisfy,
- a mind — how it decides (its policy),
- keys — the locks it can open (which actions it gets to see),
- and, as it runs: a local log, child bees, and eventually a result.
There is no separate "queen type" or "orchestrator class." The queen is simply the first bee; orchestration is just a bee dispatching other bees.
Cell
A cell is a reusable room. It renders content (what's in the room) and offers actions
(what you can do here). Cells are pure definitions — setup, content, actions — with no
runtime state of their own; a bee enters a cell with some input and the cell sets up its
state for that visit.
const folder = cell("fs.folder", {
enter: (input) => loadFolder(input),
show: (state) => xml`<folder path="${state.path}">…</folder>`,
does: { grep, open_file /* … action cards … */ },
});
Each action declares its locks; a bee only sees an action whose locks its keys can open.
Two different bees can walk into the same cell and see different action lists. The runtime
injects a resolve action into every cell, so a bee can always finish — you never write it.
Mind
A mind has exactly one method:
interface Mind {
decide(ctx: { bee; view; actions }): Promise<{ action; args }>;
}
If you've done RL or agent work, this is the policy: a function from observation (the
rendered cell) to action. Everything a bee "authors" — a child's goal, its own result — is
just an argument of the action it decides to take. There is no separate "now write your
summary" step; the bee writes it in the same breath it chooses resolve.
llmMind() is a ready-made policy backed by an LLM. A mock is just an object with a
decide. Because the mind lives on the bee, different bees can think with different models.
Locks & keys
Governance is lock-and-key, nothing more. An action declares locks: ["write"]; a bee
carries keys (["read"], or ["read", "write"]). A bee sees a door iff its keys open
every lock (AND). scout and worker are just key presets (scout → ["read"],
worker → ["read", "write"]); define your own via beeTypes. That set-membership check —
locks ⊆ keys — is the entire permission system. (Need "any of"? Mint a key both roles
carry and lock the door with that one key.)
Power only attenuates. A bee can only spawn children whose keys are a subset of its own — a read-only bee cannot spawn a write bee. Escalation fails closed (the child is never created). The queen holds every key, so she can dispatch anything; everyone downstream can only narrow. If a sub-task needs more power, the queen has to be the one to grant it.
Spawning, and the ancestor contract
A bee goes deeper by spawning a child into another cell with an explicit goal:
const result = await ctx.spawn(fileCell, { path }, "Confirm dashboard.js imports IconAffiliate.");
The child runs to its own result and returns it. What the child sees is deliberately narrow — its lexical scope:
- the goals of its ancestors (queen → … → me): the contract it's operating under,
- its own goal,
- the results its own children returned,
- its own local log.
A bee never sees its siblings or their logs. This is exactly a function's view of its call chain — and it's why context stays small and parallel bees can't interfere. The consequence: the goal is the only channel into a child. A parent threads what it learned into the next child's brief; the parent is the router.
Comb
A run weaves a comb: the queen and her tree of bees, plus the result.
const comb = await hive.run(prompt);
comb.result; // GoalResult: { outcome, summary }
comb.queen.children; // the tree
comb.budgetUsed;
A result is intentionally small — an outcome (succeeded | partial | not_found | blocked
| failed) and a summary. The detail lives in the cells the bees visited; only the
takeaway crosses a boundary.
Run context
Cell state is the room's working data. Cross-cutting request data — userId, auth,
tenant, a trace id — is different: it belongs to the whole run, not one cell. That's run
context: set once at run(), immutable, readable by every bee as ctx.context, and
inherited down the comb automatically.
await hive.run(prompt, { context: { userId, tenant, auth } });
// in any action:
run: (args, ctx) => { authorize(ctx.context.auth); }
It's read-only on purpose — a bee should never mutate the caller's identity. (Cell state is where mutable, per-visit data lives; context is the ambient environment the comb runs in.)
The interface is swappable
What a bee actually reads is produced by a renderer — the function that paints a bee's view (cell content + goal + child results + actions) into text. It defaults to a built-in, but it's a first-class, swappable seam, because the interface is the experiment: the same engine and model can behave very differently depending on how the room is drawn. Treating the workspace as space is the thesis; the renderer is where you tune it.