RongJie G e01314442c
fix(mcp): scope sessions and task access by thread incarnation (#5556)
* fix(mcp): scope sessions and task access by thread incarnation

* fix(mcp): preserve thread incarnation in delegated subagents

* fix(mcp): preserve incarnation in durable batches

* fix(mcp): bind standalone graph lifecycle context

* fix(studio): preserve implicit thread creation metadata

---------

Co-authored-by: CorgiBoyG <CorgiBoyG@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-23 08:33:20 +08:00

88 lines
3.2 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.mcp_scope import runtime_thread_incarnation
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),
thread_incarnation=runtime_thread_incarnation(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),
thread_incarnation=runtime_thread_incarnation(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.",
}