feat: add streaming responses to API and CLI

Add real-time streaming support for agent responses using Server-Sent
Events (SSE). Responses now appear as they're generated instead of
waiting for completion.

- Add run_stream method to BaseAgent and ExploreAgentImpl
- Add /agents/stream SSE endpoint to API router
- Add run_agent_stream method to CLI client
- Add --stream flag to chat and explore commands (enabled by default)
- Use --no-stream for batch mode with spinner

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-11 12:58:54 +01:00
co-authored by Claude Opus 4.5
parent d0fa5b38a7
commit f6256363a2
5 changed files with 225 additions and 25 deletions
+49
View File
@@ -1,7 +1,9 @@
"""
REST API routes for agents.
"""
import json
from fastapi import APIRouter, HTTPException
from fastapi.responses import StreamingResponse
from src.domains.agents.base import get_agent, list_agents
@@ -68,6 +70,53 @@ async def run_agent(request: AgentRunRequest) -> AgentRunResponse:
)
@router.post("/stream")
@logged()
async def stream_agent(request: AgentRunRequest) -> StreamingResponse:
"""
Run an agent with streaming response.
Returns Server-Sent Events (SSE) with text chunks.
Event types:
- "chunk": Text chunk from the agent
- "done": Stream complete
- "error": Error occurred
"""
agent = get_agent(request.agent_type)
if not agent:
raise HTTPException(
status_code=400,
detail=f"Unknown agent type: {request.agent_type}"
)
async def generate():
try:
async for chunk in agent.run_stream(
request.prompt,
working_dir=request.working_dir,
):
# SSE format: data: {json}\n\n
event = {"event": "chunk", "data": chunk}
yield f"data: {json.dumps(event)}\n\n"
# Signal completion
yield f"data: {json.dumps({'event': 'done'})}\n\n"
except Exception as e:
logger.exception(f"Stream error: {e}")
error_event = {"event": "error", "data": str(e)}
yield f"data: {json.dumps(error_event)}\n\n"
return StreamingResponse(
generate(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
}
)
@router.get("/{agent_type}", response_model=AgentInfo)
async def get_agent_info(agent_type: str) -> AgentInfo:
"""Get information about a specific agent."""