diff --git a/CHANGELOG.md b/CHANGELOG.md index c69bb3502..eb2771323 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2972,6 +2972,14 @@ This release closes that milestone with **765 merged pull requests**. and Chinese agents/threads/lead-agent pages: the required ASCII `name` request field, lowercase storage, `/api/agents/check` name-availability behavior, and no auto-derived slug from `display_name`. ([#4944]) +- **docs:** Restructure the subagent documentation into an eleven-chapter user + manual under `harness/subagents/` in both languages: concepts, quick start, + the catalog, delegating work, results and acceptance, limits and capacity, + sandbox and isolation, observability, troubleshooting by symptom, developer + integration, and a reference appendix with the June to September 2026 + change log. The former single page becomes the section index, so existing + page links keep working; deep links to sections of the old page now land + on the index. ### Internal diff --git a/CHANGELOG_zh.md b/CHANGELOG_zh.md index 0ac55c098..8fa8931b9 100644 --- a/CHANGELOG_zh.md +++ b/CHANGELOG_zh.md @@ -2222,6 +2222,11 @@ - **文档:** 自定义智能体文档与 API 对齐(中英文 agents / threads / lead-agent 页面):必填的 ASCII `name` 请求字段、小写存储、`/api/agents/check` 的名称可用 性行为,以及不再声称从 `display_name` 自动派生 slug。([#4944]) +- **文档:** 将子 Agent 文档重构为中英文各十一章的用户手册(`harness/subagents/`): + 概念、快速上手、目录、委派用法、结果与验收、限制与容量、沙箱与隔离、可观测性、 + 按症状排查、开发者集成,以及附带 2026 年 6 月至 9 月变更记录的参考附录。原单页 + 成为该章节的索引页,指向该页面的已有链接保持有效;指向旧页面小节锚点的深链接 + 会落到索引页。 ### 内部改进 diff --git a/frontend/src/content/en/harness/middlewares.mdx b/frontend/src/content/en/harness/middlewares.mdx index d4310547f..0dbfb9ffa 100644 --- a/frontend/src/content/en/harness/middlewares.mdx +++ b/frontend/src/content/en/harness/middlewares.mdx @@ -22,7 +22,7 @@ This design keeps the agent core simple and stable while allowing rich, composab loop-detection, token-budget, and summarization guards below are mirrored on the subagent chain (#3875); other Lead-Agent-specific middlewares such as memory, title generation, and clarification do not run there. See - [Subagents → Runaway guards](/docs/harness/subagents#runaway-guards). + [Subagents → Runaway guards](/docs/harness/subagents/limits#runaway-guards). ## How the chain works diff --git a/frontend/src/content/en/harness/subagents.mdx b/frontend/src/content/en/harness/subagents.mdx deleted file mode 100644 index 024293ae6..000000000 --- a/frontend/src/content/en/harness/subagents.mdx +++ /dev/null @@ -1,170 +0,0 @@ ---- -title: Subagents -description: When a task is too broad for a single reasoning thread, or when parts of it can be done in parallel, the Lead Agent delegates work to **subagents**. A subagent is a self-contained agent invocation that receives a specific task, executes it, and returns the result. ---- - -import { Callout } from "nextra/components"; - -# Subagents - - - Subagents are focused workers that the Lead Agent delegates subtasks to. They - run with isolated context, keeping the main conversation clean while handling - parallel or specialized work. - - -When a task is too broad for a single reasoning thread, or when parts of it can be done in parallel, the Lead Agent delegates work to **subagents**. A subagent is a self-contained agent invocation that receives a specific task, executes it, and returns the result. - -## Why subagents matter - -Subagents solve two key problems in long-horizon workflows: - -1. **Context isolation**: a subagent only sees the information it needs for its piece of the task, not the entire parent conversation. This keeps each agent's working context focused and tractable. -2. **Parallelism**: multiple subagents can run concurrently, allowing independent parts of a task (e.g., researching multiple topics simultaneously) to be processed in parallel. - -## Built-in subagents - -DeerFlow ships with two built-in subagents: - -### general-purpose - -A general-purpose reasoning and execution agent. Suitable for delegating complex subtasks that require multi-step reasoning, web search, file operations, and artifact production. - -- **Default timeout**: 1800 seconds (30 minutes) -- **Default max turns**: 150 - -### bash - -A subagent specialized for command-line task execution inside the sandbox. Suitable for scripting, data processing, file transformation, and environment setup tasks. - -- **Default timeout**: 1800 seconds (30 minutes) -- **Default max turns**: 60 -- **Availability**: only exposed when the sandbox's `bash` tool is available (either `allow_host_bash: true` or a container sandbox is configured) - -## Delegation flow - -The Lead Agent delegates work to a subagent using the built-in `task` tool: - -``` -task( - description="research competitors", - prompt="Research the top 5 competitors of Acme Corp and summarize their B2B SaaS pricing", - subagent_type="general-purpose" -) -``` - -The runtime then: - -1. Looks up the subagent configuration from the registry, applying any `config.yaml` overrides. -2. Creates a new agent invocation with the subagent's own prompt and tools. -3. Runs the subagent to completion — bounded by `max_turns`, the `timeout`, and a **middleware guard chain** that mirrors the Lead Agent's (loop detection, token budget, summarization). See [Runaway guards](#runaway-guards) below. -4. Returns the subagent's final output to the Lead Agent as the tool result. - -## Configuration - -Subagent timeouts, max turns, and the per-run token budget are controlled through the `subagents:` section in `config.yaml`: - -```yaml -subagents: - # Default timeout in seconds for all subagents (default: 1800 = 30 minutes) - timeout_seconds: 1800 - - # Optional: override max turns for all subagents. - # Built-in defaults: general-purpose=150, bash=60. Leave unset to keep them. - # max_turns: 120 - - # Per-run token ceiling — a backstop against a subagent burning tokens on - # trivial work. At the hard-stop the in-flight turn is capped (tool calls - # stripped, finish_reason forced to "stop") so the run completes naturally; - # the result is stamped completed + subagent_stop_reason=token_capped so the - # lead/UI can tell a budget-capped run from a clean one (#3875). - token_budget: - enabled: true - max_tokens: 2000000 # generous default — lower it to tighten cost controls - warn_threshold: 0.7 # log a warning once this fraction of the budget is spent - - # Optional: per-agent overrides - agents: - general-purpose: - timeout_seconds: 1800 # 30 minutes for complex tasks - max_turns: 160 - # token_budget: # per-agent override of the global token_budget above - # max_tokens: 3000000 - bash: - timeout_seconds: 300 # 5 minutes for quick commands - max_turns: 80 -``` - -Per-agent overrides take priority over the global `timeout_seconds`, `max_turns`, and `token_budget` settings. - -## Managed subagents and Custom Agent access - -Administrators can add reusable worker definitions from **Settings → Subagents**. A managed subagent defines its delegation description, system prompt, model, tools, skills, maximum turns, timeout, and enabled state. Built-in and `config.yaml` definitions appear in the same catalog as read-only entries. - -The default Lead Agent can see every enabled runtime subagent. Each page-created Custom Agent can narrow that catalog in its settings: - -- **All enabled subagents**: no additional restriction. -- **No subagents**: delegation is disabled even if the request enables it. -- **Selected subagents**: only the selected names are shown in the prompt and accepted by the `task` tool. - -The allowlist is copied into run metadata when a run starts and is enforced again by the `task` tool, so a client cannot bypass it by naming a hidden subagent directly. Runtime precedence is **built-in → `config.yaml` → managed**. If an operator later adds a conflicting `config.yaml` name, the managed entry remains visible in Settings with a conflict warning but is excluded from runtime discovery. Explicit `subagents.agents.` overrides continue to take priority and are shown in the Settings catalog. - -Managed definitions use the same backend selection as Custom Agents: `agent_storage.backend: file` stores one atomic JSON file per definition under `DEER_FLOW_HOME/managed-subagents/`; `agent_storage.backend: db` stores them in the shared application database for multi-instance deployments. These definitions are deployment-wide in this version, not user-scoped. - -## Delegation limits - -The `SubagentLimitMiddleware` controls how many subagents the Lead Agent can invoke in parallel in a single turn and how many total subagent delegations one lead-agent run may launch. - -- `subagent_enabled`: whether subagent delegation is active for this session -- `max_concurrent_subagents`: maximum parallel task calls in one turn (default: 3) -- `max_total_subagents`: optional per-request total cap for one run; defaults to `subagents.max_total_per_run` from `config.yaml` (default: 6, valid range: 1-50) - -If the agent tries to call more subagents than the limits allow, the middleware trims the excess calls. When the total cap is exhausted, it stops new `task` calls for that run and lets the agent synthesize from already collected results. - -## Runaway guards - -A subagent runs its own agent loop, so it needs the same runaway backstops the Lead Agent has. The subagent middleware chain mirrors three Lead Agent guards (#3875): - -- **`LoopDetectionMiddleware`** — breaks a subagent that repeats the same tool call without making progress. Subagents disallow `task`, so only the tool-loop heuristic can fire here. A hard-stop stamps the result `completed` + `subagent_stop_reason=loop_capped`, symmetric to the token budget below. Controlled by the existing `loop_detection` config. -- **`TokenBudgetMiddleware`** — enforces the per-run `subagents.token_budget` ceiling. When the budget is hit the in-flight turn is capped (a final answer is forced) and the result is stamped `completed` + `subagent_stop_reason=token_capped` so the Lead Agent can tell a capped completion from a clean one. Reaching `max_turns` is likewise surfaced as `turn_capped`. -- **`SummarizationMiddleware`** — compacts a long subagent transcript the same way it compacts the Lead Agent's, gated on the same `summarization.enabled` switch so a single config covers both chains. - -These guards engage in addition to the `max_turns` and `timeout` limits. The default `max_tokens` for the token budget is coupled to `summarization.enabled` — 1M when compaction is on, 2M when off — but an explicit `subagents.token_budget.max_tokens` (global or per-agent) always wins, so flipping the summarization switch never silently changes a value you pinned. - -## ACP agents (external agents) - -In addition to the built-in subagents, DeerFlow supports delegating to external agents through the **Agent Client Protocol (ACP)**. ACP allows DeerFlow to invoke agents running as separate processes (including third-party CLI tools wrapped with an ACP adapter). - -Configure ACP agents in `config.yaml`: - -```yaml -acp_agents: - claude_code: - command: npx - args: ["-y", "@zed-industries/claude-agent-acp"] - description: Claude Code for implementation, refactoring, and debugging - model: null - # auto_approve_permissions: false - # env: - # ANTHROPIC_API_KEY: $ANTHROPIC_API_KEY - - codex: - command: npx - args: ["-y", "@zed-industries/codex-acp"] - description: Codex CLI for repository tasks and code generation - model: null -``` - -The Lead Agent invokes ACP agents through the `invoke_acp_agent` built-in tool. - - - ACP agents run as child processes managed by DeerFlow. They communicate over - the ACP wire protocol. The standard CLI tools (like the plain `claude` or - `codex` commands) are not ACP-compatible by default — use the adapter packages - listed above or a compatible ACP wrapper. - - - - - - diff --git a/frontend/src/content/en/harness/subagents/_meta.ts b/frontend/src/content/en/harness/subagents/_meta.ts new file mode 100644 index 000000000..7ee644b67 --- /dev/null +++ b/frontend/src/content/en/harness/subagents/_meta.ts @@ -0,0 +1,36 @@ +import type { MetaRecord } from "nextra"; + +const meta: MetaRecord = { + "quick-start": { + title: "Quick Start", + }, + catalog: { + title: "Subagent Catalog", + }, + delegation: { + title: "Delegating Work", + }, + results: { + title: "Results and Acceptance", + }, + limits: { + title: "Limits, Budgets, and Capacity", + }, + sandbox: { + title: "Sandbox and Isolation", + }, + observability: { + title: "Observability", + }, + troubleshooting: { + title: "Troubleshooting", + }, + developers: { + title: "Developers and Integration", + }, + reference: { + title: "Reference", + }, +}; + +export default meta; diff --git a/frontend/src/content/en/harness/subagents/catalog.mdx b/frontend/src/content/en/harness/subagents/catalog.mdx new file mode 100644 index 000000000..2185562db --- /dev/null +++ b/frontend/src/content/en/harness/subagents/catalog.mdx @@ -0,0 +1,131 @@ +--- +title: Subagent Catalog +description: Built-in subagent defaults and availability, the three definition sources and their precedence, Custom Agent delegation scope, and external ACP agents. +--- + +import { Callout } from "nextra/components"; + +# Subagent Catalog + +The set of subagents the `task` tool can see is the **catalog**. It is merged from three sources and then filtered by the caller's allowlist. This chapter explains how each source is defined and which one wins on conflict. + +## Built-in subagents + +| Name | Tools | Default max turns | Default timeout | Use for | +| ----------------- | ------------------------------------------------------ | ----------------- | --------------- | -------------------------------------------------------------- | +| `general-purpose` | Inherits every Lead Agent tool | 150 | 1800 s | Multi-step reasoning, web search, file operations, artifacts | +| `bash` | `bash`, `ls`, `read_file`, `write_file`, `str_replace` | 60 | 1800 s | Scripting, data processing, file transformation, environment setup | + +Both use the model `inherit`, meaning the Lead Agent's current model. Both deny `task`, `ask_clarification`, and `present_files`. + +`bash` appears in the catalog only when the sandbox allows command execution: + +- Not available without a `sandbox` section. +- Available with any container sandbox (any non-local provider). +- Under the local sandbox it depends on `sandbox.allow_host_bash`, which defaults to `false`. Delegating to `bash` then returns an explicit failure explaining that the option should only be enabled in a fully trusted local environment. + +The `general-purpose` system prompt carries a `tool_restrictions` block stating that `task` is unavailable and that it must never spawn further subagents; when parallelism is needed it should use bash background processes or work sequentially. + +## Three definition sources and their precedence + +| Source | Defined in | Who changes it | +| ------------- | --------------------------------- | --------------------------------- | +| Built-in | Code | Nobody; only parameters can be overridden | +| `config.yaml` | `subagents.custom_agents.` | Operators, restart required | +| Managed | **Settings → Subagents** | Administrators, effective at once | + +Runtime precedence is **built-in → `config.yaml` → managed**. A managed definition whose name collides with a built-in or `config.yaml` entry stays stored and shows a conflict marker in Settings, but is excluded from the runtime catalog. + +On top of that, `subagents.agents.` can override `timeout_seconds`, `max_turns`, `model`, `skills`, and `token_budget` for a subagent from any source. The Settings catalog shows the `timeout_seconds`, `max_turns`, `model`, and `skills` overrides. + + + The global `subagents.timeout_seconds` and `subagents.max_turns` apply to + built-in subagents only. `config.yaml` custom agents and managed subagents + have their own defaults (900 seconds, 50 turns); change them through the + per-agent `subagents.agents.` override. + + +## Defining in config.yaml + +```yaml +subagents: + custom_agents: + analysis: + description: "Data analysis specialist for processing datasets and generating insights" # required; shown in the Lead Agent's catalog + system_prompt: | # required + You are a data analysis specialist... + tools: null # null inherits every Lead Agent tool; or give an allowlist + disallowed_tools: # default; task is always unavailable, the other two only while listed here + - task + - ask_clarification + - present_files + skills: null # null inherits all enabled skills; [] exposes none + model: inherit # or a configured model name + max_turns: 50 + timeout_seconds: 900 +``` + +The first line of `description` is rendered into the `subagent_system` block of the Lead Agent's system prompt. It is HTML-escaped first, so angle brackets in a description cannot become tags. + +## Managing in Settings + +Administrators add reusable workers under **Settings → Subagents**. Each definition has: + +| Field | Notes | +| ---------------------------- | -------------------------------------------------------------------------------------------------- | +| `name` | Letters, digits, and hyphens only; stored lowercase and used as the `subagent_type` | +| `display_name` | Optional | +| `description` | Dispatch description the Lead Agent uses to decide when to delegate | +| `system_prompt` | The system prompt | +| `tools` / `disallowed_tools` | Allow and deny lists. `task`, `ask_clarification`, and `present_files` are always merged into the deny list | +| `skills` | Skill allowlist | +| `model` | `inherit` or a configured model name, validated on save | +| `max_turns` | Default 50 | +| `timeout_seconds` | Default 900 | +| `enabled` | Disabled definitions leave the runtime catalog | + +Built-in and `config.yaml` definitions appear in the same catalog as read-only entries. + +Storage follows the Custom Agent backend: `agent_storage.backend: file` writes one atomic JSON file per definition under `DEER_FLOW_HOME/managed-subagents/`; `agent_storage.backend: db` stores them in the shared application database for multi-instance deployments. These definitions are deployment-wide, not user-scoped. The API lives at `/api/subagents`: any user can list the catalog (system prompts are shown to administrators only), while create, update, and delete require an administrator. The runtime caches definitions for about one second. + +## Custom Agent delegation scope + +The default Lead Agent sees every enabled runtime subagent. Each Custom Agent can narrow that with the **Subagent access** option in its agent settings: + +| Option | Effect | +| ------------------------ | ----------------------------------------------------------------------------------- | +| All enabled subagents | No extra restriction | +| No subagents | Delegation stays off even when the request enables Ultra mode | +| Selected subagents | The prompt lists only the selected names, and `task` and `batch_task` accept only those | + +The allowlist is snapshotted into run metadata when a run starts and enforced again by the `task` tool, so a client cannot bypass it by naming a hidden subagent directly. The assembly descriptor extensions see (`effective_policies.subagents`) is filtered by the same allowlist and never leaks the full catalog. + +## External ACP agents + +Besides built-in and custom subagents, DeerFlow can delegate to external agents that run as separate processes over the **Agent Client Protocol (ACP)**, including third-party CLIs wrapped with an ACP adapter. + +```yaml +acp_agents: + claude_code: + command: npx + args: ["-y", "@zed-industries/claude-agent-acp"] + description: Claude Code for implementation, refactoring, and debugging + model: null + # auto_approve_permissions: false # false denies every permission request from the agent + # timeout_seconds: 1800 # same shape as subagents.timeout_seconds + # env: + # ANTHROPIC_API_KEY: $ANTHROPIC_API_KEY + + codex: + command: npx + args: ["-y", "@zed-industries/codex-acp"] + description: Codex CLI for repository tasks and code generation + model: null +``` + +The Lead Agent calls them through the `invoke_acp_agent` tool. ACP agents do not go through the `task` catalog, capacity, or ledger; only their own `timeout_seconds` bounds them. + + + The plain `claude` and `codex` commands are not ACP-compatible by default. + Use the adapter packages above or another ACP-compatible wrapper. + diff --git a/frontend/src/content/en/harness/subagents/delegation.mdx b/frontend/src/content/en/harness/subagents/delegation.mdx new file mode 100644 index 000000000..c717c9250 --- /dev/null +++ b/frontend/src/content/en/harness/subagents/delegation.mdx @@ -0,0 +1,141 @@ +--- +title: Delegating Work +description: Every task parameter, when to pass a parent-context snapshot, how to write acceptance criteria that are checked automatically, when to switch to durable batches, and how skills, MCP tools, and uploads behave inside a subagent. +--- + +import { Callout } from "nextra/components"; + +# Delegating Work + +This chapter is for people who write prompts, design Custom Agents, or call `task` directly. It walks through `task` parameter by parameter, then covers `batch_task` for large fan-out. + +## task parameters + +| Parameter | Required | Notes | +| --------------------- | -------- | -------------------------------------------------------------------------------------------------------------------- | +| `prompt` | Yes | The task handed to the subagent. Be specific and self-contained | +| `subagent_type` | Yes | A catalog name such as `general-purpose`, `bash`, or a custom name | +| `description` | No | A 3 to 5 word label for logs, the task card, and the delegation ledger. The ledger falls back to the first 200 characters of `prompt` | +| `acceptance_criteria` | No | A list of completion requirements. At most 20 entries of 500 characters each | +| `context_mode` | No | `isolated` (default) or `snapshot` | + +Invalid input never starts a subagent; it returns a failed tool result instead: + +- `context_mode` is neither of the two valid values. +- `subagent_type` is not in the catalog visible to the caller. The error lists the available names, or `none permitted by caller policy` when the caller's policy filtered everything out. +- `subagent_type` is `bash` but the sandbox does not allow command execution. + +### Writing the prompt + +A subagent receives only the `prompt` (plus the optional snapshot) and has no idea what was discussed before. It also cannot ask back: `ask_clarification` is unavailable to it. So: + +- Spell out the constraints: where the input is, where to write output, format requirements, what not to do. +- Name files by absolute path or workspace-relative path. The subagent shares the thread sandbox with the Lead Agent, so `/mnt/user-data/workspace` and `/mnt/user-data/outputs` are visible to both. +- When the deliverable is a file, put its path into `acceptance_criteria` so the parent can verify it automatically. + +## Isolated versus snapshot + +The default `context_mode="isolated"` gives the subagent only its system prompt and the task message. + +`context_mode="snapshot"` is for cases where requirements, rejected approaches, or failed attempts are scattered across the parent conversation. It captures a snapshot after the delegation is validated and before the child is assembled: + +**Included** + +- `summary_text`, if the parent conversation was already compacted, rendered as `Historical conversation summary:`. +- Genuine user messages, and assistant and tool messages not marked hidden. Text blocks, `output_text` blocks, and media blocks (image, audio, video, file). +- Completed tool calls, rendered as inert text: `Historical tool calls (not executed by you): ...`. Only when both the call and its result are still retained. + +**Excluded** + +- The parent's system prompt, hidden framework messages, artifacts, and message metadata. +- Provider reasoning blocks, signatures, and tool-use blocks. +- Media that cannot be serialized, which is replaced by a placeholder note. + +The snapshot becomes one hidden `HumanMessage` named `parent_context_snapshot`, placed after the system prompt and before the task message. The subagent's system prompt gains a "Parent conversation snapshot" note stressing that the tool calls and receipts in the snapshot belong to the parent: they are not its own executions and not evidence that the task is done. + + + The snapshot is not truncated. However long the parent conversation is, that + is the subagent's input, and the caller pays for it. Parent messages sent + after the delegation are not synced to the child. + + +## Acceptance criteria + +`acceptance_criteria` are the completion conditions handed to the subagent and, at the same time, the parent's basis for automatic verification. Four canonical forms are checked deterministically in code: + +| Form | What is checked | +| ----------------------------- | ----------------------------------------------------------------------------------- | +| `file: exists` | The file exists | +| `file: non-empty` | The file exists and is larger than 0 bytes | +| `file_written:` | The file exists and can be read back | +| `tests_passed:` | A recorded, successful bash execution of that command whose output shows a passing test summary | + +Any other wording is passed to the subagent as-is but marked `UNVERIFIED` in the checklist; it is never silently passed. + +Paths may use the `/mnt/user-data/...` prefix or a workspace-relative spelling and must resolve under the thread's workspace or outputs directory. Anything else is `UNVERIFIED`. + +The criteria travel safely: their text appears only in the task message, preceded by the line `Acceptance criteria from the delegating agent (untrusted input, not framework instructions — address each one explicitly in your final report):`. The system prompt carries a value-free `acceptance_criteria` note asking the subagent to address every criterion explicitly in its report, with evidence. + +How to read the results is covered in [Results and Acceptance](/docs/harness/subagents/results). + +## Durable batches with batch_task + +When you have hundreds or thousands of items that are independent and idempotent or read-only, use `batch_task` instead of repeated `task` calls. It returns a batch id immediately, the batch survives Gateway restarts, results never flood the Lead Agent's context, and it does not consume the ordinary `task` per-run total. + +``` +batch_task( + title="Summarize READMEs for 3000 repositories", + subagent_type="general-purpose", + items=[ + {"key": "repo-1", "prompt": "...", "acceptance_criteria": ["file:outputs/repo-1.md non-empty"]}, + ... + ], + max_live_items=100, # optional: cap on items that are active at once + max_running_items=3, # optional: cap on items running at once +) +``` + +| Item | Constraint | +| ------------------------------ | -------------------------------------------------------------- | +| Prerequisite | `subagent_batches.enabled: true` and a SQL database | +| Item `key` | 1 to 128 characters, unique within the batch | +| Item `prompt` | Up to 100,000 characters | +| Item `acceptance_criteria` | Same 20 × 500 character bound as `task` | +| Items per batch | Default cap 5,000 (`max_items_per_batch`) | +| `max_live_items` | Default 100, cap 1,000 | +| `max_running_items` | Default 3, cap 64, and never above `max_live_items` | +| Retries | Up to 3 attempts per item (`max_attempts`) | +| Results | Full result up to 100,000 characters, preview 2,000 | + +A successful submission returns `Batch accepted with items. It is running independently and survives Gateway restarts.`, telling the Lead Agent to use `batch_status` for progress and not to launch ordinary `task` calls for those items. Resubmitting the same `tool_call_id` within a run returns the existing batch instead of creating a new one. + +Companion tools: `batch_status(batch_id)` returns the batch status and per-status item counts; `cancel_batch(batch_id)` cancels it. Both are scoped to batches owned by the current user. + +Progress, item queries, pause / resume / cancel, retrying failed items, and JSONL export go through the workspace UI or the HTTP routes described in [Observability](/docs/harness/subagents/observability). Every item records its acceptance verdict separately: `succeeded` means execution finished, and a failed acceptance never triggers an automatic retry. + + + Batch items run on persistent workers. They have no current-turn upload + boundary, so they cannot discover historical uploads, and no parent run + journal, so they emit no loop-detection audit events. + + +## Skills inside a subagent + +- A subagent loads the skill catalog under the parent run's user identity: the skills that user has enabled, including custom skills and same-name shadowing. Without a user identity it falls back to the default user. +- The `skills` allowlist decides what the subagent can discover and activate: `null` inherits everything, `[]` exposes nothing. +- Skills are **lazily activated**: the system prompt contains only the skill index. The body is loaded when the skill is activated by slash command or read with `read_file`, and only then does its `allowed-tools` apply. A passive skill that was never selected does not strip the subagent's ordinary tools. +- The allowlist scopes discovery and activation; it is not filesystem isolation. Concurrent subagents share the Lead Agent's thread sandbox, and the `/mnt/skills` projection is owned by the Lead Agent's run. + +## MCP tools inside a subagent + +With `tool_search.enabled`, a subagent defers MCP tools the same way the Lead Agent does: the system prompt has an `available-deferred-tools` section listing names only, and the model fetches full schemas on demand through the generated `tool_search` helper. `DeferredToolFilterMiddleware` physically hides unpromoted schemas at the request layer; the prompt section is discovery only. MCP routing hints can auto-promote relevant tools up to `tool_search.auto_promote_top_k`. The `tool_search` helper is exempt from the subagent's own tool allow and deny lists. + +## Historical uploads + +An ordinary `task` delegation can use `list_uploaded_files` to discover files uploaded earlier in the thread. This requires a valid `uploaded_files` state on the parent run (a list of entries with filenames; an empty list is valid), which the runtime deep-copies into the child's initial state. When the state is missing or malformed the tool is not offered. `batch_task` workers never get this tool. + +## What a subagent cannot do + +- It cannot call `task`. The tool is removed, and the `general-purpose` prompt says so explicitly. +- By default it cannot call `ask_clarification` to question the user, nor `present_files` to present files directly; write files into the outputs directory and give their paths in the report. Built-in and managed subagents always deny both; a `config.yaml` subagent denies them through its default `disallowed_tools`, which an operator can override. +- It cannot resume. A subagent is a one-shot execution with no checkpoint; after cancellation or timeout it does not continue from the middle. diff --git a/frontend/src/content/en/harness/subagents/developers.mdx b/frontend/src/content/en/harness/subagents/developers.mdx new file mode 100644 index 000000000..616d4c9e6 --- /dev/null +++ b/frontend/src/content/en/harness/subagents/developers.mdx @@ -0,0 +1,104 @@ +--- +title: Developers and Integration +description: What to know when using create_deerflow_agent or SubagentRuntime directly, the structured contracts, where extensions can observe and intervene, the security boundaries, and the background execution registry rules. +--- + +import { Callout } from "nextra/components"; + +# Developers and Integration + +This chapter is for developers embedding the DeerFlow harness in their own programs, writing extensions, or consuming the frontend contracts. + +## Integrating create_deerflow_agent directly + +`create_deerflow_agent` builds a graph without the Gateway. Subagent-relevant points: + +- Enabling the `subagent` feature through `RuntimeFeatures` installs `SubagentLimitMiddleware` (concurrency and per-run total) and registers the `task` tool. You may pass a `SubagentRuntime`; it requires the `subagent` feature and cannot be combined with full middleware takeover. +- `DurableContextMiddleware` is now **always** on the factory chain. It writes the delegation ledger, which the per-run total and the "already delegated" guidance depend on, and it re-injects `summary_text` after summarization. Older factory graphs lacked it, so those features silently did nothing. +- `RuntimeFeatures(token_budget=True)` builds an enabled `TokenBudgetConfig`. +- When the supplied `SubagentRuntime` owns a durable batch service, `batch_task`, `batch_status`, and `cancel_batch` bind to it; when a runtime is supplied without one, the batch tools are not registered. Only when no `SubagentRuntime` is passed at all do they fall back to the process-global submitter. An owned batch worker that has not been started raises at build time. + +### Runs without a run_id + +Under LangGraph Server, `langgraph dev`, or direct factory calls, the runtime context may have no `run_id`. The runtime handles this as follows: + +- Token budget and loop detection no longer lose their signals when `run_id` is empty: invocations without a non-empty string `run_id` are keyed by `Runtime.control`, and the stop reason is still stored under the context `run_id` as given (including `None`) so the executor can read it. +- The per-run delegation total counts the whole thread's ledger without a `run_id` and logs a warning. +- Extension task-lifecycle notifications are skipped entirely when `run_id` is empty, with a debug log. + +The LangGraph Server entrypoint must be a concrete module-level function; the harness provides `make_lead_agent`. + +### SubagentRuntime + +`SubagentRuntime` bundles the process-wide capacity with an optional durable batch service: + +- `SubagentRuntime.from_app_config(app_config, batch_repository=...)` builds it from configuration; a `SubagentRuntimeConfig` can also be passed directly. +- `max_total_per_run` is range-checked at construction (1 to 50). +- `start()` starts the owned batch worker; `stop()` drains running work without a bound under the lifecycle lock and propagates the caller's cancellation. It runs the batch service stop in a shielded, separately owned task, so it completes even when the caller cancels repeatedly. +- `async with` is supported. + +Public exports: `deerflow.agents` provides `create_deerflow_agent`, `RuntimeFeatures`, `make_lead_agent`, `ThreadState`, and more; `deerflow.subagents` provides `SubagentConfig`, `SubagentExecutor`, `SubagentResult`, `SubagentRuntime`, `get_available_subagent_names`, `get_subagent_config`, and `list_subagents`. Both are lazy exports. + +## Structured contracts + +### Status contract + +`contracts/subagent_status_contract.json` is the fixture shared by backend and frontend (version 2): + +- Valid `subagent_status`: `completed`, `failed`, `cancelled`, `timed_out`, `polling_timed_out`. +- Valid `subagent_stop_reason`: `token_capped`, `turn_capped`, `loop_capped`. +- The tool result text is display content, not part of the contract. + +The backend writes metadata with `make_subagent_additional_kwargs` and reads it with `read_subagent_result_metadata`. The writer raises `ValueError` on an invalid status; the reader returns `None` for an unknown one and maps the legacy `max_turns_reached` to `turn_capped`. The frontend's `parseSubtaskResult` reads structured fields first and falls back to text prefixes only when no structured metadata exists at all. Additive fields (model name, token usage, receipts, acceptance) do not require a contract version bump. + +### Event contract + +`contracts/run_event_stream_contract.json` defines the schemas for `subagent.start` / `subagent.step` / `subagent.end` and the `subagent` category, and notes that durable batch workers emit no parent-run journal events. + +### The two ids of a background execution + +Every delegation has two ids, kept apart on purpose: + +- **`tool_call_id`**: the provider-generated tool call id, used for the `ToolMessage`, SSE events, persistence, and frontend correlation. It may repeat across runs. +- **Execution id**: a server-generated UUID that is the sole key for the background registry, polling, cancellation, timeouts, and cleanup. + +The `ExtensionData.scope_id` extensions see is the `tool_call_id` (the execution id only when it is missing). + +## Extensions + +### Middleware contributions + +Extensions can contribute middlewares to both the Lead Agent and subagent chains at semantic placements: `MODEL_LOGICAL`, `MODEL_PHYSICAL`, `TOOL_VISIBLE`, `TOOL_RAW`, and `STANDARD`. The subagent chain uses `AgentScope.SUBAGENT` with the same anchor table as the Lead Agent, with one difference: `MODEL_PHYSICAL` prefers the inside of the system-message coalescing middleware. Ordering among `STANDARD` contributors is not guaranteed. + +### Task lifecycle + +Extensions implementing `TaskLifecycleContributor` receive `on_task_start(app_store, task_store, info)` and `on_task_stop(app_store, task_store, info, outcome)`. `TaskInfo` carries `task_id`, `run_id`, `thread_id`, `kind` (`lead` or `subagent`), `parent_task_id`, `agent_name`, and `resumed`. For a subagent, `parent_task_id` is the parent run id. Notifications are bounded by a timeout, and an extension exception never affects the subagent. + +### Assembly observation + +When an assembly observer is registered, the executor emits an assembly descriptor after building the graph (`prompt_template_id` is `deerflow-subagent-v1`). Its `effective_policies.subagents` is filtered by the caller's `allowed_subagents`, so the full catalog is never leaked to observers. Without an observer the whole step is skipped. + +## Security boundaries + +Several kinds of untrusted text reach the model prompt around subagents, and the runtime escapes or isolates each one: + +- A custom subagent's `description` is reduced to its first line and HTML-escaped before it is rendered into the `subagent_system` block, so it cannot close the block or forge framework tags. +- Skill names, descriptions, tool lists, and locations are HTML-escaped in the skill index; on slash activation, attributes are escaped and the body is embedded XML-escaped. +- The input sanitization middleware keeps a deny list of tag names shared by the subagent chain and the Lead Agent, including framework tags such as `system-reminder`, `subagent_system`, `skill_system`, `durable_context_data`, `report_contract`, `acceptance_criteria`, `tool_restrictions`, and `current_date`, plus generic words such as `system`, `instruction`, `override`, and `ignore`. +- The remote-content sanitization middleware neutralizes the same tags in results from `web_fetch`, `web_search`, `image_search`, `web_capture`, and every MCP tool; local tool output (bash, file reads) is left untouched. +- Acceptance criteria text enters only the task message; the system prompt carries a value-free note. +- Tool receipts are written under a runtime-owned key that is always overwritten, so a tool cannot forge evidence; the `is_subagent` and `agent_id` of loop-detection events are decided by the server-installed recorder, and caller-supplied keys of the same name are stripped at both the Gateway and embedded-worker boundaries. + +## The background execution registry + +`SubagentExecutor` keeps a process-wide `_background_tasks` registry keyed by execution id, with a companion `Future` table. Its rules: + +- The context copy happens **before** registration, so a copy failure cannot strand a PENDING entry; a failed submission pops the entry. +- `cleanup_background_task` removes terminal entries only; `force_cleanup_background_task` removes unconditionally and is a last resort. +- `Future.cancel()` must be called outside the registry lock, because the completion callback re-acquires it. +- After a cancellation or safety timeout the runtime schedules a deferred cleanup task that polls until the terminal state and then cleans up; the task is strongly referenced so garbage collection cannot swallow it. An unexpected exit of the polling coroutine, including a failed `task_started` emit, also requests cancellation and schedules cleanup. +- Capacity slot release runs in its own task and is shielded from repeated cancellation, so the `_running` counter cannot leak permanently. + +## Where the tests live + +Backend tests for subagents are under `backend/tests/`, in files starting with `test_subagent_`, `test_task_tool_`, `test_worker_subagent_`, `test_acceptance_`, and similar; the frontend card and status-parsing tests are under `frontend/tests/unit/core/tasks/`. When changing a contract, update the JSON under `contracts/` and the tests on both sides together. diff --git a/frontend/src/content/en/harness/subagents/index.mdx b/frontend/src/content/en/harness/subagents/index.mdx new file mode 100644 index 000000000..1317641d6 --- /dev/null +++ b/frontend/src/content/en/harness/subagents/index.mdx @@ -0,0 +1,98 @@ +--- +title: Subagents +description: A subagent is a self-contained worker the Lead Agent delegates a subtask to. This chapter covers the problem it solves, how a delegation flows, what a subagent inherits and what it does not, and how to read this manual. +asIndexPage: true +--- + +import { Callout } from "nextra/components"; + +# Subagents + + + Subagents are focused workers that the Lead Agent delegates subtasks to. They + run with isolated context, keeping the main conversation clean while handling + parallel or specialized work. + + +When a task is too broad for a single reasoning thread, or when parts of it can be done in parallel, the Lead Agent delegates work to **subagents**. A subagent is a self-contained agent invocation: it receives a specific task, executes it, and returns the result to the Lead Agent as a tool result. + +## How to read this manual + +The manual is ordered from concepts to usage to configuration to diagnosis. Different readers need different parts: + +| Reader | Start with | +| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Chat users | This page, [Quick Start](/docs/harness/subagents/quick-start), [Subagent Catalog](/docs/harness/subagents/catalog) | +| Prompt authors and orchestrators | [Delegating Work](/docs/harness/subagents/delegation), [Results and Acceptance](/docs/harness/subagents/results) | +| Operators and administrators | [Limits, Budgets, and Capacity](/docs/harness/subagents/limits), [Sandbox and Isolation](/docs/harness/subagents/sandbox), [Observability](/docs/harness/subagents/observability), [Troubleshooting](/docs/harness/subagents/troubleshooting) | +| Integration developers | [Developers and Integration](/docs/harness/subagents/developers), [Reference](/docs/harness/subagents/reference) | + +## What subagents solve + +1. **Context isolation**: a subagent only sees the information it needs for its piece of the task, not the parent conversation. Each agent's working context stays focused and tractable. +2. **Parallelism**: multiple subagents can run concurrently, so independent parts of a task (for example researching several topics) can progress at the same time. + +Isolation also means a subagent remembers nothing on the Lead Agent's behalf. It leaves exactly three things behind: the final report it returns, the files it writes into the shared workspace, and the execution evidence the runtime records. + +## Delegation flow + +The Lead Agent delegates through the built-in `task` tool: + +``` +task( + description="research competitors", + prompt="Research the top 5 competitors of Acme Corp and summarize their B2B SaaS pricing", + subagent_type="general-purpose" +) +``` + +The runtime then: + +1. **Validates the delegation.** `subagent_type` must be in the catalog visible to the caller, `context_mode` must be `isolated` or `snapshot`, and the `bash` type additionally requires a sandbox that allows command execution. An invalid call returns a failed tool result without starting a subagent. +2. **Assembles the subagent.** It reads the definition from the catalog and applies `config.yaml` overrides, filters tools by allow and deny lists, loads the user-scoped skill index, and builds the system prompt: role prompt, report contract, acceptance-criteria note, skill index, and the deferred MCP tool catalog. +3. **Runs it.** The subagent runs on a dedicated persistent event loop, bounded by process-wide capacity, `max_turns`, `timeout_seconds`, and a middleware guard chain that mirrors the Lead Agent's. +4. **Returns the result.** The final output becomes a `ToolMessage` for the Lead Agent. Structured status lives in the message metadata; the text body is display content only. + +How task cards, event streams, and the ledger surface this process is covered in [Observability](/docs/harness/subagents/observability). + +## What a subagent inherits and what it does not + +| Inherited | Not inherited | +| ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| The same thread sandbox, with its own shell session and execution lease | Parent conversation history. Isolated by default; `context_mode="snapshot"` passes an explicit snapshot | +| The user identity and the user-scoped skill catalog | User memory and the Lead Agent's dynamic context. A subagent receives one `current_date` reminder only | +| The model. `inherit` by default, overridable per agent | The parent run's checkpointer. Subgraphs compile with `checkpointer=False`: one-shot, not resumable | +| Guard configuration: `summarization`, `loop_detection`. The token budget comes from `subagents.token_budget` instead | Parent callbacks bound to the parent event loop. Token usage and audit events reach the parent through proxies | +| The request trace id and the IM `channel_user_id` | The `task` tool (no further delegation) and, by default, `ask_clarification` and `present_files` (no questions to the user) | +| Discovery of historical uploads (ordinary `task` delegations) | Lead-only middlewares: memory, todo, title generation, clarification, delegation limits | + +Details are in [Sandbox and Isolation](/docs/harness/subagents/sandbox). + +## When the Lead Agent delegates + +The Lead Agent prompt treats delegation as **optional** and defaults to direct execution. Before every `task` call it runs a delegation check: it delegates only when real parallel latency savings, specialist capability, or context isolation clearly outweigh startup overhead, duplicate repository discovery, synthesis cost, state-conflict risk, and side-effect risk. When uncertain, it executes directly. + +The prompt also carries two hard limits: at most 3 `task` calls per response and at most 6 per run by default. Excess calls are discarded by middleware and their work is lost. How to configure these numbers and how they interact with process capacity is in [Limits, Budgets, and Capacity](/docs/harness/subagents/limits). + +## Three delegation modes + +| Mode | Tool | Characteristics | +| ------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Ordinary delegation | `task` | The Lead Agent waits for the result, which lands directly in the conversation. Bounded by the per-response concurrency and per-run total | +| Durable batch | `batch_task` | For hundreds or thousands of independent items. Returns a batch id immediately, survives Gateway restarts, and does not consume the ordinary `task` per-run total. Requires a SQL database and `subagent_batches` enabled | +| External ACP agent | `invoke_acp_agent` | Calls an external agent running as a child process over the Agent Client Protocol, such as the ACP adapters for Claude Code or Codex | + +## Terminology + +- **Lead Agent**: the primary agent in a thread that reasons, calls tools, and delegates. +- **Subagent**: the delegated worker. The Settings UI calls them "Subagents" as well. +- **Delegation**: one `task` call and its result. The **delegation ledger** is a system-maintained record of delegations, stored in thread state and preserved across summarization. +- **Receipt**: the execution record the runtime creates for each tool call, numbered like `[r3 write_file]`. A subagent cites it in its report to show that an action really happened. +- **Acceptance criteria**: decidable conditions attached at delegation time, such as "this file exists and is non-empty". The parent checks them in code at zero model cost. +- **stop_reason**: the marker set when a subagent is capped by the token budget, turn budget, or loop detection. A capped run can still be `completed`. +- **Durable context**: summary text, the delegation ledger, and skill context that are stored explicitly in thread state and re-injected before every model call. + + + + + diff --git a/frontend/src/content/en/harness/subagents/limits.mdx b/frontend/src/content/en/harness/subagents/limits.mdx new file mode 100644 index 000000000..79fad9e4c --- /dev/null +++ b/frontend/src/content/en/harness/subagents/limits.mdx @@ -0,0 +1,106 @@ +--- +title: Limits, Budgets, and Capacity +description: Every limit that applies to subagents, with its config key, default, range, and what the user sees when it fires. Covers the runaway guards, process-wide capacity, and the queueing policy. +--- + +import { Callout } from "nextra/components"; + +# Limits, Budgets, and Capacity + +A subagent runs its own agent loop, so it needs the same backstops the Lead Agent has. This chapter puts every limit in one table and then explains what each one looks like when it fires. + +## Overview + +| Limit | Config key / context key | Default | Range | When it fires | +| ------------------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| Per-response concurrency | request context `max_concurrent_subagents` | `subagent_runtime.max_running` (3) | 1 to 64, capped by `subagent_runtime.max_running` | Excess `task` calls are dropped and only logged | +| Per-run total | `subagents.max_total_per_run`; request context `max_total_subagents` overrides it | 6 | 1 to 50 | Excess calls are dropped. If the total was already used up before the response, `[SUBAGENT LIMIT REACHED] ...` is appended to the assistant message and the run's `stop_reason` becomes `subagent_limit_capped`; a response that only crosses the cap is trimmed with a log line | +| Turns | `subagents.max_turns` (built-ins only), `subagents.agents..max_turns` | general-purpose 150, bash 60, custom 50 | at least 1 | Partial result kept and marked `turn_capped`; `failed` when no usable text exists | +| Timeout | `subagents.timeout_seconds` (built-ins only), `subagents.agents..timeout_seconds` | built-ins 1800 s, custom and managed 900 s | at least 1 | Status `timed_out` | +| Polling attempts | Derived from the timeout: `(timeout_seconds + 60) / 5` polls, every 5 seconds | follows the timeout | | Status `polling_timed_out`; the runtime requests cancellation and schedules deferred cleanup | +| Token budget | `subagents.token_budget`, `subagents.agents..token_budget` | enabled; `max_tokens` 2,000,000, or 1,000,000 when summarization is on; `warn_threshold` 0.7 | `max_tokens` at least 1000 | The in-flight turn is capped and forced to finish: `completed` + `token_capped` | +| Loop detection | `loop_detection` (shared with the Lead Agent) | enabled; `warn_threshold` 3, `hard_limit` 5, `window_size` 20, `tool_freq_warn` 30, `tool_freq_hard_limit` 50 | | `completed` + `loop_capped` | +| Summarization | `summarization` (shared with the Lead Agent) | example config: enabled, trigger at 32,000 tokens, keep the last 10 messages | | System prompt and latest user message survive; the summary goes into `summary_text` and is re-injected every call | +| Process-wide capacity | `subagent_runtime.max_running` | 3 | 1 to 64 | When full, queue or reject per `admission_policy` | +| Queue bound | `subagent_runtime.max_queued` | 64 | 0 to 10,000 | Rejected when the queue is full: `Subagent execution capacity is full (3 running, 64 queued)` | +| Admission policy | `subagent_runtime.admission_policy` | `queue` | `queue` / `reject` | With `reject`, a full slot set fails immediately | +| Queue timeout | `subagent_runtime.queue_timeout_seconds` | 300 | 1 to 86,400 | `Timed out after 300s waiting for a subagent execution slot`, status `failed` | +| AIO shell sessions | `sandbox.environment.MAX_SHELL_SESSIONS` | image default 10; auto-set to `max_running + 1` when needed | not below `max_running + 1` | Startup error when too low; see [Sandbox and Isolation](/docs/harness/subagents/sandbox) | + +The four `subagent_runtime` fields are frozen at Gateway startup and need a restart to change. They bound ordinary `task` calls and durable batches alike. Queued delegations wait asynchronously without holding an execution thread. A capacity rejection or queue timeout becomes a `failed` result for an ordinary `task`; durable batch items are requeued instead. + +## Concurrency and total + +`SubagentLimitMiddleware` is installed on the Lead Agent chain only and enforces two gates: + +- **Per-response concurrency**: how many `task` calls one model response may contain. It defaults to the process capacity `max_running` (3 by default) and is capped by it. Excess calls are dropped with no text appended, only a log line. The HARD LIMITS line in the prompt tells the model the number. +- **Per-run total**: the cumulative number of delegations in one run, default 6, which is two full batches at the default concurrency. Only ledger entries tagged with the current `run_id` count; without a `run_id` the middleware logs a warning and counts the whole thread. Calls beyond the remaining total are removed. When one response merely crosses the cap, the trim is only logged. Once the total was already exhausted before a response, all of its `task` calls are removed, the run's `stop_reason` becomes `subagent_limit_capped`, and the assistant message gets this appended: + +``` +[SUBAGENT LIMIT REACHED] The subagent delegation limit for this run has been reached. Continue using the subagent results already collected, execute remaining simple work directly, or summarize the remaining work instead of launching more subagents. +``` + +Both gates depend on the ledger, which `DurableContextMiddleware` writes. Graphs built directly with `create_deerflow_agent` now include it automatically; see [Developers and Integration](/docs/harness/subagents/developers). + +`batch_task` does not count against the per-run total; it has its own `max_live_items` and `max_running_items`. + +## Turns and timeouts + +`max_turns` is the operator-facing notion of a turn: one model call plus the tools it runs. The runtime converts it into LangGraph's super-step budget: + +``` +recursion_limit = max_turns × (nodes per turn) + (one-time nodes per invocation) +``` + +Nodes per turn is the number of `before_model` / `after_model` hooks implemented on the middleware chain plus 2 (the model node and the tools node); one-time nodes is the number of `before_agent` / `after_agent` hooks. Configuring 150 turns therefore really yields 150 turns, regardless of how many middlewares are installed. + +When the turns run out the executor catches `GraphRecursionError`, keeps the last assistant text as a partial result, and marks it `turn_capped`. + +The timeout is wall-clock. Once it elapses the subagent is cancelled with status `timed_out`. Turns and timeout are independent axes: when you raise `max_turns`, usually raise `timeout_seconds` too, or the failure merely moves from turns to timeout. + +## Runaway guards + +The subagent middleware chain mirrors three Lead Agent guards: + +- **Loop detection** (`LoopDetectionMiddleware`): breaks a subagent that repeats the same tool call without progress. Subagents have no `task`, so only the tool-loop heuristic can fire. A hard stop marks the result `completed` + `subagent_stop_reason=loop_capped`. Controlled by the `loop_detection` config, with `tool_freq_overrides` for per-tool thresholds. +- **Token budget** (`TokenBudgetMiddleware`): tracks the run's cumulative tokens against `subagents.token_budget`. At the hard-stop threshold it strips the current turn's tool calls and forces a final answer, marking the result `completed` + `token_capped`. Reaching `warn_threshold` injects a one-time budget warning into the subagent's next model call and logs it at INFO level. +- **Summarization** (`DeerFlowSummarizationMiddleware`): compacts long subagent transcripts under the same `summarization.enabled` switch as the Lead Agent, and by default summarizes with the subagent's own model. Compaction preserves the subagent's system prompt (role, report contract, acceptance note, skill index, deferred tool catalog) and the latest user message. `DurableContextMiddleware` sits before summarization and re-injects `summary_text` into later requests, so a compacted history never starts with an assistant message. + +The default token ceiling is coupled to the summarization switch: 1,000,000 when compaction is on, 2,000,000 when off. An explicit `subagents.token_budget.max_tokens` (global or per agent) always wins, so flipping summarization never silently changes a value you pinned. + +## Configuration example + +```yaml +subagent_runtime: # restart required; shared by task and batches + max_running: 3 + max_queued: 64 + admission_policy: queue # or reject + queue_timeout_seconds: 300 + +subagents: + timeout_seconds: 1800 # default timeout for built-in subagents + # max_turns: 120 # global turn override for built-ins; unset keeps 150 / 60 + max_total_per_run: 6 # delegations per run, 1 to 50 + + token_budget: + enabled: true + max_tokens: 2000000 + warn_threshold: 0.7 + + agents: + general-purpose: + timeout_seconds: 2700 # 45 minutes for deep research + max_turns: 250 + token_budget: + max_tokens: 3000000 + bash: + timeout_seconds: 300 + max_turns: 80 +``` + +Per-agent overrides beat global values. The global `timeout_seconds` and `max_turns` apply to built-in subagents only; custom and managed subagents have their own defaults, so change them through `agents.`. + + + To allow strictly one subagent at a time, set the request context's + `max_concurrent_subagents` to 1. The floor is 1; it is not bumped to 2. + diff --git a/frontend/src/content/en/harness/subagents/observability.mdx b/frontend/src/content/en/harness/subagents/observability.mdx new file mode 100644 index 000000000..bcf49aa1b --- /dev/null +++ b/frontend/src/content/en/harness/subagents/observability.mdx @@ -0,0 +1,121 @@ +--- +title: Observability +description: How the task card derives its state, the fields of SSE and persisted run events, token usage attribution, Langfuse and trace id correlation, and the durable batch query API. +--- + +import { Callout } from "nextra/components"; + +# Observability + +## The task card + +Every `task` call in a conversation has a subtask card (`SubtaskCard`). Its data comes from three places: + +| Information | Source | +| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Model name, token total | Live from `task_started` / `task_running` events; after a reload from the tool result metadata `subagent_model_name` / `subagent_token_usage` | +| Step timeline | Live from `task_running` events; when the card is expanded with no local steps, backfilled from the run-events endpoint as `subagent.step` | +| Terminal status | Only from the tool result metadata `subagent_status`, never inferred from `task_completed` and similar events | + +Status mapping: `completed` shows as completed; `failed`, `cancelled`, `timed_out`, and `polling_timed_out` all show as failed. Structured metadata without a status counts as in progress. Only legacy messages with no structured metadata at all fall back to parsing text prefixes. + +Without a tool result (for example after the user stops), the card stays in progress while the current turn is loading and becomes failed once the turn ends without a result. After a reload, status comes from the checkpointed tool message metadata and steps from the event backfill; neither is lost. + +Token labels are gated by `token_usage.enabled`, which the frontend reads from `GET /api/models` as `token_usage.enabled`. + +## SSE custom events + +During a delegation the `task` tool emits the following custom events through the stream writer. `task_id` is always the provider `tool_call_id`, matching the card one to one; the server-side execution id is never exposed. + +| Event | Payload | Notes | +| ---------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | +| `task_started` | `task_id`, `description`, `model_name` | `description` falls back to `prompt` | +| `task_running` | `task_id`, `message`, `message_index`, `total_messages`, `usage`, `model_name` | Once per subagent message; `usage` is a cumulative snapshot, so consumers replace rather than add | +| `task_completed` | `task_id`, `result`, `usage`, `model_name` | | +| `task_failed` | `task_id`, `error`, `usage`, `model_name` | The "task disappeared from the registry" case carries only `task_id` and `error` | +| `task_cancelled` | `task_id`, `error`, `usage`, `model_name` | | +| `task_timed_out` | `task_id`, `error` (absent for polling timeouts), `usage`, `model_name` | Polling timeouts emit this event too, while the tool result status is `polling_timed_out`; there is no separate polling-timeout event | + +## Persisted run events + +The run worker persists those events to the run event store under the `subagent` category: + +| Event | Content | +| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| `subagent.start` | `task_id`, `description` | +| `subagent.step` | `task_id`, `message_index`, `kind` (`ai` or `tool`), `text`, `truncated`; assistant steps add `tool_calls`, tool steps add `tool_name` | +| `subagent.end` | `task_id`, `status` (`completed` / `failed` / `cancelled` / `timed_out`), `model_name`, `usage`, and `result` or `error` with truncation flags | + +Step text is capped at 8,192 characters. Events are written in batches of 25, `subagent.end` flushes immediately, and a failed write is re-buffered for the next attempt rather than dropped. + +Query endpoint: + +``` +GET /api/threads/{thread_id}/runs/{run_id}/events?event_types=subagent.step&task_id=&limit=500&after_seq= +``` + +`event_types` is comma-separated, `limit` defaults to 500 with a maximum of 2,000, and `after_seq` pages forward. It requires `runs:read` and thread ownership. The event schemas are in `contracts/run_event_stream_contract.json` at the repository root. + +## Tool result metadata + +The terminal `ToolMessage` of a `task` carries these keys in `additional_kwargs`; they are the formal contract for the frontend and other consumers: + +| Key | Meaning | +| ----------------------------- | ---------------------------------------------------------- | +| `subagent_status` | One of the five terminal statuses | +| `subagent_stop_reason` | `token_capped` / `turn_capped` / `loop_capped`, optional | +| `subagent_error` | Error text for non-`completed` results, up to 2,000 characters | +| `subagent_result_brief` | Result brief for `completed`, up to 2,000 characters | +| `subagent_result_sha256` | SHA-256 of the full result | +| `subagent_model_name` | The model actually used | +| `subagent_token_usage` | `input_tokens` / `output_tokens` / `total_tokens` | +| `subagent_tool_receipts` | The subagent's receipt snapshot | +| `subagent_receipt_verdict` | Citation verification verdict | +| `subagent_acceptance_verdict` | Acceptance checklist verdict | + +The cross-language contract is pinned in `contracts/subagent_status_contract.json` (version 2): valid status values, valid `stop_reason` values, and the rule that the text body is display content. Model name, token usage, acceptance, and similar fields are additive extensions that older consumers may ignore. + +## Token usage attribution + +Every subagent model call is recorded by `SubagentTokenCollector` with the caller `subagent:`, capturing the source run id, model name, and input / output / total tokens; a prompt-cache hit adds `cache_read_tokens` (present only when greater than 0). When the subagent finishes, these records flow into the parent run's journal, land in the `subagent` caller bucket, and are attributed to the model that actually produced them. + +Query endpoint: + +``` +GET /api/threads/{thread_id}/token-usage?include_active=false +``` + +The response contains thread totals, input / output totals, run count, `by_model`, `by_caller` (`lead_agent` / `subagent` / `middleware`), and context usage. `by_model` is reduced from each run's per-model breakdown, so a subagent on a different model is not charged to the Lead Agent's model; legacy runs without a breakdown fall back to the run-level model name. Cost accounting prices uncached input, cache-hit input, and output per model. + +## Langfuse + +Subagent spans are attributed to the parent thread: `session_id` is the parent `thread_id`, `user_id` is the current user, the trace name is `subagent:` (lowercase, underscores replaced by hyphens), and tags carry the environment and model. The LangChain tag is likewise `subagent:`. Opening a thread in the Langfuse Sessions view shows every subagent it dispatched. The request-level `deerflow_trace_id` is written into the trace metadata as well. + +## Request trace id + +Every Gateway request has an `X-Trace-Id` (inherited from the request header or generated). The id travels with the run into subagents, the run record, checkpoint metadata, and Langfuse traces. Whether logs print it depends on `logging.enhance.enabled`. Subagent log lines additionally carry an 8-character short `trace_id`, formatted as `[trace=1a2b3c4d]`, for stitching one delegation's output together in the Gateway log. + +## Loop detection events + +When a subagent trips loop detection, a `middleware:loop_detection` event (category `middleware`) is recorded through the parent run's journal proxy, with `hook`, `action`, and `changes`: `is_subagent`, `agent_id` (the subagent config name), `detection_layer`, `tool_names`, `count`, and `threshold`. `is_subagent` and `agent_id` are decided server-side; client-supplied values are dropped. Durable batch workers have no parent run journal and emit none. Query them through the same `/events` endpoint with `event_types=middleware:loop_detection`. + +## Durable batch API + +Durable batches have no SSE; the workspace UI polls, every 2 seconds while a batch is active and every 15 seconds otherwise. The HTTP routes live under `/api/threads/{thread_id}/subagent-batches` and are owner-scoped: + +| Method and path | Purpose | +| ------------------------------------------ | -------------------------------------------------------------------------------- | +| `GET ""` | List the thread's batches, `limit` 1 to 100, default 20 | +| `GET /{batch_id}` | Batch detail with per-status item counts | +| `GET /{batch_id}/items` | Paged items: `offset`, `limit` (1 to 500, default 100), optional `status` filter | +| `POST /{batch_id}/pause` | Pause | +| `POST /{batch_id}/resume` | Resume | +| `POST /{batch_id}/cancel` | Cancel; 503 when the worker is not running | +| `POST /{batch_id}/items/{item_id}/retry` | Retry one item; only `failed` items, otherwise 409 | +| `GET /{batch_id}/results.jsonl` | Stream every item as NDJSON, including full results and acceptance verdicts | + +Batch statuses: `queued`, `running`, `paused` are active; `completed`, `failed`, `cancelled` are terminal. Item statuses: `pending` is waiting and not counted as active; `queued`, `leased`, `running` are active; `succeeded`, `failed`, `cancelled` are terminal. When a batch ends with failed items and no succeeded items it is `failed`, otherwise `completed`. + +## Extension observers + +Extensions with a task-lifecycle observer are notified when each subagent starts and stops: `TaskInfo.kind` is `subagent`, `task_id` is the server-side execution id, `parent_task_id` is the parent run id, and `agent_name` is the subagent name. The `TaskOutcome` at stop is `completed`, `aborted` (cancelled), or `failed` (everything else). Runs without a `run_id`, such as direct integrations, trigger no notifications. diff --git a/frontend/src/content/en/harness/subagents/quick-start.mdx b/frontend/src/content/en/harness/subagents/quick-start.mdx new file mode 100644 index 000000000..e3a0b0a84 --- /dev/null +++ b/frontend/src/content/en/harness/subagents/quick-start.mdx @@ -0,0 +1,64 @@ +--- +title: Quick Start +description: Your first delegation in five minutes. Turn subagents on, send a task that splits well, read the task card, and know what happens when you press stop. +--- + +import { Callout, Steps } from "nextra/components"; + +# Quick Start + +This chapter is for people using DeerFlow through the web interface. By the end you will know how to let the Lead Agent use subagents, how to read a task card, and where the result goes. + +## Prerequisites + +- DeerFlow is configured per [Quick Start](/docs/application/quick-start) and you can chat normally. The `subagents:` section in `config.yaml` can stay empty; the built-in defaults work. +- To use the `bash` subagent you need a container sandbox, or `sandbox.allow_host_bash: true` under the local sandbox, which is only appropriate in a fully trusted local environment. Otherwise `bash` is hidden from the catalog. + + + +### Turn subagents on + +Pick **Ultra** in the mode selector of the input box. The UI describes it as "Pro mode with subagents to divide work". Ultra mode sets `subagent_enabled` to `true` in the request context, enables plan mode, and defaults the reasoning effort to high. In Flash, Thinking, and Pro modes the Lead Agent does not see the `task` tool. + +If you are talking to a Custom Agent, the **Subagent access** option in that agent's settings may narrow or disable delegation further. See [Subagent Catalog](/docs/harness/subagents/catalog). + +### Send a task that splits well + +More delegation is not better. The Lead Agent defaults to direct execution and only delegates when parallelism, specialist capability, or context isolation gives a clear net benefit. A good candidate: + +> Research the pricing changes at companies A, B, and C over the last year, write one paragraph of conclusions for each, then combine them into a comparison table. + +The three investigations are independent, can run in parallel, and their browsing history does not need to pollute the main conversation. + +### Read the task card + +Every `task` call shows up in the conversation as a subtask card: + +- **Status icon**: a spinner while running, a check mark when completed, a red cross when failed. Cancelled, timed-out, and polling-timed-out runs all show as failed. +- **Model label and token total**: the model the subagent actually used and its cumulative tokens. The token count updates after each completed subagent model call and is hidden when `token_usage.enabled` is `false`. +- **Step timeline**: expand the card to see each step, with assistant reasoning interleaved with tool calls. After a page reload the steps are backfilled from run events, so nothing is lost. +- **Result**: the card renders the final report when completed, or a red error line when failed. + +### How the result reaches the Lead Agent + +The subagent's final report returns as a tool result starting with `Task Succeeded. Result:`. If acceptance criteria were attached, an `Acceptance checklist` section follows. The Lead Agent continues from there and synthesizes the final reply. + +The report is the subagent's **self-report**. The runtime cross-checks the receipts it cites and marks unverified parts in the delegation ledger, and the Lead Agent uses that to decide whether to double-check. See [Results and Acceptance](/docs/harness/subagents/results). + +### Stopping + +Pressing stop while a subagent is running sends it a cancellation request. The card turns to the failed state because no tool result arrives, and the ledger entry is marked `cancelled` when the next run starts, so the Lead Agent is no longer told that the task is "already delegated, do not repeat". + + + + + A subagent cannot ask you questions: `ask_clarification` is not available to + it. Put the constraints, paths, and expected deliverable into your request to + the Lead Agent, which passes them on. + + +## Next steps + +- To see which subagents exist and how to add your own, read [Subagent Catalog](/docs/harness/subagents/catalog). +- To give a subagent the parent conversation as background, or to attach automatically checked acceptance criteria, read [Delegating Work](/docs/harness/subagents/delegation). +- If subagents run too long or cost too much, read [Limits, Budgets, and Capacity](/docs/harness/subagents/limits). diff --git a/frontend/src/content/en/harness/subagents/reference.mdx b/frontend/src/content/en/harness/subagents/reference.mdx new file mode 100644 index 000000000..4a6cdc1db --- /dev/null +++ b/frontend/src/content/en/harness/subagents/reference.mdx @@ -0,0 +1,183 @@ +--- +title: Reference +description: Quick reference for subagent configuration keys, request context keys, tool signatures, status enums, events, and HTTP routes, plus the change log from June to September 2026. +--- + +# Reference + +## Configuration keys + +| Key | Default | Notes | +| -------------------------------------------- | ------------------------------------------- | --------------------------------------------------------------------------- | +| `subagents.timeout_seconds` | 1800 | Default timeout in seconds for built-in subagents | +| `subagents.max_turns` | unset | Global turn override for built-in subagents | +| `subagents.max_total_per_run` | 6 | Delegations per run, 1 to 50 | +| `subagents.token_budget.enabled` | true | | +| `subagents.token_budget.max_tokens` | 2,000,000; 1,000,000 when summarization is on | Once set explicitly, no longer coupled to the summarization switch | +| `subagents.token_budget.warn_threshold` | 0.7 | At this fraction, warns the model once on its next call | +| `subagents.agents..timeout_seconds` | | Per-agent override, applies to subagents from any source | +| `subagents.agents..max_turns` | | | +| `subagents.agents..model` | | | +| `subagents.agents..skills` | | `null` inherits all, `[]` none | +| `subagents.agents..token_budget` | | | +| `subagents.custom_agents..*` | | `description` and `system_prompt` required; `max_turns` 50, `timeout_seconds` 900 | +| `subagent_runtime.max_running` | 3 | Process-wide concurrency, 1 to 64, restart required | +| `subagent_runtime.max_queued` | 64 | 0 to 10,000 | +| `subagent_runtime.admission_policy` | `queue` | `queue` or `reject` | +| `subagent_runtime.queue_timeout_seconds` | 300 | 1 to 86,400 | +| `subagent_batches.enabled` | false | Requires a SQL database | +| `subagent_batches.max_items_per_batch` | 5,000 | | +| `subagent_batches.default_max_live_items` | 100 | Cap `max_live_items_per_batch` 1,000 | +| `subagent_batches.default_max_running_items` | 3 | Cap `max_running_items_per_batch` 64 | +| `subagent_batches.max_attempts` | 3 | | +| `subagent_batches.max_result_chars` | 100,000 | `result_preview_max_chars` 2,000 | +| `subagent_batches.lease_seconds` | 120 | `poll_interval_seconds` 1 | +| `acp_agents..*` | | `command` and `description` required; `timeout_seconds` 1800; `auto_approve_permissions` false | +| `verification.receipts_enabled` | true | Tool receipts and citation verification | +| `loop_detection.*` | see [Limits](/docs/harness/subagents/limits) | Shared with the Lead Agent | +| `summarization.*` | see [Limits](/docs/harness/subagents/limits) | Shared with the Lead Agent | +| `tool_search.enabled` / `auto_promote_top_k` | | Deferred MCP tool loading | +| `token_usage.enabled` | true | Whether cards show tokens | +| `agent_storage.backend` | `file` | Where managed subagents are stored, `file` or `db` | +| `sandbox.allow_host_bash` | false | Whether the `bash` subagent is available under the local sandbox | +| `sandbox.environment.MAX_SHELL_SESSIONS` | image default 10 | Must be at least `subagent_runtime.max_running + 1` | +| Environment variable `DEER_FLOW_DATE_TIMEZONE` | server timezone | IANA zone used for the `current_date` reminder | + +## Request context keys + +| Key | Notes | +| -------------------------- | ------------------------------------------------------------------------------------------------------------ | +| `subagent_enabled` | Whether delegation is allowed; the web UI sets it in Ultra mode | +| `max_concurrent_subagents` | Per-response concurrency, 1 to 64, capped by `subagent_runtime.max_running` | +| `max_total_subagents` | Temporary override of the per-run total, 1 to 50 | +| `allowed_subagents` | Not read from the request: comes from the Custom Agent definition, `null` all, `[]` none, a list as allowlist; snapshotted into run metadata at run start | + +## Tool signatures + +``` +task(prompt: str, subagent_type: str, *, + acceptance_criteria: list[str] | None = None, + description: str = "", + context_mode: "isolated" | "snapshot" = "isolated") + +batch_task(title: str, items: list[{key, prompt, acceptance_criteria?}], subagent_type: str, + max_live_items: int | None = None, max_running_items: int | None = None) +batch_status(batch_id: str) +cancel_batch(batch_id: str) + +invoke_acp_agent(...) # external ACP agents, see Subagent Catalog +``` + +Canonical acceptance criteria: `file: exists`, `file: non-empty`, `file_written:`, `tests_passed:`. At most 20 entries of 500 characters each. + +## Statuses and cap reasons + +| Field | Values | +| --------------------------- | --------------------------------------------------------------------------------------- | +| `subagent_status` | `completed`, `failed`, `cancelled`, `timed_out`, `polling_timed_out` | +| `subagent_stop_reason` | `token_capped`, `turn_capped`, `loop_capped` | +| Acceptance leaf | Booleans `checked` and `holds`; rendered as `holds` / `does not hold` / `UNVERIFIED` | +| Receipt verification | `resolved`, `failed`, `unknown`, `no_citation_claims` | +| Delegation ledger status | `in_progress` plus the five terminal statuses | +| Batch status | `queued`, `running`, `paused`, `completed`, `failed`, `cancelled` | +| Batch item status | `pending`, `queued`, `leased`, `running`, `succeeded`, `failed`, `cancelled` | + +Result text formats: `Task Succeeded. Result: ...`, `Task Succeeded (capped: