mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-01 01:51:44 +00:00
Adds a live + post-hoc audit surface for recursive delegate_task fan-out. None of cc/oc/oclaw tackle nested subagent trees inside an Ink overlay; this ships a view-switched dashboard that handles arbitrary depth + width. Python - delegate_tool: every subagent event now carries subagent_id, parent_id, depth, model, tool_count; subagent.complete also ships input/output/ reasoning tokens, cost, api_calls, files_read/files_written, and a tail of tool-call outputs - delegate_tool: new subagent.spawn_requested event + _active_subagents registry so the overlay can kill a branch by id and pause new spawns - tui_gateway: new RPCs delegation.status, delegation.pause, subagent.interrupt, spawn_tree.save/list/load (disk under \$HERMES_HOME/spawn-trees/<session>/<ts>.json) TUI - /agents overlay: full-width list mode (gantt strip + row picker) and Enter-to-drill full-width scrollable detail mode; inverse+amber selection, heat-coloured branch markers, wall-clock gantt with tick ruler, per-branch rollups - Detail pane: collapsible accordions (Budget, Files, Tool calls, Output, Progress, Summary); open-state persists across agents + mode switches via a shared atom - /replay [N|last|list|load <path>] for in-memory + disk history; /replay-diff <a> <b> for side-by-side tree comparison - Status-bar SpawnHud warns as depth/concurrency approaches caps; overlay auto-follows the just-finished turn onto history[1] - Theme: bump DARK dim #B8860B → #CC9B1F for readable secondary text globally; keep LIGHT untouched Tests: +29 new subagentTree unit tests; 215/215 passing.
77 lines
2.5 KiB
TypeScript
77 lines
2.5 KiB
TypeScript
import { atom } from 'nanostores'
|
|
|
|
import type { DelegationStatusResponse } from '../gatewayTypes.js'
|
|
|
|
export interface DelegationState {
|
|
// Last known caps from `delegation.status` RPC. null until fetched.
|
|
maxConcurrentChildren: null | number
|
|
maxSpawnDepth: null | number
|
|
// True when spawning is globally paused (see tools/delegate_tool.py).
|
|
paused: boolean
|
|
// Monotonic clock of the last successful status fetch.
|
|
updatedAt: null | number
|
|
}
|
|
|
|
const buildState = (): DelegationState => ({
|
|
maxConcurrentChildren: null,
|
|
maxSpawnDepth: null,
|
|
paused: false,
|
|
updatedAt: null
|
|
})
|
|
|
|
export const $delegationState = atom<DelegationState>(buildState())
|
|
|
|
export const getDelegationState = () => $delegationState.get()
|
|
|
|
export const patchDelegationState = (next: Partial<DelegationState>) =>
|
|
$delegationState.set({ ...$delegationState.get(), ...next })
|
|
|
|
export const resetDelegationState = () => $delegationState.set(buildState())
|
|
|
|
// ── Overlay accordion open-state ──────────────────────────────────────
|
|
//
|
|
// Lifted out of OverlaySection's local useState so collapse choices
|
|
// survive:
|
|
// - navigating to a different subagent (Detail remounts)
|
|
// - switching list ↔ detail mode (Detail unmounts in list mode)
|
|
// - walking history (←/→)
|
|
// Keyed by section title; missing entries fall back to the section's
|
|
// `defaultOpen` prop.
|
|
|
|
export const $overlaySectionsOpen = atom<Record<string, boolean>>({})
|
|
|
|
export const toggleOverlaySection = (title: string, defaultOpen: boolean) => {
|
|
const state = $overlaySectionsOpen.get()
|
|
const current = title in state ? state[title]! : defaultOpen
|
|
|
|
$overlaySectionsOpen.set({ ...state, [title]: !current })
|
|
}
|
|
|
|
export const getOverlaySectionOpen = (title: string, defaultOpen: boolean): boolean => {
|
|
const state = $overlaySectionsOpen.get()
|
|
|
|
return title in state ? state[title]! : defaultOpen
|
|
}
|
|
|
|
/** Merge a raw RPC response into the store. Tolerant of partial/omitted fields. */
|
|
export const applyDelegationStatus = (r: DelegationStatusResponse | null | undefined) => {
|
|
if (!r) {
|
|
return
|
|
}
|
|
|
|
const patch: Partial<DelegationState> = { updatedAt: Date.now() }
|
|
|
|
if (typeof r.max_spawn_depth === 'number') {
|
|
patch.maxSpawnDepth = r.max_spawn_depth
|
|
}
|
|
|
|
if (typeof r.max_concurrent_children === 'number') {
|
|
patch.maxConcurrentChildren = r.max_concurrent_children
|
|
}
|
|
|
|
if (typeof r.paused === 'boolean') {
|
|
patch.paused = r.paused
|
|
}
|
|
|
|
patchDelegationState(patch)
|
|
}
|