openclaw/.cursor/plans/interactive_subagent_panel_fb79f5b8.plan.md
kumarabhirup 5b41523f17
feat: add CLI-only streaming hardening and interactive subagent panel plans
- Introduced a new plan for CLI-only streaming hardening, focusing on protocol-level improvements and the removal of web WS clients.
- Added a plan for an interactive subagent panel, enabling subagents to operate independently from the parent agent's event stream, with unified API routes for enhanced interactivity.
2026-02-21 14:52:27 -08:00

6.4 KiB

name overview todos isProject
Interactive Subagent Panel Make subagents fully independent of the parent agent's event stream. Each subagent gets its own gateway subscription immediately on registration. Then make the subagent panel interactive with stop, send, queue matching the main chat, using unified API routes.
id content status
decouple-subagent SubagentRunManager: subscribe immediately on registration, remove routeRawEvent/preRegBuffer/activateGatewayFallback pending
id content status
remove-parent-routing active-runs.ts: remove subagent event routing from parent NDJSON stream pending
id content status
srm-methods SubagentRunManager: add persistUserMessage(), reactivateSubagent(), abortSubagent(), spawnSubagentMessage() pending
id content status
unify-chat-route Extend POST /api/chat to dispatch to subagent flow when sessionKey is a subagent key pending
id content status
unify-stop-route Extend POST /api/chat/stop to dispatch to SubagentRunManager when sessionKey is a subagent key pending
id content status
unify-stream-route Extend GET /api/chat/stream to dispatch to SubagentRunManager when sessionKey is a subagent key pending
id content status
parser-turns Extend createStreamParser to handle user-message events as turn boundaries pending
id content status
panel-rewrite Rewrite SubagentPanel with ChatEditor, send/stop/queue, multi-turn conversation pending
false

Interactive Subagent Panel

Core Problem

Subagent events piggyback on the parent agent's CLI NDJSON stream. When the parent finishes (spawns subagents then exits), the stream dies and subagent events stop flowing. The activateGatewayFallback() partially compensates but loses early events.

The root cause is architectural: subagents are treated as appendages of the parent. They should be independent sessions.

Architecture Change

A subagent is just an agent session. The only link to the parent is the completion announcement. Each subagent gets its own gateway subscription from the moment it's registered.

flowchart TB
    subgraph before [Current: Coupled]
        GW1[Gateway] --> ParentCLI[Parent CLI stdout]
        ParentCLI --> ARM1[ActiveRunManager]
        ParentCLI -.->|"routeRawEvent<br/>(filtered by runId, never arrives)"| SRM1[SubagentRunManager]
        ARM1 -.->|"activateGatewayFallback<br/>(after parent exits, loses early events)"| SRM1
    end

    subgraph after [New: Independent]
        GW2[Gateway] --> ParentCLI2[Parent CLI stdout]
        GW2 --> SubProc[Subscribe Process per subagent]
        ParentCLI2 --> ARM2[ActiveRunManager]
        SubProc --> SRM2[SubagentRunManager]
    end

Phase 1: Decouple Subagents

1. SubagentRunManager (subagent-runs.ts)

In registerSubagent() (line 266-270): replace the comment with:

if (run.status === "running") {
  startSubagentSubscribeStream(run);
}

Each subagent immediately gets its own subscribe process (spawnAgentSubscribeProcess) that connects to the gateway and streams events for that subagent's sessionKey. No dependency on the parent's stream.

Remove dead code:

  • routeRawEvent() (lines 419-448) -- no longer called; events come from per-subagent subscribe processes
  • preRegBuffer from the registry type and getRegistry() -- no pre-registration buffering needed; the subscribe process handles everything
  • activateGatewayFallback() (lines 368-375) -- no longer needed; subscription starts at registration time

2. active-runs.ts (active-runs.ts)

Remove subagent event routing from the parent NDJSON handler: the block that checks ev.sessionKey !== parentSessionKey and calls routeSubagentEvent() -- delete it entirely. Parent NDJSON stream now only processes parent events. No imports of routeRawEvent, ensureRegisteredFromDisk, hasActiveSubagent from subagent-runs needed for routing.

Remove activateGatewayFallback() call from the parent exit handler.

Keep: the waiting-for-subagents state transition and hasRunningSubagentsForParent() check -- the parent still needs to know when all subagents finish so it can finalize.

3. No CLI changes needed

The runId filter in src/commands/agent.ts is correct -- the parent's NDJSON stream should only contain parent events. Subagent events flow independently through their own subscribe processes.

Phase 2: Unified API Routes

Same primitive, same routes. Dispatch based on session key format (:subagent: vs :web:).

4. SubagentRunManager: interactive methods

  • **persistUserMessage(sessionKey, msg)** -- append {type: "user-message", text, id} to event buffer + JSONL
  • **reactivateSubagent(sessionKey)** -- set status to "running", clear endedAt, restart subscribe process
  • **abortSubagent(sessionKey)** -- spawn CLI gateway call chat.abort, mark "error", signal subscribers
  • **spawnSubagentMessage(sessionKey, message)** -- spawn CLI gateway call agent --params '{"message":"...", "sessionKey":"...", "lane":"subagent", ...}'

5. Extend POST /api/chat (route.ts)

If sessionKey contains :subagent::

  • Reject if running (409)
  • persistUserMessage() + reactivateSubagent() + spawnSubagentMessage()
  • Subscribe via subscribeToSubagent(sessionKey, ..., { replay: false }) for SSE response

Otherwise: existing parent flow.

6. Extend POST /api/chat/stop (stop/route.ts)

Accept sessionKey. If :subagent:: abortSubagent(). Otherwise: abortRun().

7. Extend GET /api/chat/stream (stream/route.ts)

Accept sessionKey. If :subagent:: lazy-register from disk, ensureSubagentStreamable(), subscribeToSubagent(). Otherwise: existing parent flow.

Remove apps/web/app/api/chat/subagent-stream/route.ts after migration.

Phase 3: Frontend

8. Stream parser turn boundaries (chat-panel.tsx)

Add user-message to ParsedPart and createStreamParser for multi-turn subagent conversations.

9. Rewrite SubagentPanel (subagent-panel.tsx)

Full ChatPanel-like experience: ChatEditor, send/stop/queue buttons, AttachmentStrip, message queue, auto-scroll. Uses the unified routes with sessionKey.