mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-11 14:38:38 +00:00
* feat(mcp): add reliable task notifications and cancellation * feat(mcp): add background task chat UI * fix(mcp): hide and sanitize task notification prompts * fix(mcp): sanitize projected task names * fix(mcp): harden task notifications and details * fix(mcp): harden task lifecycle recovery * fix(mcp): gate task UI and isolate cancellations * test: scope plain-text response locator * fix(mcp): align task notification boundaries * fix(mcp): bound task delivery retries * fix background task notification races
85 lines
3.1 KiB
Python
85 lines
3.1 KiB
Python
"""Natural-language management tools for the current thread's MCP tasks."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Annotated, Any
|
|
|
|
from langchain.tools import tool
|
|
|
|
from deerflow.agents.middlewares.input_sanitization_middleware import neutralize_untrusted_tags
|
|
from deerflow.mcp.tasks.runtime import get_mcp_task_submitter
|
|
from deerflow.tools.builtins.list_uploaded_files_tool import _resolve_thread_id, _resolve_user_id
|
|
from deerflow.tools.types import Runtime
|
|
|
|
|
|
def _public_task(record: dict[str, Any]) -> dict[str, Any]:
|
|
return {
|
|
"task_id": record["id"],
|
|
"task_name": neutralize_untrusted_tags(str(record.get("task_name") or "Background task")),
|
|
"status": record.get("status"),
|
|
"created_at": record.get("created_at"),
|
|
"updated_at": record.get("updated_at"),
|
|
"error": neutralize_untrusted_tags(str(record["error"])) if record.get("error") else None,
|
|
"cancel_requested": bool(record.get("cancel_requested_at")),
|
|
}
|
|
|
|
|
|
async def _list_background_tasks_impl(
|
|
runtime: Runtime,
|
|
*,
|
|
active_only: bool = False,
|
|
limit: int = 20,
|
|
) -> dict[str, Any]:
|
|
thread_id = _resolve_thread_id(runtime)
|
|
if thread_id is None:
|
|
return {"tasks": [], "message": "The current thread could not be resolved."}
|
|
records = await get_mcp_task_submitter().list_tasks(
|
|
thread_id=thread_id,
|
|
user_id=_resolve_user_id(runtime),
|
|
limit=max(1, min(limit, 50)),
|
|
active_only=active_only,
|
|
)
|
|
tasks = [_public_task(record) for record in records]
|
|
return {"tasks": tasks, "count": len(tasks)}
|
|
|
|
|
|
@tool
|
|
async def list_background_tasks(
|
|
runtime: Runtime,
|
|
active_only: Annotated[bool, "Return only tasks that are still active."] = False,
|
|
) -> dict[str, Any]:
|
|
"""List current and recent durable background tasks for this chat."""
|
|
return await _list_background_tasks_impl(runtime, active_only=active_only)
|
|
|
|
|
|
@tool
|
|
async def cancel_background_task(
|
|
runtime: Runtime,
|
|
task: Annotated[
|
|
str | None,
|
|
"Optional exact task name or DeerFlow task ID. Omit it only when one active task exists.",
|
|
] = None,
|
|
) -> dict[str, Any]:
|
|
"""Cancel one active background task in this chat.
|
|
|
|
If several tasks are active, provide the exact task name shown by
|
|
list_background_tasks. Remote MCP task handles are never needed or exposed.
|
|
"""
|
|
thread_id = _resolve_thread_id(runtime)
|
|
if thread_id is None:
|
|
return {"cancelled": False, "message": "The current thread could not be resolved."}
|
|
try:
|
|
record = await get_mcp_task_submitter().cancel_matching_task(
|
|
thread_id=thread_id,
|
|
user_id=_resolve_user_id(runtime),
|
|
task=task,
|
|
)
|
|
except (LookupError, ValueError) as exc:
|
|
return {"cancelled": False, "message": neutralize_untrusted_tags(str(exc))}
|
|
public = _public_task(record)
|
|
return {
|
|
"cancelled": public["status"] == "cancelled",
|
|
"task": public,
|
|
"message": "Cancellation requested. DeerFlow will keep retrying safely if the remote server is temporarily unavailable.",
|
|
}
|