Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/tree-find-load.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The tree's `find_bots` now reports each Bot's live runtime load (how many turns it is running), the same number `list_children` shows, so a coordinator can pick a free Bot when it searches the branch.
2 changes: 1 addition & 1 deletion docs/subsystems/tree.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Everything in `plugins/tree/` (entry `plugins/tree/index.js` and any helpers).

Allocation puts a new Bot in a sensible place once; the self-organization below (#446) is the safety net that keeps the tree healthy afterwards. `src/allocate.js` uses `organize.js`'s shared primitives (`organizer.shared`: role profiles, homes, `fits`, `createLead`, `markLead`, `move`, journal, the reorganization queue), so explicit allocation and automatic reorganization have one set of limit checks and one prompt / trace refresh.

- **Reuse before create.** `find_bots(query?)` (read-only, every tree Bot) lists the Bots in the caller's branch (for Chief: the whole tree) with `id`, `name`, `role`, `parent`, `depth`, `path`, `children`, `lead` and `via` (the caller's direct child on the path, whom it delegates to). With a query, only matches, best first: same role (`sameRole`), the query in the name or role, shared role words (stems: "test" finds "Tester"), the same project, the same domain. Empty query: an overview (first 100). `create_child_bot` checks the whole branch first: a Bot with the same role at any depth is returned as `{ reused, note }` and nothing is created. A Bot outside the caller's branch is not looked at (the daily tidy merges duplicates across branches).
- **Reuse before create.** `find_bots(query?)` (read-only, every tree Bot) lists the Bots in the caller's branch (for Chief: the whole tree) with `id`, `name`, `role`, `parent`, `depth`, `path`, `children`, `load` (its live open turns, like `list_children`, #508) `lead` and `via` (the caller's direct child on the path, whom it delegates to). With a query, only matches, best first: same role (`sameRole`), the query in the name or role, shared role words (stems: "test" finds "Tester"), the same project, the same domain. Empty query: an overview (first 100). `create_child_bot` checks the whole branch first: a Bot with the same role at any depth is returned as `{ reused, note }` and nothing is created. A Bot outside the caller's branch is not looked at (the daily tidy merges duplicates across branches).
- **Placement-aware creation.** `create_child_bot(name, role_description, parent?)`: `parent` (id or unique name) must be the caller or a descendant, else `parent must be you or one of your descendants`. Limits, depth and the sibling-name check apply at the target, under that parent's lock. Without `parent`, a **marked group lead** in the caller's branch whose group the new Bot belongs to takes it (most specific group first, then the shallower lead, when it has room): "FlyBot Docs" goes under "FlyBot Lead". A Bot that could lead that group itself, or a namesake, never goes under it; with no such lead the caller is the parent. The result names the parent and the tree path.
- **Teams: lead plus children.** `create_team(team, members, parent?)` with `members` as `"Name: role"` strings (2 to `maxChildren`): `"<team> Lead"` (role "Lead of the <team> group: coordinates …", `lead { key, label }` from `roles.js`) under the caller or `parent`, and the members under it: `Chief → FlyBot Lead → {Dev, Test, Maintenance, Repo}`, one slot under Chief. An existing lead of that group in the caller's branch (marked for it, named "<team>" / "<team> Lead", or coordinating it) is reused and marked; a member with the same role already in the branch is reused, and moved under the lead when it sits flat next to it (that frees its slot, so a team fits at a full level when it reuses a flat member). Members that do not fit are listed as not created. A team that moves or marks existing Bots is a journaled run (trigger `team`, one-click undo); a fresh team is a plain create.
- **Plan card**: not built. The codebase has no chat card for a model-proposed plan (approval cards belong to social's message holds), so forward allocation proceeds and names the resulting paths; deferral recorded on #447. Automatic reorganization never asks first.
Expand Down
4 changes: 2 additions & 2 deletions plugins/tree/src/allocate.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const MAX_TEAM_LENGTH = MAX_NAME_LENGTH - LEAD_SUFFIX.length
const MAX_FOUND = 30
const MAX_OVERVIEW = 100

export function createAllocator({ fb, limits, snapshot, childrenOf, depthOf, traceOf, shared, createUnder, withParentLock, refreshAll, classify }) {
export function createAllocator({ fb, limits, snapshot, childrenOf, depthOf, traceOf, shared, createUnder, withParentLock, refreshAll, classify, loadOf = () => 0 }) {
const { profile, subtreeOf, heightOf, fits, homesFor, createLead, markLead, move, begin, commit, exclusive } = shared
const get = (id, key) => fb.getField(id, key)
const profileFor = (name, role) => profileOf({ name, treeRole: role }, classify)
Expand Down Expand Up @@ -53,7 +53,7 @@ export function createAllocator({ fb, limits, snapshot, childrenOf, depthOf, tra
const lead = get(id, 'lead')
return {
id, name: n.name, role: n.role, depth: depthOf(nodes, id), path: traceOf(nodes, id),
parent: nodes[n.parent]?.name ?? null, children: childrenOf(nodes, id).length,
parent: nodes[n.parent]?.name ?? null, children: childrenOf(nodes, id).length, load: loadOf(id),
...(lead?.label ? { lead: lead.label } : {}), ...(via && via.id !== id ? { via: via.name } : {}),
}
}
Expand Down
2 changes: 1 addition & 1 deletion plugins/tree/src/tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export function createTree(fb, { limits, rootName = 'Root', rootBotId, adoptExis
const findRootId = () => { const nodes = snapshot(); return Object.values(nodes).find(n => n.root)?.id ?? Object.values(nodes).find(n => n.parent === null)?.id }
const organizer = createOrganizer({ fb, limits, snapshot, childrenOf, depthOf, warn, refreshAll: () => refreshAll(), writeBlock: (id, block) => writeBlock(id, block), findRootId, now, classify, tidy })
// Forward allocation (#447) on the organizer's shared primitives: find_bots, reuse, parent?, teams.
const allocator = createAllocator({ fb, limits, snapshot, childrenOf, depthOf, traceOf, shared: organizer.shared, createUnder: (parentId, name, role) => createChildNow(parentId, name, role), withParentLock: (id, fn) => withParentLock(id, fn), refreshAll: () => refreshAll(), classify })
const allocator = createAllocator({ fb, limits, snapshot, childrenOf, depthOf, traceOf, shared: organizer.shared, createUnder: (parentId, name, role) => createChildNow(parentId, name, role), withParentLock: (id, fn) => withParentLock(id, fn), refreshAll: () => refreshAll(), classify, loadOf: id => load.of(id) })

// ---------------------------------------------------------------- root
/** The Main Bot (Chief): config.rootBotId if it exists, else the Main Bot named 'Chief', else the first Main Bot. */
Expand Down
14 changes: 13 additions & 1 deletion plugins/tree/test/tree.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1296,7 +1296,7 @@ test('#447: create_child_bot reuses a Bot with the same role at depth 3 instead
// find_bots: the root sees the whole tree; a match at depth 3 comes first.
const found = await m.call(m.root, 'find_bots', { query: 'tester' })
assert.equal(found.scope, 'the whole tree')
assert.deepEqual({ ...found.bots[0], role: undefined }, { id: tester, name: 'Tester', role: undefined, depth: 3, path: 'Chief → Engineering → Tester', parent: 'Engineering', children: 0, via: 'Engineering', match: 'same role' })
assert.deepEqual({ ...found.bots[0], role: undefined }, { id: tester, name: 'Tester', role: undefined, depth: 3, path: 'Chief → Engineering → Tester', parent: 'Engineering', children: 0, load: 0, via: 'Engineering', match: 'same role' })
assert.equal(found.bots[0].role, 'Runs the test suite and reports failures')
assert.ok((await m.call(m.root, 'find_bots', { query: 'test' })).bots.some(b => b.id === tester), 'a stem of a role word matches')
const all = await m.call(m.root, 'find_bots', {})
Expand Down Expand Up @@ -1458,3 +1458,15 @@ test('#450: a merge carries the archived Bot\'s own memory into the kept Bot', a
assert.ok(moved, JSON.stringify(kept))
assert.equal(moved.meta.source, 'merged')
})

test('#508 follow-up: find_bots reports each Bot\'s runtime load', async () => {
const m = await setup()
const work = await make(m, m.root, 'Work')
m.feed('turn.start', { botId: work, place: 'chat', sessionId: 's', turnId: 't1', turn: 1, time: 1 })
m.feed('turn.start', { botId: work, place: 'chat', sessionId: 's', turnId: 't2', turn: 2, time: 2 })
const found = await m.call(m.root, 'find_bots', {})
assert.equal(found.bots.find(bot => bot.name === 'Work').load, 2)
// A search result carries the same field.
const one = await m.call(m.root, 'find_bots', { query: 'Work' })
assert.equal(one.bots.find(bot => bot.name === 'Work').load, 2)
})
Loading