Delegation Patterns
Crucible doesn’t ship hardcoded “team” types. Three common delegation patterns — broadcast, router, supervisor — are short Lua recipes against the existing cru.session primitives. This page is a reference, not a library: copy a recipe, edit it for your case.
The primitives you’ll use:
cru.session.create({ type = "chat", kilns = {...} })— spawn a fresh session (optionally with kilns attached for knowledge access).cru.session.create({ delegate = true, prompt = "...", target = "..." })— spawn through the daemon’s delegation service instead. The parent is stamped from the session your Lua runs for (never settable from data), the parent’s owndelegation_configgates it (enabled,allowed_targets), and the result is a job record —{ delegation_id, child_session_id, status }— thatcollect_subagentspolls. Reachable from a session VM’s own Lua and fromlua.init_session; the shared plugin VM has no current session, sodelegate = truethere is refused with that reason.cru.session.configure_agent(id, { agent_name = "..." })— pick which agent profile drives this session.cru.session.send_and_collect(id, prompt, { timeout = N })— blocking. Returns an iterator that yields structured response parts:{ type = "text"|"tool_call"|"tool_result"|"thinking", ... }.cru.session.send_message(id, content)— async dispatch; returns a request id. Use this when you want the agent processing in the background and don’t need to await output inline.cru.session.collect_subagents(job_ids, timeout)— await N background subagent jobs.delegation_idfrom a delegated create is one of these job ids, so the delegate primitive and the session primitive meet on one polling surface.cru.session.fork(id, opts?)— clone a session’s history into a new session, e.g. for A/B exploration.cru.session.end_session(id)— clean up.
See Sessions for full signatures and the Lua Runtime API reference for return shapes.
Helper: collect the text from a streamed response
send_and_collect returns an iterator over structured parts. Most
recipes want the prose text concatenated:
local function collect_text(stream) local parts = {} if not stream then return "" end while true do local part = stream() if not part then break end if part.type == "text" then parts[#parts + 1] = part.content end end return table.concat(parts, "")endThe reflection plugin uses this pattern — see
runtime/plugins/reflection/init.lua for a working reference.
A plugin is rarely the right home for it, though. “Spawn a subagent with a
kiln attached and ask it” is an agent card plus a kiln plus delegation, and a
card you author says it more directly than Lua that assembles the same three
pieces by hand. kiln-expert was exactly that plugin and was removed for it.
Router — classify input, dispatch to one specialist
Use when: input type determines which agent should handle it. Cheap, single-shot, no coordination overhead.
local function classify(msg) -- Decider can be anything: pattern match, regex, config-driven, -- or a small LLM call. This one is pure Lua. if msg:match("^!search ") then return "researcher" elseif msg:match("```") then return "code-reviewer" else return "default" endend
local function route(prompt) local agent_name = classify(prompt) local s = cru.session.create({ type = "chat" }) cru.session.configure_agent(s.id, { agent_name = agent_name })
local stream = cru.session.send_and_collect(s.id, prompt, { timeout = 60 }) local reply = collect_text(stream) cru.session.end_session(s.id) return reply, agent_nameendSupervisor — iterative decision loop
Use when: a “manager” agent decides which specialist runs next based on accumulated context. The supervisor is just another session; you ask it what to do, run that, and feed the output back.
local function supervise(task, agent_pool, max_steps) local sup = cru.session.create({ type = "chat" }) cru.session.configure_agent(sup.id, { agent_name = "supervisor" })
local history = {} for step = 1, (max_steps or 10) do -- Ask the supervisor what to do next. Encode history however -- you like — JSON, bulleted markdown, etc. local plan_prompt = string.format( 'Task: %s\nHistory so far: %s\nAvailable: %s\n' .. 'Reply with JSON: { "agent": "name", "prompt": "..." } ' .. 'or { "done": true }.', task, cru.json.encode(history), table.concat(agent_pool, ", ")) local plan = collect_text( cru.session.send_and_collect(sup.id, plan_prompt, { timeout = 60 }))
local ok, decision = pcall(cru.json.decode, plan) if not ok or not decision then break end if decision.done then break end
-- Run the chosen specialist on the chosen sub-prompt. local worker = cru.session.create({ type = "chat" }) cru.session.configure_agent(worker.id, { agent_name = decision.agent }) local output = collect_text( cru.session.send_and_collect(worker.id, decision.prompt, { timeout = 120 })) cru.session.end_session(worker.id)
history[#history + 1] = { agent = decision.agent, output = output } end
cru.session.end_session(sup.id) return historyendThe decider can be anything that returns { agent, prompt } or
{ done = true }. Above it’s an LLM-judged JSON plan; for a
hard-coded sequence (e.g. researcher → writer → fact-checker), drop
the LLM call and return history[step] from a static table.
Broadcast — fan out to N agents
Use when: multiple agents should weigh in on the same input.
Note: cru.session.send_and_collect is blocking, so a loop over N
sessions runs sequentially. That’s the right answer when each
sub-call is cheap or you don’t mind serialised latency:
local function broadcast_sequential(agents, prompt) local results = {} for i, agent_name in ipairs(agents) do local s = cru.session.create({ type = "chat" }) cru.session.configure_agent(s.id, { agent_name = agent_name }) results[i] = { agent = agent_name, output = collect_text( cru.session.send_and_collect(s.id, prompt, { timeout = 60 })), } cru.session.end_session(s.id) end return resultsendFor true parallelism, use the subagent-spawning path instead. Tools that delegate work produce delegation ids (the child session id); collect them all at once:
-- Inside a tool handler with access to the subagent factory:local job_ids = {}for _, agent in ipairs({ "researcher", "skeptic", "writer" }) do -- delegate_session returns a delegation id (child session id) -- and your plugin's subagent factory for the exact call shape. job_ids[#job_ids + 1] = spawn_subagent(agent, prompt)endlocal results = cru.session.collect_subagents(job_ids, 60)-- results is { { id, status, output | error, exit_code }, ... }collect_subagents waits on background jobs from the daemon’s
subagent infrastructure (distinct from sessions created by
cru.session.create). See Delegation for how to
spawn those jobs and the delegate_session tool for the host-side
contract.
Why no built-in delegation types?
Three reasons:
- Patterns vary. Your supervisor might use a Lua decider, an LLM,
a regex, or a config-driven DAG. A hardcoded
Supervisorstruct picks one and shuts out the others. - Primitives compose. `create + configure_agent + send_and_collect
- end_session` lets you express the three patterns above plus pipelines, retries, fan-out-fan-in, chained delegation, and workflows the authors didn’t think of — anything you can put into Lua control flow.
- Surface area honesty. A built-in type for every delegation shape means shipping abstractions nobody asked for. Recipes stay current precisely because you can see and edit them.
If a recipe pattern recurs across many of your plugins, fold it into a
helper module — require("delegation_helpers").router(prompt, classify)
— and ship that as a plugin. The primitives are the right contract;
opinionated helpers are user-land.