feat: multi-level agent management — background agents, lifecycle tools, 3-level hierarchy

agent_manager.py (new):
- AgentRecord dataclass: agent_id, level (1/2/3), role, task, status, started,
  parent_id (lineage), finished, result, notify, _task_ref
- register() / finish() / cancel_agent() / list_agents() / get() / set_task_ref()
- Calls notification.notify() on completion when notify=True (same channel as
  reminders and cron completions)
- 24-hour pruning of completed records on each new registration

spawn_agent (tools/agents.py):
- background=True: fires asyncio.create_task(), registers in agent_manager, returns
  agent_id string immediately — sync path unchanged (no regression)
- notify=True: push/Talk notification when the background task completes
- Level enforcement: _agent_level param tracks hierarchy depth; when spawning from
  Level 2, child automatically gets spawn_agent + aider_run denied so Level 3 agents
  cannot delegate further

New lifecycle tools (tools/agents.py + __init__.py):
- agent_status(agent_id) — status, role, level, elapsed, task, result preview; user-level
- agent_list(status, limit) — all agents for current user, newest first; user-level
- agent_cancel(agent_id) — kills background task; admin-only, confirm-required

tests/test_agent_manager.py (new, 41 tests):
- agent_manager CRUD, pruning, notification hook
- spawn_agent background: returns immediately, completes async, timeout, failure
- Level enforcement: L1→L2 permits spawn, L2→L3 auto-denies; explicit tool_list path
- agent_status / agent_list / agent_cancel output formatting
- aider_run background: returns agent_id, completes async, sync path unchanged
- All tests run without browser or Cortex service (~2.5s total)
  Run: cd cortex && .venv/bin/python -m pytest tests/test_agent_manager.py -v

Docs: ARCH__FUTURE.md §13 (full design), ROADMAP.md, TODO__Agents.md, MASTER.md,
HELP.md (orchestrator description corrected, tool schema line updated to reflect
keyword routing), CLAUDE.md tool count 66→69.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-06-03 22:40:20 -04:00
parent 29d8aa4aae
commit 658c508925
9 changed files with 1307 additions and 26 deletions

View File

