Repository navigation
@flybot/tree (recursive-bot): organize bots as a tree #6
Description
Part of #3 (umbrella). Implemented in #2. Owner: recursive-bot.
Source: Birfy/recursive-bot, branch flybot-tree, ds-bot/plugins/tree/ (current tip 324fb59).
Goal
Organize bots as a recursive tree. A Root coordinator splits a task, creates child bots under itself, and delegates level by level:
- Each layer sees only its direct children: its prompt lists them, and it can message only them and its parent. It never plans for grandchildren; each child splits its own part.
- Only leaves do the work. A bot with children acts as a coordinator: it splits, delegates in parallel and aggregates. A bot without children answers the request itself.
Design
- Plugin
@flybot/tree(name: 'flybot-tree',inject: ['flybot'], cordis-styleapply(ctx, config)) inds-bot/plugins/tree/. Onlysrc/adapter.jstouchesctx.flybot(flybot API v1,ds-bot/types/flybot.d.ts). - Layout:
index.js(wiring),src/adapter.js,src/tree.js(model),src/limits.js+limits.json,src/middleware.js,src/tools.js,src/prompt.js,client.js(browser),test/mock-flybot.js,test/tree.test.js. ds-bot'stest/tree-plugin.test.jsruns the plugin tests under ds-botnpm test, andscripts/lint.mjslintsplugins/. - Root is created automatically.
tree.ensureRoot()(async, idempotent) createsRoot(name configurable withconfig.rootName) when no tree bot has a nullparent. The tree starts with only the root; children are created during conversations.
Data (fields)
All reads and writes go through ctx.flybot.scope('flybot-tree'), so the data is stored under state.bots[id].fields['flybot-tree'] and never under fields.shared:
parent: parent bot id;nullfor the root.treeRole: role description. A bot is a tree member if it hastreeRoleorparent.- The system prompt itself lives on the bot record (
BotView.systemPrompt, set withbot.setSystemPrompt). It is not a field. - Events use
on('change' | 'bot.removed', fn, { all? }).changedelivers only this plugin's fields; tree subscribers react toparentandtreeRole.bot.createdis left to social; tree does not subscribe to it.
Tools (visible only to tree bots; visibleTo is sync)
The caller always comes from the tool context ({ botId }), never from the arguments, so a bot can create children only under itself.
create_child_bot({ name, role_description })name: string, 1–60 characters (after trimming), required.role_description: string, 1–1000 characters (after trimming), required.- Duplicate names among siblings are rejected (case-insensitive), as are limit violations.
- Awaits
bot.create(...)and returns{ created: { id, name, depth } }, whereidis the resolved string id. It then rebuilds the parent's prompt so the new child is listed.
list_children(): takes no arguments. Returns{ children: [{ id, name, role }], depth, limits }.- Validation:
tools.registerkeeps onlytype/description/enum/itemsfrom JSON Schema and dropsminLength/maxLength. So eachexecuterunsvalidateArgs, which checks required fields, string type,minLength/maxLengthand unknown arguments (additionalProperties: false). Errors come back as{ error }.
Limits
limits.json:{ "maxDepth": 5, "maxChildren": 8 }. Depth counts the root as 1.- Overridable through plugin config
maxDepth/maxChildren(positive integers; invalid values fall back tolimits.json).
Prompt injection
src/prompt.js builds the system prompt for every tree bot, so a new child inherits the tree rules. Each prompt has the bot's name, role and position trace (e.g. Root → Work → Dev), the rule "message only your parent and direct children", and the tool usage.
- Leaf: do the work, answer concisely, and create a child only for a clearly separate domain.
- Coordinator: lists its direct children. Its rules: split the request; delegate only to children whose scope fits; create a child only when none fits (check
list_children, no duplicates); send in parallel; aggregate the answers (pass a single answer through); start with a delegation trace. - Prompts are rebuilt when a child is created or removed.
Messaging rules (send middleware tree, before: ['social'])
- Tree bot → tree bot: allowed only between a parent and its direct children. Grandchildren, siblings and the grandparent are denied.
- Tree bot → local bot outside the tree (e.g. Chief): denied.
- Local bot outside the tree → tree: allowed only to the root. Any other tree node is denied (e.g. Chief → child = deny, Chief → Root = pass).
- Outside → outside (local): no opinion.
- Remote traffic is deferred to social. Remote targets (
target.remote === true, fallbackbot.isRemote(to), i.e.{ kind: 'remote' }/@handle) and remote senders get no opinion (undefined). The social plugin then applies its friendship checks and approval. Remote bots are never tree members, and their fields are not read. - Bypass: allowed tree edges return
{ action: 'allow', bypass: ['social'] }only when plugin configbypassSocial: trueis set. The host honours it only ifconfig.flybot.bypass.treelistssocial.
Lifecycle
- On
bot.removed(fires after deletion), the parent is read from the event's last record,bot.fields['flybot-tree'].parent. The removed bot's children are re-attached to that parent, and the parent's system prompt is rebuilt withbot.setSystemPrompt. - Removal of a bot that is not in the tree is ignored. Subscribers get a
removedevent.
UI
- The
flybot.sidebar.sectionslot (idflybot-tree, order 100) shows a collapsible "Tree" section. - It is built in the browser from
ctx.flybot.bots()(readingfields['flybot-tree']) and refreshed onfields.on('change')forparent/treeRoleand onon('bot.removed'). - Hand-written
client.jsin thewindow.__ModuleLoader__.loadformat, with no build step.
Tests
- Plugin tests: 21/21 (
node --test "test/*.test.js", Node 24), againsttest/mock-flybot.js. The mock mirrors the real API:scope(), asynccreate, null removes a key,on(event, fn, { all }), the real send-target andbot.removedshapes, and schema keywords dropped bytools.register. Coverage:- scoped fields
- the resolved id from
create_child_bot - parent/child edges
- denying sends to local bots outside the tree
- outside → root only
- remote pass-through
- min/maxLength validation
- limits
- re-attaching children on removal
- client forest
- recursive-bot
flybot-tree: ds-botnpm test538/538, plusbuild-client.mjs --checkand lint pass. - flybot-dev
main(a730804) with the plugin copied in:npm test561/561, plusbuild-client.mjs --checkand lint pass.
Commit history (d0706c5..324fb59)
d0706c5feat(tree): @flybot/tree plugin package (parent field, send middleware, tree tools, sidebar)1a4c9b7feat(tree): move @flybot/tree into FlyBot layout at ds-bot/plugins/tree, wire into ds-bot npm test and lintf3755ccfeat(tree): match finalized flybot interface (sync fields, create->id, setSystemPrompt, bot.removed, sync visibleTo, client tree from bots())25b0663feat(tree): flybot.on('bot.removed') only, namespaced fields, browser reads fields['flybot-tree'], parent from removal recordfdb4a3ffeat(tree): pass remote send targets (target.remote, fallback bot.isRemote) to social; bot.created left to sociala7e51c5fix(tree): align with real flybot API v1: scope('flybot-tree'), awaited bot.create, deny tree → outside local bots, validate min/maxLength in execute, mock matches the real API324fb59fix(tree): local non-tree senders may message only the tree root
Open items
- Tool to attach a remote bot as a tree child.
- Cost/usage roll-up per subtree.
- Integration test of the plugin against the real
ctx.flybothost (src/host/hooks.js); today the plugin tests use the mock. Done:plugins/tree/test/host.test.js. - Merge the plugin into FlyBot
ds-bot/plugins/tree(replacing the placeholder README) and addtest/tree-plugin.test.jsthere. Merged as the peer pluginplugins/tree(Decouple plugins: tree and social as peer plugins under plugins/ (#34 PR 2, stacked on #37) #40). - Re-parent / move a subtree, and rename a child (no tool yet).
- Guard against concurrent
ensureRoot()calls creating two roots. - Non-Main tree Bots may create children (Decision: should non-Main tree Bots be allowed to call create_child_bot? (PR #2 review) #16 Option B, docs(tree): allow non-Main tree Bots to create children (#16 Option B) #119).
Activity
tree: #6 and #20 tree items from recursive-bot (flybot-tree 5631cbc)