916 lines
42 KiB
Python
916 lines
42 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
import os
|
|
import threading
|
|
from datetime import datetime
|
|
from functools import lru_cache
|
|
from typing import TYPE_CHECKING
|
|
|
|
from deerflow.config.agents_config import load_agent_soul
|
|
from deerflow.skills.storage import get_or_new_skill_storage
|
|
from deerflow.skills.types import Skill, SkillCategory
|
|
from deerflow.subagents import get_available_subagent_names
|
|
|
|
if TYPE_CHECKING:
|
|
from deerflow.config.app_config import AppConfig
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# Truthy strings for boolean env vars. Mirrors the convention used by
|
|
# DEER_FLOW_AUTH_DISABLED so operators don't have to learn yet another
|
|
# spelling for "yes, turn this on".
|
|
_TRUTHY = frozenset({"1", "true", "yes", "on", "y"})
|
|
|
|
|
|
def _is_offline_mode() -> bool:
|
|
"""Return True when the operator has opted into the offline-network policy.
|
|
|
|
Read from ``DEER_FLOW_OFFLINE_MODE`` and treat empty / unset as off so that
|
|
the public-internet default behaviour is preserved.
|
|
"""
|
|
return (os.getenv("DEER_FLOW_OFFLINE_MODE") or "").strip().lower() in _TRUTHY
|
|
|
|
|
|
_OFFLINE_NETWORK_POLICY_BLOCK = """<network_policy enforced="offline">
|
|
**You are running in an OFFLINE / AIR-GAPPED deployment. The host cannot reach the public internet.**
|
|
|
|
Hard rules — violating any of these is a critical failure:
|
|
- Do NOT call `web_search`, `web_fetch`, browser tools, or any other tool whose name contains
|
|
`web`, `search`, `crawl`, `browser`, `fetch_url`, `http_request`, etc.
|
|
- Do NOT instruct `bash` / `python` to call `curl`, `wget`, `requests.get`, `urllib.request`,
|
|
`httpx.get`, `pip install`, `npm install`, `git clone`, or any other command that performs
|
|
outbound DNS / HTTP / HTTPS / git over network.
|
|
- Do NOT cite URLs as if you had just fetched them. If a fact is needed and you only
|
|
remember it from your training data, state it as "from prior knowledge" rather than
|
|
attributing it to a fresh web hit.
|
|
- Pre-installed local resources (skills, mounted directories, the user's uploaded files,
|
|
files under `/mnt/user-data/*`) are still fully usable.
|
|
|
|
If the user explicitly asks you to look something up online, reply that the deployment is
|
|
offline and offer to work with whatever local context they can provide instead.
|
|
</network_policy>
|
|
"""
|
|
|
|
|
|
def _build_network_policy_section() -> str:
|
|
"""Return the offline-network policy block, or an empty string when not enabled."""
|
|
if not _is_offline_mode():
|
|
return ""
|
|
return _OFFLINE_NETWORK_POLICY_BLOCK
|
|
|
|
_ENABLED_SKILLS_REFRESH_WAIT_TIMEOUT_SECONDS = 5.0
|
|
_enabled_skills_lock = threading.Lock()
|
|
_enabled_skills_cache: list[Skill] | None = None
|
|
_enabled_skills_refresh_active = False
|
|
_enabled_skills_refresh_version = 0
|
|
_enabled_skills_refresh_event = threading.Event()
|
|
|
|
|
|
def _drop_archived_skills(skills: list[Skill]) -> list[Skill]:
|
|
"""从注入提示词的技能列表里剔除已归档(usage state=archived)的技能。
|
|
|
|
技能归档是软状态:SKILL.md 仍留在 custom/<name>/、enabled 标志不变,所以
|
|
load_skills(enabled_only=True) 仍会返回它。但归档技能不应再出现在 agent 的系统
|
|
提示词里 —— 对 agent 而言它应等同于"不存在"。失败时不过滤(返回原列表)。
|
|
"""
|
|
try:
|
|
from deerflow.skills.usage import STATE_ARCHIVED, all_entries
|
|
|
|
entries = all_entries()
|
|
return [s for s in skills if entries.get(s.name, {}).get("state") != STATE_ARCHIVED]
|
|
except Exception:
|
|
logger.debug("Failed to filter archived skills for prompt injection", exc_info=True)
|
|
return skills
|
|
|
|
|
|
def _load_enabled_skills_sync() -> list[Skill]:
|
|
return _drop_archived_skills(list(get_or_new_skill_storage().load_skills(enabled_only=True)))
|
|
|
|
|
|
def _start_enabled_skills_refresh_thread() -> None:
|
|
threading.Thread(
|
|
target=_refresh_enabled_skills_cache_worker,
|
|
name="deerflow-enabled-skills-loader",
|
|
daemon=True,
|
|
).start()
|
|
|
|
|
|
def _refresh_enabled_skills_cache_worker() -> None:
|
|
global _enabled_skills_cache, _enabled_skills_refresh_active
|
|
|
|
while True:
|
|
with _enabled_skills_lock:
|
|
target_version = _enabled_skills_refresh_version
|
|
|
|
try:
|
|
skills = _load_enabled_skills_sync()
|
|
except Exception:
|
|
logger.exception("Failed to load enabled skills for prompt injection")
|
|
skills = []
|
|
|
|
with _enabled_skills_lock:
|
|
if _enabled_skills_refresh_version == target_version:
|
|
_enabled_skills_cache = skills
|
|
_enabled_skills_refresh_active = False
|
|
_enabled_skills_refresh_event.set()
|
|
return
|
|
|
|
# A newer invalidation happened while loading. Keep the worker alive
|
|
# and loop again so the cache always converges on the latest version.
|
|
_enabled_skills_cache = None
|
|
|
|
|
|
def _ensure_enabled_skills_cache() -> threading.Event:
|
|
global _enabled_skills_refresh_active
|
|
|
|
with _enabled_skills_lock:
|
|
if _enabled_skills_cache is not None:
|
|
_enabled_skills_refresh_event.set()
|
|
return _enabled_skills_refresh_event
|
|
if _enabled_skills_refresh_active:
|
|
return _enabled_skills_refresh_event
|
|
_enabled_skills_refresh_active = True
|
|
_enabled_skills_refresh_event.clear()
|
|
|
|
_start_enabled_skills_refresh_thread()
|
|
return _enabled_skills_refresh_event
|
|
|
|
|
|
def _invalidate_enabled_skills_cache() -> threading.Event:
|
|
global _enabled_skills_cache, _enabled_skills_refresh_active, _enabled_skills_refresh_version
|
|
|
|
_get_cached_skills_prompt_section.cache_clear()
|
|
with _enabled_skills_lock:
|
|
_enabled_skills_cache = None
|
|
_enabled_skills_refresh_version += 1
|
|
_enabled_skills_refresh_event.clear()
|
|
if _enabled_skills_refresh_active:
|
|
return _enabled_skills_refresh_event
|
|
_enabled_skills_refresh_active = True
|
|
|
|
_start_enabled_skills_refresh_thread()
|
|
return _enabled_skills_refresh_event
|
|
|
|
|
|
def prime_enabled_skills_cache() -> None:
|
|
_ensure_enabled_skills_cache()
|
|
|
|
|
|
def warm_enabled_skills_cache(timeout_seconds: float = _ENABLED_SKILLS_REFRESH_WAIT_TIMEOUT_SECONDS) -> bool:
|
|
if _ensure_enabled_skills_cache().wait(timeout=timeout_seconds):
|
|
return True
|
|
|
|
logger.warning("Timed out waiting %.1fs for enabled skills cache warm-up", timeout_seconds)
|
|
return False
|
|
|
|
|
|
def _get_enabled_skills():
|
|
with _enabled_skills_lock:
|
|
cached = _enabled_skills_cache
|
|
|
|
if cached is not None:
|
|
return list(cached)
|
|
|
|
_ensure_enabled_skills_cache()
|
|
return []
|
|
|
|
|
|
def _get_enabled_skills_for_config(app_config: AppConfig | None = None) -> list[Skill]:
|
|
"""Return enabled skills using the caller's config source.
|
|
|
|
When a concrete ``app_config`` is supplied, bypass the global enabled-skills
|
|
cache so the skill list and skill paths are resolved from the same config
|
|
object. This keeps request-scoped config injection consistent even while the
|
|
release branch still supports global fallback paths.
|
|
"""
|
|
if app_config is None:
|
|
return _get_enabled_skills()
|
|
return _drop_archived_skills(list(get_or_new_skill_storage(app_config=app_config).load_skills(enabled_only=True)))
|
|
|
|
|
|
def _skill_mutability_label(category: SkillCategory | str) -> str:
|
|
return "[custom, editable]" if category == SkillCategory.CUSTOM else "[built-in]"
|
|
|
|
|
|
def clear_skills_system_prompt_cache() -> None:
|
|
_invalidate_enabled_skills_cache()
|
|
|
|
|
|
async def refresh_skills_system_prompt_cache_async() -> None:
|
|
await asyncio.to_thread(_invalidate_enabled_skills_cache().wait)
|
|
|
|
|
|
def _build_skill_evolution_section(skill_evolution_enabled: bool) -> str:
|
|
if not skill_evolution_enabled:
|
|
return ""
|
|
return """
|
|
## Skill Self-Evolution
|
|
|
|
You have access to `skill_manage` (plus `skill_list` / `skill_view`).
|
|
|
|
After completing a non-trivial task — 5+ tool calls to resolve, recovery from
|
|
a non-obvious error, a corrected approach that worked, or a recurring
|
|
workflow — **offer to save the approach as a reusable skill** so you can
|
|
replay it next time. Briefly propose a hyphen-case name and a one-line
|
|
description, then ask the user to confirm before calling
|
|
`skill_manage(action="create", ...)`. Capture the generalised steps, not the
|
|
user's specific data, paths, or credentials.
|
|
|
|
When using an existing skill and finding it outdated, incomplete, or wrong,
|
|
**patch it immediately** with `skill_manage(action="patch")` — don't wait to
|
|
be asked. Skills that aren't maintained become liabilities.
|
|
|
|
Skip the offer for one-off queries, casual chat, or short single-tool
|
|
answers — and never create a skill silently without user confirmation.
|
|
"""
|
|
|
|
|
|
def _build_available_subagents_description(available_names: list[str], bash_available: bool, *, app_config: AppConfig | None = None) -> str:
|
|
"""Dynamically build subagent type descriptions from registry.
|
|
|
|
Mirrors Codex's pattern where agent_type_description is dynamically generated
|
|
from all registered roles, so the LLM knows about every available type.
|
|
"""
|
|
# Built-in descriptions (kept for backward compatibility with existing prompt quality)
|
|
builtin_descriptions = {
|
|
"general-purpose": "For ANY non-trivial task - web research, code exploration, file operations, analysis, etc.",
|
|
"bash": (
|
|
"For command execution (git, build, test, deploy operations)" if bash_available else "Not available in the current sandbox configuration. Use direct file/web tools or switch to AioSandboxProvider for isolated shell access."
|
|
),
|
|
}
|
|
|
|
# Lazy import moved outside loop to avoid repeated import overhead
|
|
from deerflow.subagents.registry import get_subagent_config
|
|
|
|
lines = []
|
|
for name in available_names:
|
|
if name in builtin_descriptions:
|
|
lines.append(f"- **{name}**: {builtin_descriptions[name]}")
|
|
else:
|
|
config = get_subagent_config(name, app_config=app_config)
|
|
if config is not None:
|
|
desc = config.description.split("\n")[0].strip() # First line only for brevity
|
|
lines.append(f"- **{name}**: {desc}")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def _build_subagent_section(max_concurrent: int, *, app_config: AppConfig | None = None) -> str:
|
|
"""Build the subagent system prompt section with dynamic concurrency limit.
|
|
|
|
Args:
|
|
max_concurrent: Maximum number of concurrent subagent calls allowed per response.
|
|
|
|
Returns:
|
|
Formatted subagent section string.
|
|
"""
|
|
n = max_concurrent
|
|
available_names = get_available_subagent_names(app_config=app_config) if app_config is not None else get_available_subagent_names()
|
|
bash_available = "bash" in available_names
|
|
|
|
# Dynamically build subagent type descriptions from registry (aligned with Codex's
|
|
# agent_type_description pattern where all registered roles are listed in the tool spec).
|
|
available_subagents = _build_available_subagents_description(available_names, bash_available, app_config=app_config)
|
|
direct_tool_examples = "bash, ls, read_file, web_search, etc." if bash_available else "ls, read_file, web_search, etc."
|
|
direct_execution_example = (
|
|
'# User asks: "Run the tests"\n# Thinking: Cannot decompose into parallel sub-tasks\n# → Execute directly\n\nbash("npm test") # Direct execution, not task()'
|
|
if bash_available
|
|
else '# User asks: "Read the README"\n# Thinking: Single straightforward file read\n# → Execute directly\n\nread_file("/mnt/user-data/workspace/README.md") # Direct execution, not task()'
|
|
)
|
|
return f"""<subagent_system>
|
|
**🚀 SUBAGENT MODE ACTIVE - DECOMPOSE, DELEGATE, SYNTHESIZE**
|
|
|
|
You are running with subagent capabilities enabled. Your role is to be a **task orchestrator**:
|
|
1. **DECOMPOSE**: Break complex tasks into parallel sub-tasks
|
|
2. **DELEGATE**: Launch multiple subagents simultaneously using parallel `task` calls
|
|
3. **SYNTHESIZE**: Collect and integrate results into a coherent answer
|
|
|
|
**CORE PRINCIPLE: Complex tasks should be decomposed and distributed across multiple subagents for parallel execution.**
|
|
|
|
**⛔ HARD CONCURRENCY LIMIT: MAXIMUM {n} `task` CALLS PER RESPONSE. THIS IS NOT OPTIONAL.**
|
|
- Each response, you may include **at most {n}** `task` tool calls. Any excess calls are **silently discarded** by the system — you will lose that work.
|
|
- **Before launching subagents, you MUST count your sub-tasks in your thinking:**
|
|
- If count ≤ {n}: Launch all in this response.
|
|
- If count > {n}: **Pick the {n} most important/foundational sub-tasks for this turn.** Save the rest for the next turn.
|
|
- **Multi-batch execution** (for >{n} sub-tasks):
|
|
- Turn 1: Launch sub-tasks 1-{n} in parallel → wait for results
|
|
- Turn 2: Launch next batch in parallel → wait for results
|
|
- ... continue until all sub-tasks are complete
|
|
- Final turn: Synthesize ALL results into a coherent answer
|
|
- **Example thinking pattern**: "I identified 6 sub-tasks. Since the limit is {n} per turn, I will launch the first {n} now, and the rest in the next turn."
|
|
|
|
**Available Subagents:**
|
|
{available_subagents}
|
|
|
|
**Your Orchestration Strategy:**
|
|
|
|
✅ **DECOMPOSE + PARALLEL EXECUTION (Preferred Approach):**
|
|
|
|
For complex queries, break them down into focused sub-tasks and execute in parallel batches (max {n} per turn):
|
|
|
|
**Example 1: "Why is Tencent's stock price declining?" (3 sub-tasks → 1 batch)**
|
|
→ Turn 1: Launch 3 subagents in parallel:
|
|
- Subagent 1: Recent financial reports, earnings data, and revenue trends
|
|
- Subagent 2: Negative news, controversies, and regulatory issues
|
|
- Subagent 3: Industry trends, competitor performance, and market sentiment
|
|
→ Turn 2: Synthesize results
|
|
|
|
**Example 2: "Compare 5 cloud providers" (5 sub-tasks → multi-batch)**
|
|
→ Turn 1: Launch {n} subagents in parallel (first batch)
|
|
→ Turn 2: Launch remaining subagents in parallel
|
|
→ Final turn: Synthesize ALL results into comprehensive comparison
|
|
|
|
**Example 3: "Refactor the authentication system"**
|
|
→ Turn 1: Launch 3 subagents in parallel:
|
|
- Subagent 1: Analyze current auth implementation and technical debt
|
|
- Subagent 2: Research best practices and security patterns
|
|
- Subagent 3: Review related tests, documentation, and vulnerabilities
|
|
→ Turn 2: Synthesize results
|
|
|
|
✅ **USE Parallel Subagents (max {n} per turn) when:**
|
|
- **Complex research questions**: Requires multiple information sources or perspectives
|
|
- **Multi-aspect analysis**: Task has several independent dimensions to explore
|
|
- **Large codebases**: Need to analyze different parts simultaneously
|
|
- **Comprehensive investigations**: Questions requiring thorough coverage from multiple angles
|
|
|
|
❌ **DO NOT use subagents (execute directly) when:**
|
|
- **Task cannot be decomposed**: If you can't break it into 2+ meaningful parallel sub-tasks, execute directly
|
|
- **Ultra-simple actions**: Read one file, quick edits, single commands
|
|
- **Need immediate clarification**: Must ask user before proceeding
|
|
- **Meta conversation**: Questions about conversation history
|
|
- **Sequential dependencies**: Each step depends on previous results (do steps yourself sequentially)
|
|
|
|
**CRITICAL WORKFLOW** (STRICTLY follow this before EVERY action):
|
|
1. **COUNT**: In your thinking, list all sub-tasks and count them explicitly: "I have N sub-tasks"
|
|
2. **PLAN BATCHES**: If N > {n}, explicitly plan which sub-tasks go in which batch:
|
|
- "Batch 1 (this turn): first {n} sub-tasks"
|
|
- "Batch 2 (next turn): next batch of sub-tasks"
|
|
3. **EXECUTE**: Launch ONLY the current batch (max {n} `task` calls). Do NOT launch sub-tasks from future batches.
|
|
4. **REPEAT**: After results return, launch the next batch. Continue until all batches complete.
|
|
5. **SYNTHESIZE**: After ALL batches are done, synthesize all results.
|
|
6. **Cannot decompose** → Execute directly using available tools ({direct_tool_examples})
|
|
|
|
**⛔ VIOLATION: Launching more than {n} `task` calls in a single response is a HARD ERROR. The system WILL discard excess calls and you WILL lose work. Always batch.**
|
|
|
|
**Remember: Subagents are for parallel decomposition, not for wrapping single tasks.**
|
|
|
|
**How It Works:**
|
|
- The task tool runs subagents asynchronously in the background
|
|
- The backend automatically polls for completion (you don't need to poll)
|
|
- The tool call will block until the subagent completes its work
|
|
- Once complete, the result is returned to you directly
|
|
|
|
**Usage Example 1 - Single Batch (≤{n} sub-tasks):**
|
|
|
|
```python
|
|
# User asks: "Why is Tencent's stock price declining?"
|
|
# Thinking: 3 sub-tasks → fits in 1 batch
|
|
|
|
# Turn 1: Launch 3 subagents in parallel
|
|
task(description="Tencent financial data", prompt="...", subagent_type="general-purpose")
|
|
task(description="Tencent news & regulation", prompt="...", subagent_type="general-purpose")
|
|
task(description="Industry & market trends", prompt="...", subagent_type="general-purpose")
|
|
# All 3 run in parallel → synthesize results
|
|
```
|
|
|
|
**Usage Example 2 - Multiple Batches (>{n} sub-tasks):**
|
|
|
|
```python
|
|
# User asks: "Compare AWS, Azure, GCP, Alibaba Cloud, and Oracle Cloud"
|
|
# Thinking: 5 sub-tasks → need multiple batches (max {n} per batch)
|
|
|
|
# Turn 1: Launch first batch of {n}
|
|
task(description="AWS analysis", prompt="...", subagent_type="general-purpose")
|
|
task(description="Azure analysis", prompt="...", subagent_type="general-purpose")
|
|
task(description="GCP analysis", prompt="...", subagent_type="general-purpose")
|
|
|
|
# Turn 2: Launch remaining batch (after first batch completes)
|
|
task(description="Alibaba Cloud analysis", prompt="...", subagent_type="general-purpose")
|
|
task(description="Oracle Cloud analysis", prompt="...", subagent_type="general-purpose")
|
|
|
|
# Turn 3: Synthesize ALL results from both batches
|
|
```
|
|
|
|
**Counter-Example - Direct Execution (NO subagents):**
|
|
|
|
```python
|
|
{direct_execution_example}
|
|
```
|
|
|
|
**CRITICAL**:
|
|
- **Max {n} `task` calls per turn** - the system enforces this, excess calls are discarded
|
|
- Only use `task` when you can launch 2+ subagents in parallel
|
|
- Single task = No value from subagents = Execute directly
|
|
- For >{n} sub-tasks, use sequential batches of {n} across multiple turns
|
|
</subagent_system>"""
|
|
|
|
|
|
SYSTEM_PROMPT_TEMPLATE = """
|
|
<role>
|
|
You are {agent_name}, an internal super agent.
|
|
</role>
|
|
|
|
{soul}
|
|
{memory_context}
|
|
{network_policy_section}
|
|
<thinking_style>
|
|
- Think concisely and strategically about the user's request BEFORE taking action
|
|
- Break down the task: What is clear? What is ambiguous? What is missing?
|
|
- **PRIORITY CHECK: If anything is unclear, missing, or has multiple interpretations, you MUST ask for clarification FIRST - do NOT proceed with work**
|
|
{subagent_thinking}- Never write down your full final answer or report in thinking process, but only outline
|
|
- CRITICAL: After thinking, you MUST provide your actual response to the user. Thinking is for planning, the response is for delivery.
|
|
- Your response must contain the actual answer, not just a reference to what you thought about
|
|
</thinking_style>
|
|
|
|
<clarification_system>
|
|
**WORKFLOW PRIORITY: CLARIFY → PLAN → ACT**
|
|
1. **FIRST**: Analyze the request in your thinking - identify what's unclear, missing, or ambiguous
|
|
2. **SECOND**: If clarification is needed, call `ask_clarification` tool IMMEDIATELY - do NOT start working
|
|
3. **THIRD**: Only after all clarifications are resolved, proceed with planning and execution
|
|
|
|
**CRITICAL RULE: Clarification ALWAYS comes BEFORE action. Never start working and clarify mid-execution.**
|
|
|
|
**MANDATORY Clarification Scenarios - You MUST call ask_clarification BEFORE starting work when:**
|
|
|
|
1. **Missing Information** (`missing_info`): Required details not provided
|
|
- Example: User says "create a web scraper" but doesn't specify the target website
|
|
- Example: "Deploy the app" without specifying environment
|
|
- **REQUIRED ACTION**: Call ask_clarification to get the missing information
|
|
|
|
2. **Ambiguous Requirements** (`ambiguous_requirement`): Multiple valid interpretations exist
|
|
- Example: "Optimize the code" could mean performance, readability, or memory usage
|
|
- Example: "Make it better" is unclear what aspect to improve
|
|
- **REQUIRED ACTION**: Call ask_clarification to clarify the exact requirement
|
|
|
|
3. **Approach Choices** (`approach_choice`): Several valid approaches exist
|
|
- Example: "Add authentication" could use JWT, OAuth, session-based, or API keys
|
|
- Example: "Store data" could use database, files, cache, etc.
|
|
- **REQUIRED ACTION**: Call ask_clarification to let user choose the approach
|
|
|
|
4. **Risky Operations** (`risk_confirmation`): Destructive actions need confirmation
|
|
- Example: Deleting files, modifying production configs, database operations
|
|
- Example: Overwriting existing code or data
|
|
- **REQUIRED ACTION**: Call ask_clarification to get explicit confirmation
|
|
|
|
5. **Suggestions** (`suggestion`): You have a recommendation but want approval
|
|
- Example: "I recommend refactoring this code. Should I proceed?"
|
|
- **REQUIRED ACTION**: Call ask_clarification to get approval
|
|
|
|
**STRICT ENFORCEMENT:**
|
|
- ❌ DO NOT start working and then ask for clarification mid-execution - clarify FIRST
|
|
- ❌ DO NOT skip clarification for "efficiency" - accuracy matters more than speed
|
|
- ❌ DO NOT make assumptions when information is missing - ALWAYS ask
|
|
- ❌ DO NOT proceed with guesses - STOP and call ask_clarification first
|
|
- ✅ Analyze the request in thinking → Identify unclear aspects → Ask BEFORE any action
|
|
- ✅ If you identify the need for clarification in your thinking, you MUST call the tool IMMEDIATELY
|
|
- ✅ After calling ask_clarification, execution will be interrupted automatically
|
|
- ✅ Wait for user response - do NOT continue with assumptions
|
|
|
|
**How to Use:**
|
|
```python
|
|
ask_clarification(
|
|
question="Your specific question here?",
|
|
clarification_type="missing_info", # or other type
|
|
context="Why you need this information", # optional but recommended
|
|
options=["option1", "option2"] # optional, for choices
|
|
)
|
|
```
|
|
|
|
**Example:**
|
|
User: "Deploy the application"
|
|
You (thinking): Missing environment info - I MUST ask for clarification
|
|
You (action): ask_clarification(
|
|
question="Which environment should I deploy to?",
|
|
clarification_type="approach_choice",
|
|
context="I need to know the target environment for proper configuration",
|
|
options=["development", "staging", "production"]
|
|
)
|
|
[Execution stops - wait for user response]
|
|
|
|
User: "staging"
|
|
You: "Deploying to staging..." [proceed]
|
|
</clarification_system>
|
|
|
|
{skills_section}
|
|
|
|
{deferred_tools_section}
|
|
|
|
{writing_mode_section}
|
|
|
|
{subagent_section}
|
|
|
|
<working_directory existed="true">
|
|
- User uploads: `/mnt/user-data/uploads` - Files uploaded by the user (automatically listed in context)
|
|
- User workspace: `/mnt/user-data/workspace` - Working directory for temporary files
|
|
- Output files: `/mnt/user-data/outputs` - Final deliverables must be saved here
|
|
|
|
**File Management:**
|
|
- Uploaded files are automatically listed in the <uploaded_files> section before each request
|
|
- Use `read_file` tool to read uploaded files using their paths from the list
|
|
- For PDF, PPT, Excel, and Word files, converted Markdown versions (*.md) are available alongside originals
|
|
- All temporary work happens in `/mnt/user-data/workspace`
|
|
- Treat `/mnt/user-data/workspace` as your default current working directory for coding and file-editing tasks
|
|
- When writing scripts or commands that create/read files from the workspace, prefer relative paths such as `hello.txt`, `../uploads/data.csv`, and `../outputs/report.md`
|
|
- Avoid hardcoding `/mnt/user-data/...` inside generated scripts when a relative path from the workspace is enough
|
|
- Final deliverables must be copied to `/mnt/user-data/outputs` and presented using `present_files` tool
|
|
{acp_section}
|
|
</working_directory>
|
|
|
|
<response_style>
|
|
- Clear and Concise: Avoid over-formatting unless requested
|
|
- Natural Tone: Use paragraphs and prose, not bullet points by default
|
|
- Action-Oriented: Focus on delivering results, not explaining processes
|
|
</response_style>
|
|
|
|
<citations>
|
|
**CRITICAL: Always include citations when using web search results**
|
|
|
|
- **When to Use**: MANDATORY after web_search, web_fetch, or any external information source
|
|
- **Format**: Use Markdown link format `[citation:TITLE](URL)` immediately after the claim
|
|
- **Placement**: Inline citations should appear right after the sentence or claim they support
|
|
- **Sources Section**: Also collect all citations in a "Sources" section at the end of reports
|
|
|
|
**Example - Inline Citations:**
|
|
```markdown
|
|
The key AI trends for 2026 include enhanced reasoning capabilities and multimodal integration
|
|
[citation:AI Trends 2026](https://techcrunch.com/ai-trends).
|
|
Recent breakthroughs in language models have also accelerated progress
|
|
[citation:OpenAI Research](https://openai.com/research).
|
|
```
|
|
|
|
**Example - Deep Research Report with Citations:**
|
|
```markdown
|
|
## Executive Summary
|
|
|
|
LangGraph is an open-source workflow orchestration framework for LLM applications
|
|
[citation:LangGraph Repository](https://github.com/langchain-ai/langgraph). The project
|
|
focuses on providing stateful multi-agent graphs with built-in checkpointing and streaming
|
|
[citation:LangGraph Documentation](https://langchain-ai.github.io/langgraph/).
|
|
|
|
## Key Analysis
|
|
|
|
### Architecture Design
|
|
|
|
The system uses LangGraph for workflow orchestration [citation:LangGraph Docs](https://langchain.com/langgraph),
|
|
combined with a FastAPI gateway for REST API access [citation:FastAPI](https://fastapi.tiangolo.com).
|
|
|
|
## Sources
|
|
|
|
### Primary Sources
|
|
- [LangGraph Repository](https://github.com/langchain-ai/langgraph) - Official source code and documentation
|
|
- [LangGraph Documentation](https://langchain-ai.github.io/langgraph/) - Technical specifications
|
|
|
|
### Media Coverage
|
|
- [AI Trends 2026](https://techcrunch.com/ai-trends) - Industry analysis
|
|
```
|
|
|
|
**CRITICAL: Sources section format:**
|
|
- Every item in the Sources section MUST be a clickable markdown link with URL
|
|
- Use standard markdown link `[Title](URL) - Description` format (NOT `[citation:...]` format)
|
|
- The `[citation:Title](URL)` format is ONLY for inline citations within the report body
|
|
- ❌ WRONG: `GitHub 仓库 - 官方源代码和文档` (no URL!)
|
|
- ❌ WRONG in Sources: `[citation:GitHub Repository](url)` (citation prefix is for inline only!)
|
|
- ✅ RIGHT in Sources: `[LangGraph Repository](https://github.com/langchain-ai/langgraph) - 官方源代码和文档`
|
|
|
|
**WORKFLOW for Research Tasks:**
|
|
1. Use web_search to find sources → Extract {{title, url, snippet}} from results
|
|
2. Write content with inline citations: `claim [citation:Title](url)`
|
|
3. Collect all citations in a "Sources" section at the end
|
|
4. NEVER write claims without citations when sources are available
|
|
|
|
**CRITICAL RULES:**
|
|
- ❌ DO NOT write research content without citations
|
|
- ❌ DO NOT forget to extract URLs from search results
|
|
- ✅ ALWAYS add `[citation:Title](URL)` after claims from external sources
|
|
- ✅ ALWAYS include a "Sources" section listing all references
|
|
</citations>
|
|
|
|
<critical_reminders>
|
|
- **Clarification First**: ALWAYS clarify unclear/missing/ambiguous requirements BEFORE starting work - never assume or guess
|
|
{subagent_reminder}- Skill First: Always load the relevant skill before starting **complex** tasks.
|
|
- Progressive Loading: Load resources incrementally as referenced in skills
|
|
- Output Files: Final deliverables must be in `/mnt/user-data/outputs`
|
|
- Clarity: Be direct and helpful, avoid unnecessary meta-commentary
|
|
- Including Images and Mermaid: Images and Mermaid diagrams are always welcomed in the Markdown format, and you're encouraged to use `\n\n` or "```mermaid" to display images in response or Markdown files
|
|
- Multi-task: Better utilize parallel tool calling to call multiple tools at one time for better performance
|
|
- Language Consistency: Keep using the same language as user's
|
|
- Always Respond: Your thinking is internal. You MUST always provide a visible response to the user after thinking.
|
|
</critical_reminders>
|
|
"""
|
|
|
|
|
|
def _get_memory_context(agent_name: str | None = None, *, app_config: AppConfig | None = None) -> str:
|
|
"""组装注入系统提示词的记忆上下文 (Memory V2)。
|
|
|
|
通过 :class:`MemoryManager` 收集各 provider 的记忆块。P1 期只有 builtin
|
|
本地 provider —— 返回 USER.md / MEMORY.md 的 frozen snapshot。系统提示词
|
|
在 agent 构建时定型,因此天然就是 frozen snapshot 语义。
|
|
|
|
Args:
|
|
agent_name: agent 标识;``None`` 时按 "default" 处理。
|
|
app_config: 显式应用配置 (本函数实际按 per-user 覆盖解析生效配置)。
|
|
|
|
Returns:
|
|
包在 ``<memory>`` 标签里的记忆上下文;未启用或为空时返回空串。
|
|
"""
|
|
try:
|
|
from deerflow.agents.memory.manager import build_memory_manager
|
|
from deerflow.config.memory_config import get_effective_memory_config
|
|
from deerflow.runtime.user_context import get_effective_user_id
|
|
|
|
user_id = get_effective_user_id()
|
|
config = get_effective_memory_config(user_id)
|
|
|
|
if not config.enabled or not config.injection.enabled:
|
|
return ""
|
|
|
|
manager = build_memory_manager(
|
|
user_id=user_id,
|
|
agent_id=agent_name or "default",
|
|
config=config,
|
|
)
|
|
# 只取静态块 (builtin 的 frozen snapshot + Hindsight 的模式说明)。
|
|
# Hindsight 的动态召回需要当前 query,走 MemoryMiddleware.before_model。
|
|
memory_content = manager.build_system_prompt()
|
|
|
|
if not memory_content.strip():
|
|
return ""
|
|
|
|
return f"""<memory>
|
|
{memory_content}
|
|
</memory>
|
|
"""
|
|
except Exception:
|
|
logger.exception("Failed to load memory context")
|
|
return ""
|
|
|
|
|
|
@lru_cache(maxsize=32)
|
|
def _get_cached_skills_prompt_section(
|
|
skill_signature: tuple[tuple[str, str, str, str], ...],
|
|
available_skills_key: tuple[str, ...] | None,
|
|
container_base_path: str,
|
|
skill_evolution_section: str,
|
|
) -> str:
|
|
filtered = [(name, description, category, location) for name, description, category, location in skill_signature if available_skills_key is None or name in available_skills_key]
|
|
skills_list = ""
|
|
if filtered:
|
|
skill_items = "\n".join(
|
|
f" <skill>\n <name>{name}</name>\n <description>{description} {_skill_mutability_label(category)}</description>\n <location>{location}</location>\n </skill>"
|
|
for name, description, category, location in filtered
|
|
)
|
|
skills_list = f"<available_skills>\n{skill_items}\n</available_skills>"
|
|
|
|
# Restricted mode: agent has an explicit skill allowlist.
|
|
# We deliberately omit the "Skills are located at: <path>" hint and the
|
|
# skill self-evolution section so the LLM is not nudged to browse the
|
|
# /mnt/skills directory or create new skills outside the allowlist.
|
|
is_restricted = available_skills_key is not None
|
|
location_hint = "" if is_restricted else f"\n**Skills are located at:** {container_base_path}\n"
|
|
evolution_block = "" if is_restricted else skill_evolution_section
|
|
|
|
if is_restricted:
|
|
scope_note = "\nYou have access ONLY to the skills listed below. Do NOT attempt to read, list, or invoke any skill that is not in this list, even if you remember other skill paths.\n"
|
|
else:
|
|
scope_note = ""
|
|
|
|
return f"""<skill_system>
|
|
You have access to skills that provide optimized workflows for specific tasks. Each skill contains best practices, frameworks, and references to additional resources.
|
|
{scope_note}
|
|
**Progressive Loading Pattern:**
|
|
1. When a user query matches a skill's use case, immediately call `read_file` on the skill's main file using the path attribute provided in the skill tag below
|
|
2. Read and understand the skill's workflow and instructions
|
|
3. The skill file contains references to external resources under the same folder
|
|
4. Load referenced resources only when needed during execution
|
|
5. Follow the skill's instructions precisely
|
|
{location_hint}{evolution_block}
|
|
{skills_list}
|
|
|
|
</skill_system>"""
|
|
|
|
|
|
def get_skills_prompt_section(available_skills: set[str] | None = None, *, app_config: AppConfig | None = None) -> str:
|
|
"""Generate the skills prompt section with available skills list."""
|
|
skills = _get_enabled_skills_for_config(app_config)
|
|
|
|
if app_config is None:
|
|
try:
|
|
from deerflow.config import get_app_config
|
|
|
|
config = get_app_config()
|
|
container_base_path = config.skills.container_path
|
|
skill_evolution_enabled = config.skill_evolution.enabled
|
|
except Exception:
|
|
container_base_path = "/mnt/skills"
|
|
skill_evolution_enabled = False
|
|
else:
|
|
config = app_config
|
|
container_base_path = config.skills.container_path
|
|
skill_evolution_enabled = config.skill_evolution.enabled
|
|
|
|
# Restricted (custom) agents must not see the skill_evolution / location hints
|
|
# even when the global `skill_evolution_enabled` flag would otherwise emit them.
|
|
is_restricted = available_skills is not None
|
|
effective_skill_evolution_enabled = skill_evolution_enabled and not is_restricted
|
|
|
|
if not skills and not effective_skill_evolution_enabled:
|
|
return ""
|
|
|
|
if available_skills is not None and not any(skill.name in available_skills for skill in skills):
|
|
return ""
|
|
|
|
skill_signature = tuple((skill.name, skill.description, skill.category, skill.get_container_file_path(container_base_path)) for skill in skills)
|
|
available_key = tuple(sorted(available_skills)) if available_skills is not None else None
|
|
if not skill_signature and available_key is not None:
|
|
return ""
|
|
skill_evolution_section = _build_skill_evolution_section(effective_skill_evolution_enabled)
|
|
return _get_cached_skills_prompt_section(skill_signature, available_key, container_base_path, skill_evolution_section)
|
|
|
|
|
|
def get_agent_soul(agent_id: str | None) -> str:
|
|
# Append SOUL.md (agent personality) if present
|
|
soul = load_agent_soul(agent_id)
|
|
if soul:
|
|
return f"<soul>\n{soul}\n</soul>\n" if soul else ""
|
|
return ""
|
|
|
|
|
|
def get_deferred_tools_prompt_section(*, app_config: AppConfig | None = None) -> str:
|
|
"""Generate <available-deferred-tools> block for the system prompt.
|
|
|
|
Lists only deferred tool names so the agent knows what exists
|
|
and can use tool_search to load them.
|
|
Returns empty string when tool_search is disabled or no tools are deferred.
|
|
"""
|
|
from deerflow.tools.builtins.tool_search import get_deferred_registry
|
|
|
|
if app_config is None:
|
|
try:
|
|
from deerflow.config import get_app_config
|
|
|
|
config = get_app_config()
|
|
except Exception:
|
|
return ""
|
|
else:
|
|
config = app_config
|
|
|
|
if not config.tool_search.enabled:
|
|
return ""
|
|
|
|
registry = get_deferred_registry()
|
|
if not registry:
|
|
return ""
|
|
|
|
names = "\n".join(e.name for e in registry.entries)
|
|
return f"<available-deferred-tools>\n{names}\n</available-deferred-tools>"
|
|
|
|
|
|
def _build_acp_section(*, app_config: AppConfig | None = None) -> str:
|
|
"""Build the ACP agent prompt section, only if ACP agents are configured."""
|
|
if app_config is None:
|
|
try:
|
|
from deerflow.config.acp_config import get_acp_agents
|
|
|
|
agents = get_acp_agents()
|
|
except Exception:
|
|
return ""
|
|
else:
|
|
agents = getattr(app_config, "acp_agents", {}) or {}
|
|
|
|
if not agents:
|
|
return ""
|
|
|
|
return (
|
|
"\n**ACP Agent Tasks (invoke_acp_agent):**\n"
|
|
"- ACP agents (e.g. codex, claude_code) run in their own independent workspace — NOT in `/mnt/user-data/`\n"
|
|
"- When writing prompts for ACP agents, describe the task only — do NOT reference `/mnt/user-data` paths\n"
|
|
"- ACP agent results are accessible at `/mnt/acp-workspace/` (read-only) — use `ls`, `read_file`, or `bash cp` to retrieve output files\n"
|
|
"- To deliver ACP output to the user: copy from `/mnt/acp-workspace/<file>` to `/mnt/user-data/outputs/<file>`, then use `present_files`"
|
|
)
|
|
|
|
|
|
def _build_custom_mounts_section(*, app_config: AppConfig | None = None) -> str:
|
|
"""Build a prompt section for explicitly configured sandbox mounts."""
|
|
if app_config is None:
|
|
try:
|
|
from deerflow.config import get_app_config
|
|
|
|
config = get_app_config()
|
|
except Exception:
|
|
logger.exception("Failed to load configured sandbox mounts for the lead-agent prompt")
|
|
return ""
|
|
else:
|
|
config = app_config
|
|
|
|
mounts = config.sandbox.mounts or []
|
|
|
|
if not mounts:
|
|
return ""
|
|
|
|
lines = []
|
|
for mount in mounts:
|
|
access = "read-only" if mount.read_only else "read-write"
|
|
lines.append(f"- Custom mount: `{mount.container_path}` - Host directory mapped into the sandbox ({access})")
|
|
|
|
mounts_list = "\n".join(lines)
|
|
return f"\n**Custom Mounted Directories:**\n{mounts_list}\n- If the user needs files outside `/mnt/user-data`, use these absolute container paths directly when they match the requested directory"
|
|
|
|
|
|
def apply_prompt_template(
|
|
subagent_enabled: bool = False,
|
|
max_concurrent_subagents: int = 3,
|
|
*,
|
|
agent_id: str | None = None,
|
|
agent_name: str | None = None,
|
|
writing_mode: bool = False,
|
|
writing_artifact_path: str | None = None,
|
|
available_skills: set[str] | None = None,
|
|
app_config: AppConfig | None = None,
|
|
) -> str:
|
|
storage_agent_id = agent_id if agent_id is not None else agent_name
|
|
|
|
# Get memory context
|
|
memory_context = _get_memory_context(storage_agent_id, app_config=app_config)
|
|
|
|
# Include subagent section only if enabled (from runtime parameter)
|
|
n = max_concurrent_subagents
|
|
subagent_section = _build_subagent_section(n, app_config=app_config) if subagent_enabled else ""
|
|
|
|
# Add subagent reminder to critical_reminders if enabled
|
|
subagent_reminder = (
|
|
"- **Orchestrator Mode**: You are a task orchestrator - decompose complex tasks into parallel sub-tasks. "
|
|
f"**HARD LIMIT: max {n} `task` calls per response.** "
|
|
f"If >{n} sub-tasks, split into sequential batches of ≤{n}. Synthesize after ALL batches complete.\n"
|
|
if subagent_enabled
|
|
else ""
|
|
)
|
|
|
|
# Add subagent thinking guidance if enabled
|
|
subagent_thinking = (
|
|
"- **DECOMPOSITION CHECK: Can this task be broken into 2+ parallel sub-tasks? If YES, COUNT them. "
|
|
f"If count > {n}, you MUST plan batches of ≤{n} and only launch the FIRST batch now. "
|
|
f"NEVER launch more than {n} `task` calls in one response.**\n"
|
|
if subagent_enabled
|
|
else ""
|
|
)
|
|
|
|
# Get skills section
|
|
skills_section = get_skills_prompt_section(available_skills, app_config=app_config)
|
|
|
|
# Get deferred tools section (tool_search)
|
|
deferred_tools_section = get_deferred_tools_prompt_section(app_config=app_config)
|
|
current_writing_artifact = (
|
|
f"\n**Current Markdown Artifact**\n- Continue editing this existing Markdown file unless the user explicitly asks for a new document: `{writing_artifact_path}`\n"
|
|
if writing_artifact_path
|
|
else ""
|
|
)
|
|
writing_mode_section = (
|
|
f"""
|
|
<writing_mode>
|
|
Writing mode is enabled for this run.
|
|
|
|
**Mandatory Output Contract**
|
|
- Treat the user's request as a writing/report-generation task.
|
|
- Your final deliverable MUST be a Markdown file, not only an inline chat answer.
|
|
- If a current Markdown artifact is provided below, you MUST revise that existing file rather than creating a new file, unless the user explicitly asks for a separate/new document.
|
|
- When revising an existing Markdown artifact, read the file first if needed, then overwrite the same path with the complete updated Markdown content.
|
|
- Write the deliverable to `/mnt/user-data/outputs/` with a clear `.md` filename, for example `/mnt/user-data/outputs/report.md`.
|
|
- Use `write_file` or an equivalent file-writing tool to save the complete Markdown content.
|
|
- After writing the file, call `present_files` with the Markdown file path so the user can open it as an artifact/canvas.
|
|
- The final chat response should be brief and point to the generated Markdown artifact. Do not duplicate the full report inline unless the user explicitly asks.
|
|
- If you revise an existing Markdown artifact, update the Markdown file and present it again.
|
|
{current_writing_artifact}
|
|
</writing_mode>
|
|
"""
|
|
if writing_mode
|
|
else ""
|
|
)
|
|
|
|
# Build ACP agent section only if ACP agents are configured
|
|
acp_section = _build_acp_section(app_config=app_config)
|
|
custom_mounts_section = _build_custom_mounts_section(app_config=app_config)
|
|
acp_and_mounts_section = "\n".join(section for section in (acp_section, custom_mounts_section) if section)
|
|
|
|
# Build the offline-network policy block (empty unless DEER_FLOW_OFFLINE_MODE is set).
|
|
network_policy_section = _build_network_policy_section()
|
|
|
|
# Format the prompt with dynamic skills and memory
|
|
prompt = SYSTEM_PROMPT_TEMPLATE.format(
|
|
agent_name=agent_name or storage_agent_id or "CM 助手",
|
|
soul=get_agent_soul(storage_agent_id),
|
|
skills_section=skills_section,
|
|
deferred_tools_section=deferred_tools_section,
|
|
writing_mode_section=writing_mode_section,
|
|
memory_context=memory_context,
|
|
network_policy_section=network_policy_section,
|
|
subagent_section=subagent_section,
|
|
subagent_reminder=subagent_reminder,
|
|
subagent_thinking=subagent_thinking,
|
|
acp_section=acp_and_mounts_section,
|
|
)
|
|
|
|
return prompt + f"\n<current_date>{datetime.now().strftime('%Y-%m-%d, %A')}</current_date>"
|