feat: enhance task management and timeout configurations in multi-agent chat

- Added new environment variables for controlling task execution limits, including `SURFSENSE_SUBAGENT_INVOKE_TIMEOUT_SECONDS`, `SURFSENSE_TASK_BATCH_CONCURRENCY`, and `SURFSENSE_TASK_BATCH_MAX_SIZE`.
- Updated documentation to reflect new batch processing capabilities for `task` calls, allowing for concurrent execution of multiple subagent tasks.
- Improved error handling and receipt generation for deliverables, ensuring consistent feedback on task status.
- Refactored middleware to incorporate search space ID for better task management.
This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-05-27 14:58:10 -07:00
parent 820f541f08
commit 9d6e9b7e2d
66 changed files with 2561 additions and 380 deletions

View file

@ -6,6 +6,9 @@ import json
from collections.abc import Iterator
from typing import Any
from langchain_core.messages import ToolMessage
from langgraph.types import Command
from app.tasks.chat.streaming.handlers.tools import (
ToolCompletionEmissionContext,
iter_tool_completion_emission_frames,
@ -19,6 +22,38 @@ from app.tasks.chat.streaming.relay.task_span import (
from app.tasks.chat.streaming.relay.thinking_step_sse import emit_thinking_step_frame
def _unwrap_command_output(raw_output: Any) -> Any:
"""Replace a ``Command`` from a tool return with its inner ``ToolMessage``.
Tools that participate in receipt-style state writes (see
``app.agents.shared.receipt_command.with_receipt``) return a
``Command(update={"messages": [ToolMessage(...)], "receipts": [...]})``.
LangChain's ``on_tool_end`` event surfaces that ``Command`` verbatim as
``data.output``, which the rest of this handler can't introspect: it has
no ``.content``, isn't a ``dict``, and stringifies to ``"Command(...)"``.
That stringified payload reaches the frontend and breaks tool-specific
UI components (e.g. the podcast card) that look for ``status`` /
``podcast_id`` at the top level.
We extract the first ``ToolMessage`` from the Command's ``messages`` list
so downstream code can read ``.content`` normally. Commands that don't
contain a ``ToolMessage`` (rare, e.g. pure state updates) are returned
unchanged the existing ``str(raw_output)`` fallback handles them.
"""
if not isinstance(raw_output, Command):
return raw_output
update = raw_output.update
if not isinstance(update, dict):
return raw_output
messages = update.get("messages")
if not isinstance(messages, list):
return raw_output
for msg in messages:
if isinstance(msg, ToolMessage):
return msg
return raw_output
def iter_tool_end_frames(
event: dict[str, Any],
*,
@ -33,7 +68,7 @@ def iter_tool_end_frames(
state.active_tool_depth = max(0, state.active_tool_depth - 1)
run_id = event.get("run_id", "")
tool_name = event.get("name", "unknown_tool")
raw_output = event.get("data", {}).get("output", "")
raw_output = _unwrap_command_output(event.get("data", {}).get("output", ""))
staged_file_path = state.file_path_by_run.pop(run_id, None) if run_id else None
if hasattr(raw_output, "content"):

View file

@ -15,12 +15,24 @@ def iter_completion_emission_frames(
out = ctx.tool_output
payload = out if isinstance(out, dict) else {"result": out}
yield ctx.emit_tool_output_card(payload)
if isinstance(out, dict) and out.get("status") == "pending":
if not isinstance(out, dict):
return
status = out.get("status")
# ``ready`` is the live success status now that the tool waits for the
# Celery worker to reach a terminal state. ``pending`` is retained as a
# legacy branch for old saved chats that pre-date the wait-for-terminal
# change (see ``app.agents.shared.deliverable_wait``).
if status == "ready":
yield ctx.streaming_service.format_terminal_info(
f"Video presentation generated successfully: {out.get('title', 'Presentation')}",
"success",
)
elif status == "pending":
yield ctx.streaming_service.format_terminal_info(
f"Video presentation queued: {out.get('title', 'Presentation')}",
"success",
)
elif isinstance(out, dict) and out.get("status") == "failed":
elif status == "failed":
error_msg = out.get("error", "Unknown error")
yield ctx.streaming_service.format_terminal_info(
f"Presentation generation failed: {error_msg}",