@@ -317,6 +317,149 @@ This pattern maps naturally to several existing concepts:
---
## 13. Multi-Level Agent Management
**Status:** Design complete — implementation not yet started. See `TODO__Agents.md` for the task breakdown.
Cortex personas can spawn specialized sub-agents to handle parallel or long-running work.
Sub-agents can in turn spawn lightweight support agents for simple subtasks. The hierarchy
is capped at three levels to prevent runaway delegation.
### Level Definitions
| Level | Name | Created by | Can spawn | Tool scope |
|---|---|---|---|---|
| **1** | Cortex Persona (Inara) | HTTP request / cron | Level 2 | Full orchestrator tool set |
| **2** | Specialized Sub-Agent | Level 1 `spawn_agent` | Level 3 only | Role-scoped; `spawn_agent` auto-restricted so children are Level 3 |
| **3** | Basic Support Agent | Level 2 `spawn_agent` | Nothing | Narrow tool set; `spawn_agent` and `aider_run` denied |
**Examples:**
- Level 1 spawns a Level 2 **Coder** agent (has file + git + shell tools; can spawn a Level 3 syntax-checker)
- Level 1 spawns a Level 2 **Research** agent (web tools only; can spawn a Level 3 web reader for parallel page fetches)
- Level 2 spawns a Level 3 **Support** agent for a focused subtask (web_search only, no writes, no further delegation)
### Core Problem: Everything is Currently Synchronous
Both `spawn_agent` and `aider_run` block the calling coroutine for their full duration
(default 120s / 300s respectively). Level 1 (Inara) cannot respond to the user, send
notifications, or inspect other agents while waiting. For 5-minute Aider runs or multi-step
research agents this is unusable — the user sees nothing until completion or timeout.
### Design
#### 1. Agent Manager (`cortex/agent_manager.py`)
A lightweight in-process registry of running and recently completed agents. Module-level
dict protected by `asyncio.Lock()`:
```python
@dataclass
class AgentRecord:
agent_id: str # UUID
level: int # 1 / 2 / 3
role: str # e.g. "coder", "research"
task: str # first 200 chars of the task
status: str # running / done / failed / cancelled / timeout
started: datetime
finished: datetime | None
parent_id: str | None # lineage — which agent spawned this one
result: str | None # populated on completion (first 500 chars)
notify: bool # fire web_push/NC Talk notification on completion
user: str
_agents: dict[str, AgentRecord] = {}
_lock = asyncio.Lock()
```
On completion, the manager calls `notification.py notify()` if `notify=True` — the same
function used by reminder checks and cron completions. Completed agents stay in the
registry for 24 hours then are pruned on next access.
#### 2. Background Mode for `spawn_agent`
Add `background: bool = False` and `notify: bool = False` to `spawn_agent`. When
`background=False` (default): existing synchronous blocking behaviour — unchanged, no
regression. When `background=True`: wraps the run in `asyncio.create_task()`, registers
in the agent manager, returns an `agent_id` string immediately.
```python
# Level 1 — non-blocking delegation:
agent_id = await spawn_agent(
task="Research Zigbee mesh repeaters; summarize findings to my journal",
role="research",
background=True,
notify=True, # web_push + NC Talk when done
)
# Returns "550e8400-..." immediately. Inara continues responding to the user.
```
#### 3. Agent Lifecycle Tools
Three new tools, wired into `cortex/tools/__init__.py` under the "Agents" category:
| Tool | Params | Description |
|---|---|---|
| `agent_status(agent_id)` | `agent_id: str` | Status, role, task, elapsed, result preview |
| `agent_list(status=None, limit=10)` | `status: str \| None` | All agents for current user; filter by status |
| `agent_cancel(agent_id)` | `agent_id: str` | Cancel a running background agent (admin, confirm-required) |
Level 1 can call these between tool rounds to check on delegated work without blocking.
#### 4. Level Enforcement
`agent_level` is passed through `spawn_agent` calls as a ContextVar so each agent knows
where it sits in the hierarchy. Enforcement is automatic and simple:
- **L1 → spawns L2:** `spawn_agent` called normally. Child agent inherits role tools.
- **L2 → spawns L3:** `spawn_agent` automatically adds `deny_tools=["spawn_agent", "aider_run"]`
to the child's effective tool set. Level 3 agents cannot further delegate.
- **Level 3:** `spawn_agent` and `aider_run` are never in the tool list.
Level is stored in `AgentRecord.level` — the lineage (`parent_id`) provides a full call tree.
#### 5. `aider_run` Background Mode
Add `background: bool = False` and `notify: bool = False` to `aider_run`. When `True`,
runs the Aider subprocess via `asyncio.create_task()`, registers in the agent manager,
returns `agent_id` immediately. When called in background mode, `aider_run` is removed
from `CONFIRM_REQUIRED` — the user is not blocking on a confirmation gate since the call
returns instantly.
```python
# Level 1 or 2 — fire and forget a code change:
agent_id = await aider_run(
project="cortex",
task="Add max_chars param to http_fetch in tools/web.py, cap at 32768",
background=True,
notify=True,
)
```
### Implementation Order
1. **`agent_manager.py`** — AgentRecord + registry CRUD + completion notification hook.
Foundation for everything else; ~100 lines.
2. **`spawn_agent` background mode** — `background` + `notify` + `agent_level` params;
`asyncio.create_task()`; registers in manager. Existing sync path unchanged.
3. **`agent_status` / `agent_list` / `agent_cancel`** — wire into `__init__.py`; add to
`TOOL_CATEGORIES["Agents"]`, `TOOL_ROLES` (cancel = admin), `CONFIRM_REQUIRED` (cancel).
4. **Level enforcement**`agent_level` ContextVar; auto `deny_tools` at L2→L3 boundary.
5. **`aider_run` background mode** — same pattern as step 2.
### Files to Create/Modify
| File | Change |
|---|---|
| `cortex/agent_manager.py` | **New** — AgentRecord, registry dict, start/finish/cancel/list functions |
| `cortex/tools/agents.py` | Add `background`, `notify`, `agent_level` to `spawn_agent`; add `agent_status`, `agent_list`, `agent_cancel` functions + declarations |
| `cortex/tools/aider.py` | Add `background`, `notify` params; register with agent_manager when background |
| `cortex/tools/__init__.py` | Register new agent tools; update TOOL_CATEGORIES, TOOL_ROLES, CONFIRM_REQUIRED |
See §12 for the existing `allow_tools` / `deny_tools` per-call restrictions that level
enforcement builds on.
---
## 12. Spawner-Level Tool Restrictions — `spawn_agent` Permission Control
**Status:** Design complete, not yet built.