Add task brief research agent: Stage 1 deep-research + canvas draft emission
- run_task_brief_research() runner with brief-specific tool set and max_steps=12 - New agents: client_agent (list_clients, get_client) and relations_agent (query_relations) - search_associative tool wrapping MemoryMiddleware semantic search - BRIEF_RESEARCH_TOOLS constant: read-only task/project/note/timeline + memory + client/relations - canvas block extraction in output_formatter (splits visible text from <canvas> draft) - device_ws.py: task_brief_research request type; emits canvas_draft mutation on stream_end - Stage 2 briefMode: briefing_context injected into floating system prompt when present - briefingContext kwarg wired through compile_prompt call chain Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -43,7 +43,8 @@ from app.api.routes.agent_setup import handle_journey_message, handle_journey_st
|
||||
from app.config.settings import settings
|
||||
from app.core.agent_runner import trigger_pending_runs
|
||||
from app.core.brief_agent import run_home_brief, run_project_brief
|
||||
from app.core.deep_agent import run_floating_stream, run_home_stream
|
||||
from app.core.deep_agent import run_floating_stream, run_home_stream, run_task_brief_research_stream
|
||||
from app.core.output_formatter import extract_canvas_block
|
||||
from app.core.device_manager import device_manager
|
||||
from app.core.memory_middleware import MemoryMiddleware
|
||||
from app.core.output_formatter import StreamFormatter
|
||||
@@ -164,6 +165,11 @@ async def _message_loop(websocket: WebSocket, user_id: str) -> None:
|
||||
_handle_brief_request(websocket, user_id, frame)
|
||||
)
|
||||
|
||||
elif frame_type == WsFrameType.task_brief_request:
|
||||
asyncio.create_task(
|
||||
_handle_task_brief_request(websocket, user_id, frame)
|
||||
)
|
||||
|
||||
elif frame_type == WsFrameType.journey_start:
|
||||
asyncio.create_task(
|
||||
_handle_journey_start(websocket, user_id, frame)
|
||||
@@ -415,6 +421,97 @@ async def _handle_brief_request(
|
||||
)
|
||||
|
||||
|
||||
# ── v6 Task Brief Handler ────────────────────────────────────────────
|
||||
|
||||
|
||||
async def _handle_task_brief_request(
|
||||
websocket: WebSocket,
|
||||
user_id: str,
|
||||
frame: dict,
|
||||
) -> None:
|
||||
"""Handle a task_brief_request frame — Stage-1 executive assistant deep research.
|
||||
|
||||
Streams the briefing markdown back to the client.
|
||||
On stream_end, emits a ``canvas_draft`` mutation if the agent produced one.
|
||||
"""
|
||||
request_id = frame.get("request_id") or str(uuid4())
|
||||
session_id = frame.get("session_id") or str(uuid4())
|
||||
task_id: str = frame.get("task_id") or frame.get("taskId") or ""
|
||||
|
||||
logger.info(
|
||||
"device_ws: task_brief_request_start user=%s req=%s task=%s [cache_miss]",
|
||||
user_id, request_id, task_id,
|
||||
)
|
||||
|
||||
if not task_id:
|
||||
await websocket.send_text(
|
||||
WsStreamEnd(request_id=request_id, error="task_id is required").model_dump_json()
|
||||
)
|
||||
return
|
||||
|
||||
async with async_session() as db:
|
||||
memory = MemoryMiddleware(db)
|
||||
memory_context = await memory.enrich_context(
|
||||
user_id,
|
||||
f"task brief: {task_id}",
|
||||
trace_id=request_id,
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
context: dict = {
|
||||
"_debug": {"request_id": request_id, "session_id": session_id, "user_id": user_id},
|
||||
"format_prefs": frame.get("format_prefs"),
|
||||
**memory_context,
|
||||
}
|
||||
|
||||
executor = await _make_ws_executor(websocket, user_id)
|
||||
set_client_executor(executor)
|
||||
response_chunks: list[str] = []
|
||||
|
||||
try:
|
||||
event_stream = run_task_brief_research_stream(user_id, task_id, context)
|
||||
formatter = StreamFormatter(request_id=request_id)
|
||||
async for ws_frame in formatter.format(event_stream):
|
||||
if ws_frame.type == "stream_text": # type: ignore[union-attr]
|
||||
response_chunks.append(ws_frame.chunk) # type: ignore[union-attr]
|
||||
await websocket.send_text(ws_frame.model_dump_json())
|
||||
elif ws_frame.type == "stream_start":
|
||||
await websocket.send_text(ws_frame.model_dump_json())
|
||||
# stream_end is emitted below with mutations — skip formatter's version
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
"device_ws: task_brief_request failed user=%s req=%s task=%s: %s",
|
||||
user_id, request_id, task_id, exc,
|
||||
)
|
||||
await websocket.send_text(
|
||||
WsStreamEnd(request_id=request_id, error=str(exc)).model_dump_json()
|
||||
)
|
||||
return
|
||||
finally:
|
||||
clear_client_executor()
|
||||
|
||||
# Extract canvas block then emit stream_end with optional mutations.
|
||||
full_response = "".join(response_chunks)
|
||||
_visible, canvas_content, canvas_kind = extract_canvas_block(full_response)
|
||||
|
||||
mutations: list[dict] = []
|
||||
if canvas_content:
|
||||
mutations.append({
|
||||
"type": "canvas_draft",
|
||||
"content": canvas_content,
|
||||
"kind": canvas_kind,
|
||||
})
|
||||
|
||||
await websocket.send_text(
|
||||
WsStreamEnd(request_id=request_id, mutations=mutations or None).model_dump_json()
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"device_ws: task_brief_request_end user=%s req=%s task=%s response_chars=%d canvas=%s",
|
||||
user_id, request_id, task_id, len(full_response), canvas_kind or "none",
|
||||
)
|
||||
|
||||
|
||||
# ── v4 Journey Handlers ─────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user