import logging from langchain.agents import create_agent from langchain.agents.middleware import AgentMiddleware from langchain_core.runnables import RunnableConfig from deerflow.agents.lead_agent.prompt import apply_prompt_template from deerflow.agents.middlewares.clarification_middleware import ClarificationMiddleware from deerflow.agents.middlewares.loop_detection_middleware import LoopDetectionMiddleware from deerflow.agents.middlewares.memory_middleware import MemoryMiddleware from deerflow.agents.middlewares.prompt_prefix_middleware import PromptPrefixMiddleware from deerflow.agents.middlewares.rag_citation_middleware import RagCitationMiddleware from deerflow.agents.middlewares.setup_agent_approval_middleware import SetupAgentApprovalMiddleware from deerflow.agents.middlewares.setup_writing_approval_middleware import SetupWritingApprovalMiddleware from deerflow.agents.middlewares.subagent_limit_middleware import SubagentLimitMiddleware from deerflow.agents.middlewares.summarization_middleware import BeforeSummarizationHook, DeerFlowSummarizationMiddleware from deerflow.agents.middlewares.task_external_context_middleware import TaskExternalContextMiddleware from deerflow.agents.middlewares.title_middleware import TitleMiddleware from deerflow.agents.middlewares.todo_middleware import TodoMiddleware from deerflow.agents.middlewares.token_usage_middleware import TokenUsageMiddleware from deerflow.agents.middlewares.tool_error_handling_middleware import build_lead_runtime_middlewares from deerflow.agents.middlewares.view_image_middleware import ViewImageMiddleware from deerflow.agents.thread_state import ThreadState from deerflow.config.agents_config import load_agent_config, load_agent_soul, validate_agent_id from deerflow.config.app_config import AppConfig, get_app_config from deerflow.models import create_chat_model logger = logging.getLogger(__name__) # 写作配置 Q&A:意图澄清最多几轮(一条用户消息 = 一轮),到顶强制提交配置卡。 # 写作配置 Q&A 现在「先生成大纲并与用户多轮确认 → 再提交配置」,需要更多来回,故上限放宽。 # 仍是兜底安全阀:到顶才用 tool_choice 强制提交,正常流程由用户确认大纲后自然提交。 WRITING_SETUP_MAX_ROUNDS = 8 def _get_runtime_config(config: RunnableConfig) -> dict: """Merge legacy configurable options with LangGraph runtime context.""" cfg = dict(config.get("configurable", {}) or {}) context = config.get("context", {}) or {} if isinstance(context, dict): cfg.update(context) return cfg def _resolve_model_name(requested_model_name: str | None = None, *, app_config: AppConfig | None = None) -> str: """Resolve a runtime model name safely, falling back to default if invalid. Returns None if no models are configured.""" app_config = app_config or get_app_config() default_model_name = app_config.models[0].name if app_config.models else None if default_model_name is None: raise ValueError("No chat models are configured. Please configure at least one model in config.yaml.") if requested_model_name and app_config.get_model_config(requested_model_name): return requested_model_name if requested_model_name and requested_model_name != default_model_name: logger.warning(f"Model '{requested_model_name}' not found in config; fallback to default model '{default_model_name}'.") return default_model_name def _create_summarization_middleware(*, app_config: AppConfig | None = None, force_enabled: bool = False) -> DeerFlowSummarizationMiddleware | None: """Create and configure the summarization middleware from config. Args: force_enabled: When True, build the middleware regardless of the config ``summarization.enabled`` flag. The per-request 上下文压缩 toggle (chat input box, default off) drives this — the config file no longer owns the on/off switch, only the tuning (trigger/keep/...). """ resolved_app_config = app_config or get_app_config() config = resolved_app_config.summarization if not (config.enabled or force_enabled): return None # Prepare trigger parameter trigger = None if config.trigger is not None: if isinstance(config.trigger, list): trigger = [t.to_tuple() for t in config.trigger] else: trigger = config.trigger.to_tuple() # Prepare keep parameter keep = config.keep.to_tuple() # Prepare model parameter. # Bind "middleware:summarize" tag so RunJournal identifies these LLM calls # as middleware rather than lead_agent (SummarizationMiddleware is a # LangChain built-in, so we tag the model at creation time). if config.model_name: model = create_chat_model(name=config.model_name, thinking_enabled=False, app_config=resolved_app_config) else: model = create_chat_model(thinking_enabled=False, app_config=resolved_app_config) model = model.with_config(tags=["middleware:summarize"]) # Prepare kwargs kwargs = { "model": model, "trigger": trigger, "keep": keep, } if config.trim_tokens_to_summarize is not None: kwargs["trim_tokens_to_summarize"] = config.trim_tokens_to_summarize if config.summary_prompt is not None: kwargs["summary_prompt"] = config.summary_prompt # Memory V2:不再有后台抽取队列,无需 summarization 前的 flush hook。 hooks: list[BeforeSummarizationHook] = [] # The logic below relies on two assumptions holding true: this factory is # the sole entry point for DeerFlowSummarizationMiddleware, and the runtime # config is not expected to change after startup. skills_container_path = resolved_app_config.skills.container_path or "/mnt/skills" return DeerFlowSummarizationMiddleware( **kwargs, skills_container_path=skills_container_path, skill_file_read_tool_names=config.skill_file_read_tool_names, before_summarization=hooks, preserve_recent_skill_count=config.preserve_recent_skill_count, preserve_recent_skill_tokens=config.preserve_recent_skill_tokens, preserve_recent_skill_tokens_per_skill=config.preserve_recent_skill_tokens_per_skill, ) def _create_todo_list_middleware(is_plan_mode: bool) -> TodoMiddleware | None: """Create and configure the TodoList middleware. Args: is_plan_mode: Whether to enable plan mode with TodoList middleware. Returns: TodoMiddleware instance if plan mode is enabled, None otherwise. """ if not is_plan_mode: return None # Custom prompts matching DeerFlow's style system_prompt = """ You have access to the `write_todos` tool to help you manage and track complex multi-step objectives. **CRITICAL RULES:** - Mark todos as completed IMMEDIATELY after finishing each step - do NOT batch completions - Keep EXACTLY ONE task as `in_progress` at any time (unless tasks can run in parallel) - Update the todo list in REAL-TIME as you work - this gives users visibility into your progress - DO NOT use this tool for simple tasks (< 3 steps) - just complete them directly **When to Use:** This tool is designed for complex objectives that require systematic tracking: - Complex multi-step tasks requiring 3+ distinct steps - Non-trivial tasks needing careful planning and execution - User explicitly requests a todo list - User provides multiple tasks (numbered or comma-separated list) - The plan may need revisions based on intermediate results **When NOT to Use:** - Single, straightforward tasks - Trivial tasks (< 3 steps) - Purely conversational or informational requests - Simple tool calls where the approach is obvious **Best Practices:** - Break down complex tasks into smaller, actionable steps - Use clear, descriptive task names - Remove tasks that become irrelevant - Add new tasks discovered during implementation - Don't be afraid to revise the todo list as you learn more **Task Management:** Writing todos takes time and tokens - use it when helpful for managing complex problems, not for simple requests. """ tool_description = """Use this tool to create and manage a structured task list for complex work sessions. **IMPORTANT: Only use this tool for complex tasks (3+ steps). For simple requests, just do the work directly.** ## When to Use Use this tool in these scenarios: 1. **Complex multi-step tasks**: When a task requires 3 or more distinct steps or actions 2. **Non-trivial tasks**: Tasks requiring careful planning or multiple operations 3. **User explicitly requests todo list**: When the user directly asks you to track tasks 4. **Multiple tasks**: When users provide a list of things to be done 5. **Dynamic planning**: When the plan may need updates based on intermediate results ## When NOT to Use Skip this tool when: 1. The task is straightforward and takes less than 3 steps 2. The task is trivial and tracking provides no benefit 3. The task is purely conversational or informational 4. It's clear what needs to be done and you can just do it ## How to Use 1. **Starting a task**: Mark it as `in_progress` BEFORE beginning work 2. **Completing a task**: Mark it as `completed` IMMEDIATELY after finishing 3. **Updating the list**: Add new tasks, remove irrelevant ones, or update descriptions as needed 4. **Multiple updates**: You can make several updates at once (e.g., complete one task and start the next) ## Task States - `pending`: Task not yet started - `in_progress`: Currently working on (can have multiple if tasks run in parallel) - `completed`: Task finished successfully ## Task Completion Requirements **CRITICAL: Only mark a task as completed when you have FULLY accomplished it.** Never mark a task as completed if: - There are unresolved issues or errors - Work is partial or incomplete - You encountered blockers preventing completion - You couldn't find necessary resources or dependencies - Quality standards haven't been met If blocked, keep the task as `in_progress` and create a new task describing what needs to be resolved. ## Best Practices - Create specific, actionable items - Break complex tasks into smaller, manageable steps - Use clear, descriptive task names - Update task status in real-time as you work - Mark tasks complete IMMEDIATELY after finishing (don't batch completions) - Remove tasks that are no longer relevant - **IMPORTANT**: When you write the todo list, mark your first task(s) as `in_progress` immediately - **IMPORTANT**: Unless all tasks are completed, always have at least one task `in_progress` to show progress Being proactive with task management demonstrates thoroughness and ensures all requirements are completed successfully. **Remember**: If you only need a few tool calls to complete a task and it's clear what to do, it's better to just do the task directly and NOT use this tool at all. """ return TodoMiddleware(system_prompt=system_prompt, tool_description=tool_description) # ThreadDataMiddleware must be before SandboxMiddleware to ensure thread_id is available # UploadsMiddleware should be after ThreadDataMiddleware to access thread_id # DanglingToolCallMiddleware patches missing ToolMessages before model sees the history # SummarizationMiddleware should be early to reduce context before other processing # TodoListMiddleware should be before ClarificationMiddleware to allow todo management # TitleMiddleware generates title after first exchange # MemoryMiddleware queues conversation for memory update (after TitleMiddleware) # ViewImageMiddleware should be before ClarificationMiddleware to inject image details before LLM # ToolErrorHandlingMiddleware should be before ClarificationMiddleware to convert tool exceptions to ToolMessages # ClarificationMiddleware should be last to intercept clarification requests after model calls def _build_middlewares( config: RunnableConfig, model_name: str | None, agent_name: str | None = None, custom_middlewares: list[AgentMiddleware] | None = None, *, app_config: AppConfig | None = None, ): """Build middleware chain based on runtime configuration. Args: config: Runtime configuration containing configurable options like is_plan_mode. agent_name: If provided, MemoryMiddleware will use per-agent memory storage. custom_middlewares: Optional list of custom middlewares to inject into the chain. Returns: List of middleware instances. """ resolved_app_config = app_config or get_app_config() cfg = _get_runtime_config(config) middlewares = build_lead_runtime_middlewares(app_config=resolved_app_config, lazy_init=True) # Inject the admin prompt prefix into the model request (kept out of the # persisted/displayed message so the user never sees it). middlewares.append(PromptPrefixMiddleware()) if cfg.get("rag_mode_enabled", False): middlewares.append(RagCitationMiddleware()) # Add summarization middleware. The on/off switch now lives in the chat UI # (上下文压缩 toggle, default off) and arrives as ``summarization_enabled`` in # the runtime config; the config file only supplies tuning. force_enabled # lets the per-request toggle turn it on even when config.enabled is false, # while non-UI paths (channels/scheduled) still fall back to config.enabled. summarization_requested = cfg.get("summarization_enabled", False) summarization_middleware = _create_summarization_middleware(app_config=resolved_app_config, force_enabled=summarization_requested) if summarization_middleware is not None: middlewares.append(summarization_middleware) # Add TodoList middleware if plan mode is enabled is_plan_mode = cfg.get("is_plan_mode", False) is_bootstrap = cfg.get("is_bootstrap", False) is_writing_setup = cfg.get("is_writing_setup", False) todo_list_middleware = _create_todo_list_middleware(is_plan_mode) if todo_list_middleware is not None: middlewares.append(todo_list_middleware) # Add TokenUsageMiddleware when token_usage tracking is enabled if resolved_app_config.token_usage.enabled: middlewares.append(TokenUsageMiddleware()) # Add TitleMiddleware — but skip it for scheduled runs. Each scheduled run # executes on a throwaway thread whose title is never shown to a user, and # generating one would fire an extra LLM call on the *default* model # (config.title.model_name is usually unset → models[0]), which is not the # model the task selected. Skipping it keeps scheduled/HTML runs on exactly # the chosen model. if not cfg.get("is_scheduled_run", False): middlewares.append(TitleMiddleware(app_config=resolved_app_config)) # Ephemeral runs (e.g. the open Q&A API /api/open/chat) must NOT touch any # server-side per-user state: no memory recall/retain, no knowledge-base # recall/auto-ingest. Those runs use the shared "default" user bucket, so # persisting their conversations there would both store records the caller # explicitly asked NOT to keep and leak content across unrelated callers. # Skip both write-capable middlewares entirely. is_ephemeral = cfg.get("ephemeral", False) # Add MemoryMiddleware (after TitleMiddleware) — skip in bootstrap mode (agent creation flow) and ephemeral runs memory_injection_enabled = cfg.get("memory_injection_enabled", True) if not is_bootstrap and not is_writing_setup and not is_ephemeral: middlewares.append(MemoryMiddleware(agent_name=agent_name, memory_config=resolved_app_config.memory, injection_enabled=memory_injection_enabled)) # KnowledgeRagMiddleware — inject knowledge-base recall (phase 2). Gated # per-conversation by the ``knowledge_rag_enabled`` runtime flag, defaulting # to the global ``knowledge.rag_enabled`` config. No-op when knowledge is # disabled. Skipped in bootstrap/writing-setup/ephemeral like MemoryMiddleware. knowledge_cfg = getattr(resolved_app_config, "knowledge", None) if not is_bootstrap and not is_writing_setup and not is_ephemeral and knowledge_cfg is not None and knowledge_cfg.enabled: from deerflow.agents.middlewares.knowledge_rag_middleware import KnowledgeRagMiddleware middlewares.append(KnowledgeRagMiddleware(limit=knowledge_cfg.rag_limit, default_enabled=knowledge_cfg.rag_enabled)) # TaskExternalContextMiddleware — inject hidden per-run business context # (for example rwfx's completed 3q report) into the model request only. middlewares.append(TaskExternalContextMiddleware()) # Add ViewImageMiddleware only for the NATIVE-vision path (main model # ingests raw image data). In delegated mode the view_image tool already # returns a text description and stores nothing in `viewed_images`, so the # middleware would otherwise inject a spurious "No images have been viewed." # message. Use the resolved runtime model_name to avoid stale config values. from deerflow.models.vision import is_delegated_vision, vision_enabled if vision_enabled(model_name, app_config=resolved_app_config) and not is_delegated_vision(app_config=resolved_app_config): middlewares.append(ViewImageMiddleware()) # Add DeferredToolFilterMiddleware to hide deferred tool schemas from model binding if resolved_app_config.tool_search.enabled: from deerflow.agents.middlewares.deferred_tool_filter_middleware import DeferredToolFilterMiddleware middlewares.append(DeferredToolFilterMiddleware()) # Add SubagentLimitMiddleware to truncate excess parallel task calls subagent_enabled = cfg.get("subagent_enabled", False) if subagent_enabled: max_concurrent_subagents = cfg.get("max_concurrent_subagents", 3) middlewares.append(SubagentLimitMiddleware(max_concurrent=max_concurrent_subagents)) # SkillReviewMiddleware — trigger background skill review after each turn. # Gated by skill_evolution.enabled + skill_evolution.review_worker.enabled. if resolved_app_config.skill_evolution.enabled and resolved_app_config.skill_evolution.review_worker.enabled: from deerflow.agents.middlewares.skill_review_middleware import SkillReviewMiddleware middlewares.append(SkillReviewMiddleware()) # LoopDetectionMiddleware — detect and break repetitive tool call loops middlewares.append(LoopDetectionMiddleware()) # Inject custom middlewares before ClarificationMiddleware if custom_middlewares: middlewares.extend(custom_middlewares) middlewares.append(SetupAgentApprovalMiddleware()) middlewares.append(SetupWritingApprovalMiddleware()) # SkillStopMiddleware — intercepts skill invocations and halts execution # when a configured skill is about to run. Request-level options # (skill_stop_names / skill_stop_message) take precedence; static # config in ``app_config.skill_stop`` is the fallback. skill_stop_names = cfg.get("skill_stop_names") skill_stop_message = cfg.get("skill_stop_message") if skill_stop_names: from deerflow.agents.middlewares.skill_stop_middleware import SkillStopMiddleware middlewares.append( SkillStopMiddleware( skill_names=skill_stop_names, skills_root=resolved_app_config.skills.container_path or "/mnt/skills", stop_message=skill_stop_message, stop_on_match=True, ) ) logger.info("SkillStopMiddleware enabled (from request): %s", skill_stop_names) elif resolved_app_config.skill_stop.enabled and resolved_app_config.skill_stop.skill_names: from deerflow.agents.middlewares.skill_stop_middleware import SkillStopMiddleware middlewares.append( SkillStopMiddleware( skill_names=resolved_app_config.skill_stop.skill_names, skills_root=resolved_app_config.skills.container_path or "/mnt/skills", stop_message=resolved_app_config.skill_stop.stop_message, stop_on_match=resolved_app_config.skill_stop.stop_on_match, ) ) logger.info("SkillStopMiddleware enabled (from config): %s", resolved_app_config.skill_stop.skill_names) # ClarificationMiddleware should always be last middlewares.append(ClarificationMiddleware()) return middlewares def make_lead_agent(config: RunnableConfig): """LangGraph graph factory; keep the signature compatible with LangGraph Server.""" runtime_config = _get_runtime_config(config) runtime_app_config = runtime_config.get("app_config") return _make_lead_agent(config, app_config=runtime_app_config or get_app_config()) def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig): # Lazy import to avoid circular dependency from deerflow.tools import get_available_tools from deerflow.tools.builtins import ask_clarification_tool, setup_agent, setup_writing cfg = _get_runtime_config(config) resolved_app_config = app_config thinking_enabled = cfg.get("thinking_enabled", True) # Hard "thinking off" switch for internal vLLM/Qwen models whose config only # declares `supports_thinking: true` (no disable shape) — passes through to # create_chat_model(force_disable_thinking=...) so enable_thinking=false is # actually injected. Default off → no behavior change for normal chats. thinking_force_disabled = bool(cfg.get("thinking_force_disabled", False)) reasoning_effort = cfg.get("reasoning_effort", None) requested_model_name: str | None = cfg.get("model_name") or cfg.get("model") is_plan_mode = cfg.get("is_plan_mode", False) subagent_enabled = cfg.get("subagent_enabled", False) max_concurrent_subagents = cfg.get("max_concurrent_subagents", 3) is_bootstrap = cfg.get("is_bootstrap", False) # 写作配置 Q&A:与 is_bootstrap 同构的另一种「最小化 bootstrap」——挂 setup_writing 工具、 # 走写作配置专用 prompt,只负责帮用户把写作表单聊清并提交,不真正开始写作。 is_writing_setup = cfg.get("is_writing_setup", False) writing_setup_article_types = cfg.get("valid_article_types") if is_writing_setup else None # 快捷模式:跳过逐项选项澄清,给标题直接出大纲(见 build_writing_setup_prompt)。 writing_setup_quick = bool(cfg.get("writing_setup_quick", False)) if is_writing_setup else False writing_sample_context = cfg.get("writing_sample_context") if is_writing_setup else None memory_injection_enabled = bool(cfg.get("memory_injection_enabled", True)) rag_mode_enabled = bool(cfg.get("rag_mode_enabled", False)) writing_mode = bool(cfg.get("writing_mode", False)) writing_artifact_path = cfg.get("writing_artifact_path") if writing_mode else None if not isinstance(writing_artifact_path, str) or not writing_artifact_path.strip(): writing_artifact_path = None agent_id = validate_agent_id(cfg.get("agent_id") or cfg.get("agent_name")) requested_excluded_tools = cfg.get("excluded_tools") or None try: agent_config = load_agent_config(agent_id) if not is_bootstrap and not is_writing_setup else None except FileNotFoundError: logger.warning("Agent directory not found for '%s'; running with default config", agent_id) agent_config = None agent_display_name = agent_config.name if agent_config else agent_id # Custom agent model from agent config (if any), or None to let _resolve_model_name pick the default agent_model_name = agent_config.model if agent_config and agent_config.model else None # Final model name resolution: request → agent config → global default, with fallback for unknown names model_name = _resolve_model_name(requested_model_name or agent_model_name, app_config=resolved_app_config) model_config = resolved_app_config.get_model_config(model_name) if model_config is None: raise ValueError("No chat model could be resolved. Please configure at least one model in config.yaml or provide a valid 'model_name'/'model' in the request.") if thinking_enabled and not model_config.supports_thinking: logger.warning(f"Thinking mode is enabled but model '{model_name}' does not support it; fallback to non-thinking mode.") thinking_enabled = False logger.info( "Create Agent(%s) -> thinking_enabled: %s, reasoning_effort: %s, model_name: %s, is_plan_mode: %s, subagent_enabled: %s, max_concurrent_subagents: %s, writing_mode: %s", agent_id or "default", thinking_enabled, reasoning_effort, model_name, is_plan_mode, subagent_enabled, max_concurrent_subagents, writing_mode, ) # Inject run metadata for LangSmith trace tagging if "metadata" not in config: config["metadata"] = {} if is_bootstrap: runtime_available_skills: list[str] | None = None # all enabled skills visible in bootstrap elif agent_config: # Custom agents must use an explicit allowlist. If omitted, default to none. runtime_available_skills = list(agent_config.skills or []) else: # Default lead agent keeps existing behavior (all enabled skills). runtime_available_skills = None config["metadata"].update( { "agent_id": agent_id, "agent_name": agent_display_name or "default", "model_name": model_name or "default", "thinking_enabled": thinking_enabled, "reasoning_effort": reasoning_effort, "is_plan_mode": is_plan_mode, "subagent_enabled": subagent_enabled, "writing_mode": writing_mode, "writing_artifact_path": writing_artifact_path, "tool_groups": agent_config.tool_groups if agent_config else None, "available_skills": runtime_available_skills, } ) if is_writing_setup: # 写作配置 Q&A:最小化 agent —— 只挂 setup_writing(提交配置)+ ask_clarification(弹澄清卡)。 # 关键:绝不给全套工具、绝不复用 lead 超级体提示,否则模型会把「新能源汽车市场分析」之类 # 长得像任务的输入直接做掉,而不是转成写作配置提交。 # 末尾追加 WritingSetupRoundCapMiddleware:澄清最多 3 轮,到顶用 tool_choice 强制提交配置卡。 # 快捷模式:用默认选项、不逐项澄清提问(但仍生成并确认大纲)——直接**不挂** ask_clarification, # 让模型物理上问不了澄清问题,比只靠提示词约束更可靠。 from deerflow.agents.lead_agent.prompt import build_writing_setup_prompt from deerflow.agents.middlewares.writing_setup_round_cap_middleware import WritingSetupRoundCapMiddleware setup_tools = [setup_writing] if writing_setup_quick else [setup_writing, ask_clarification_tool] return create_agent( model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, force_disable_thinking=thinking_force_disabled, app_config=resolved_app_config), tools=setup_tools, middleware=[ *_build_middlewares(config, model_name=model_name, app_config=resolved_app_config), WritingSetupRoundCapMiddleware(max_rounds=WRITING_SETUP_MAX_ROUNDS), ], system_prompt=build_writing_setup_prompt( writing_setup_article_types, quick_mode=writing_setup_quick, sample_context=writing_sample_context, ), state_schema=ThreadState, ) if is_bootstrap: # Special bootstrap agent with minimal prompt for initial custom agent creation flow return create_agent( model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, force_disable_thinking=thinking_force_disabled, app_config=resolved_app_config), tools=get_available_tools( model_name=model_name, subagent_enabled=subagent_enabled, is_scheduled_run=cfg.get("is_scheduled_run", False), excluded_tools=requested_excluded_tools, app_config=resolved_app_config, ) + [setup_agent], middleware=_build_middlewares(config, model_name=model_name, app_config=resolved_app_config), system_prompt=apply_prompt_template( subagent_enabled=subagent_enabled, max_concurrent_subagents=max_concurrent_subagents, writing_mode=writing_mode, writing_artifact_path=writing_artifact_path, available_skills=None, is_bootstrap=True, app_config=resolved_app_config, memory_injection_enabled=memory_injection_enabled, rag_mode_enabled=rag_mode_enabled, ), state_schema=ThreadState, ) # Default lead agent (unchanged behavior) is_notebook_mode = bool(cfg.get("notebook_search_space_id")) notebook_source_ids: list[int] = cfg.get("notebook_source_ids") or [] default_tools = get_available_tools( model_name=model_name, groups=agent_config.tool_groups if agent_config else None, subagent_enabled=subagent_enabled, is_scheduled_run=cfg.get("is_scheduled_run", False), excluded_tools=requested_excluded_tools, app_config=resolved_app_config, ) if is_notebook_mode: from deerflow.tools.builtins.notebook_search_tool import notebook_search_tool default_tools = [notebook_search_tool] + default_tools # Custom agents are steered ONLY by their own SOUL.md — no generic # super-agent system template and no personal memory injection. We branch to # the agent-only prompt builder whenever a custom agent config carries a # non-empty SOUL.md. The plain default lead agent (no agent_config) and # notebook mode keep the full template. See apply_agent_only_prompt_template. has_agent_soul = bool(agent_config) and bool(load_agent_soul(agent_id)) use_agent_only_prompt = has_agent_soul and not is_notebook_mode if use_agent_only_prompt: from deerflow.agents.lead_agent.prompt import apply_agent_only_prompt_template system_prompt = apply_agent_only_prompt_template( agent_id=agent_id, agent_name=agent_display_name, subagent_enabled=subagent_enabled, max_concurrent_subagents=max_concurrent_subagents, available_skills=(set(agent_config.skills or []) if agent_config else None), app_config=resolved_app_config, rag_mode_enabled=rag_mode_enabled, writing_mode=writing_mode, writing_artifact_path=writing_artifact_path, ) else: system_prompt = apply_prompt_template( subagent_enabled=subagent_enabled, max_concurrent_subagents=max_concurrent_subagents, agent_id=agent_id, agent_name=agent_display_name, writing_mode=writing_mode, writing_artifact_path=writing_artifact_path, available_skills=(set(agent_config.skills or []) if agent_config else None), app_config=resolved_app_config, memory_injection_enabled=memory_injection_enabled, rag_mode_enabled=rag_mode_enabled, is_notebook_mode=is_notebook_mode, notebook_source_ids=notebook_source_ids, ) return create_agent( model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, reasoning_effort=reasoning_effort, force_disable_thinking=thinking_force_disabled, app_config=resolved_app_config), tools=default_tools, middleware=_build_middlewares(config, model_name=model_name, agent_name=agent_id, app_config=resolved_app_config), system_prompt=system_prompt, state_schema=ThreadState, )