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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user