- 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.
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. |
|
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 processespreRegBufferfrom the registry type andgetRegistry()-- no pre-registration buffering needed; the subscribe process handles everythingactivateGatewayFallback()(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", clearendedAt, restart subscribe process**abortSubagent(sessionKey)**-- spawn CLIgateway call chat.abort, mark"error", signal subscribers**spawnSubagentMessage(sessionKey, message)**-- spawn CLIgateway 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.