deep agent
This commit is contained in:
@@ -200,6 +200,9 @@ async def _message_loop(websocket: WebSocket, user_id: str) -> None:
|
||||
|
||||
# ── v3 Chat Handlers ──────────────────────────────────────────────────
|
||||
|
||||
_WS_TOOL_CALL_TIMEOUT = 30 # seconds to wait for Electron tool_result
|
||||
|
||||
|
||||
async def _make_ws_executor(websocket: WebSocket, user_id: str):
|
||||
"""Return a callback that sends tool_call frames and awaits tool_result."""
|
||||
async def _executor(payload: dict) -> dict:
|
||||
@@ -208,7 +211,18 @@ async def _make_ws_executor(websocket: WebSocket, user_id: str):
|
||||
logger.info("ws_executor: sending tool_call id=%s action=%s", call_id, payload.get("action"))
|
||||
await websocket.send_text(json.dumps(payload))
|
||||
future = device_manager.create_pending_call(user_id, call_id)
|
||||
result = await future
|
||||
try:
|
||||
result = await asyncio.wait_for(future, timeout=_WS_TOOL_CALL_TIMEOUT)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(
|
||||
"ws_executor: timeout waiting for tool_result id=%s action=%s user=%s",
|
||||
call_id, payload.get("action"), user_id,
|
||||
)
|
||||
# Clean up the pending future so it doesn't leak
|
||||
conn = device_manager._connections.get(user_id)
|
||||
if conn:
|
||||
conn.pending_calls.pop(call_id, None)
|
||||
return {"error": f"Tool call timed out after {_WS_TOOL_CALL_TIMEOUT}s", "rows": []}
|
||||
logger.info("ws_executor: tool_result id=%s result_type=%s result_keys=%s",
|
||||
call_id, type(result).__name__,
|
||||
list(result.keys()) if isinstance(result, dict) else "N/A")
|
||||
|
||||
@@ -114,7 +114,8 @@ def _make_subagent_specs() -> list[dict[str, Any]]:
|
||||
"name": "task_agent",
|
||||
"description": (
|
||||
"Manages tasks and comments: list, create, update, delete, "
|
||||
"due-today, comments. Delegate task-related queries here."
|
||||
"due-today, and comments. Use when the user asks about tasks, "
|
||||
"to-dos, assignments, deadlines, or anything task-related."
|
||||
),
|
||||
"system_prompt": (
|
||||
"You are a task management assistant. You create, update, list, "
|
||||
@@ -128,14 +129,13 @@ def _make_subagent_specs() -> list[dict[str, Any]]:
|
||||
" - For update_task, use -1 for integer fields you do not want to change\n"
|
||||
" - Always confirm the action in plain, user-friendly language."
|
||||
),
|
||||
"tools": _TASK_TOOLS,
|
||||
"model": llm,
|
||||
"tools": _TASK_TOOLS
|
||||
},
|
||||
{
|
||||
"name": "note_agent",
|
||||
"description": (
|
||||
"Manages notes: list, get, create, update, delete. "
|
||||
"Delegate note-related queries here."
|
||||
"Use when the user asks about notes, documents, or written content."
|
||||
),
|
||||
"system_prompt": (
|
||||
"You are a note-taking assistant. You help users create, retrieve, "
|
||||
@@ -146,14 +146,13 @@ def _make_subagent_specs() -> list[dict[str, Any]]:
|
||||
"content before appending or replacing sections\n"
|
||||
" - Do not fabricate note content."
|
||||
),
|
||||
"tools": _NOTE_TOOLS,
|
||||
"model": llm,
|
||||
"tools": _NOTE_TOOLS
|
||||
},
|
||||
{
|
||||
"name": "project_agent",
|
||||
"description": (
|
||||
"Manages projects: list, get, create, update, archive, delete. "
|
||||
"Delegate project-related queries here."
|
||||
"Use when the user asks about projects, workspaces, or project status."
|
||||
),
|
||||
"system_prompt": (
|
||||
"You are a project management assistant. You help users create, "
|
||||
@@ -163,14 +162,14 @@ def _make_subagent_specs() -> list[dict[str, Any]]:
|
||||
" - Prefer archiving over deletion\n"
|
||||
" - ai_summary is populated only when the user asks for a summary."
|
||||
),
|
||||
"tools": _PROJECT_TOOLS,
|
||||
"model": llm,
|
||||
"tools": _PROJECT_TOOLS
|
||||
},
|
||||
{
|
||||
"name": "timeline_agent",
|
||||
"description": (
|
||||
"Manages project timelines (milestones): list, create, update, "
|
||||
"delete. Delegate timeline/milestone queries here."
|
||||
"Manages project timelines and milestones: list, create, update, "
|
||||
"delete. Use when the user asks about timelines, milestones, "
|
||||
"deadlines, or project scheduling."
|
||||
),
|
||||
"system_prompt": (
|
||||
"You are a project timeline assistant. Timelines are milestone "
|
||||
@@ -181,8 +180,7 @@ def _make_subagent_specs() -> list[dict[str, Any]]:
|
||||
" - For update_timeline, use -1 for integer fields you do not "
|
||||
"want to change."
|
||||
),
|
||||
"tools": _TIMELINE_TOOLS,
|
||||
"model": llm,
|
||||
"tools": _TIMELINE_TOOLS
|
||||
},
|
||||
]
|
||||
|
||||
@@ -221,6 +219,11 @@ _HOME_SYSTEM = (
|
||||
"multiple sub-agents in parallel if needed.\n\n"
|
||||
"You also have an update_core_memory tool — use it when the user states "
|
||||
"a preference or important fact worth remembering long-term.\n\n"
|
||||
"IMPORTANT: You do NOT have direct access to workspace data. Always "
|
||||
"delegate to your subagents using the task() tool. Do not attempt to "
|
||||
"answer workspace queries yourself — the subagents have the tools to "
|
||||
"fetch and modify data. You can call multiple subagents in parallel "
|
||||
"when the request spans multiple domains.\n\n"
|
||||
"## Entity References\n"
|
||||
"When your response mentions specific workspace entities, embed them "
|
||||
"inline using entity tags so the UI can render interactive components.\n"
|
||||
@@ -263,6 +266,10 @@ _FLOATING_SYSTEM = (
|
||||
"if the request requires it.\n\n"
|
||||
"You also have an update_core_memory tool — use it when the user states "
|
||||
"a preference or important fact worth remembering long-term.\n\n"
|
||||
"IMPORTANT: You do NOT have direct access to workspace data. Always "
|
||||
"delegate to your subagents using the task() tool. Do not attempt to "
|
||||
"answer workspace queries yourself — the subagents have the tools to "
|
||||
"fetch and modify data.\n\n"
|
||||
"Provide direct, conversational responses.\n\n"
|
||||
"Memory context:\n{memory_context}"
|
||||
)
|
||||
@@ -367,6 +374,42 @@ async def _run_graph_stream(
|
||||
):
|
||||
if stream_mode == "messages":
|
||||
msg, metadata = chunk
|
||||
agent_name = (
|
||||
metadata.get("lc_agent_name", "?")
|
||||
if isinstance(metadata, dict) else "?"
|
||||
)
|
||||
node = (
|
||||
metadata.get("langgraph_node", "?")
|
||||
if isinstance(metadata, dict) else "?"
|
||||
)
|
||||
|
||||
# Log every message event with agent attribution
|
||||
if isinstance(msg, (AIMessage, AIMessageChunk)) and msg.content:
|
||||
logger.info(
|
||||
"[%s] %s node=%s content=%s",
|
||||
agent_name,
|
||||
type(msg).__name__,
|
||||
node,
|
||||
str(msg.content),
|
||||
)
|
||||
elif isinstance(msg, (AIMessage, AIMessageChunk)) and msg.tool_calls:
|
||||
tool_names = [tc["name"] for tc in msg.tool_calls]
|
||||
logger.info(
|
||||
"[%s] %s node=%s tool_calls=%s",
|
||||
agent_name,
|
||||
type(msg).__name__,
|
||||
node,
|
||||
tool_names,
|
||||
)
|
||||
elif hasattr(msg, "name") and hasattr(msg, "content") and msg.content:
|
||||
# ToolMessage — log tool result
|
||||
logger.info(
|
||||
"[%s] ToolMessage tool=%s node=%s result=%s",
|
||||
agent_name,
|
||||
getattr(msg, "name", "?"),
|
||||
node,
|
||||
str(msg.content),
|
||||
)
|
||||
# Only yield tokens from the supervisor's final response
|
||||
# (not from sub-agent internal LLM calls).
|
||||
# Accept both AIMessageChunk (streamed tokens) and AIMessage
|
||||
|
||||
Reference in New Issue
Block a user