mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-11 14:38:38 +00:00
* feat(harness): subagent report contract and delegation acceptance criteria (RFC #4651 PR3) Layer 1 receipt verification is inert unless subagents actually cite their execution record. This lands the prompt layer that closes the adoption gap: - New subagents/report_contract.py owns the model-facing contract text, derived from the single-owner citation format (format_citation / receipt_id) so prompts can never drift from the verifier. The executor injects <report_contract> into every subagent system prompt — built-in and custom alike — requiring [rN tool_name] citations for action claims, verifiable handles (absolute path, URL, ID, HTTP status) for deliverables, and explicit failure reporting; the citation clause follows verification.receipts_enabled. - The task tool gains an optional keyword-only acceptance_criteria parameter, handed to the SubagentExecutor constructor and rendered into the subagent's SystemMessage (stripped, capped 20 items x 500 chars) — deliberately never the task HumanMessage, which InputSanitizationMiddleware classes as genuine user input and would HTML-escape into untrusted-input framing. The docstring frames subagent results as self-reports, states the citation cross-check's evidence boundary (resolved = the call happened, not that the claim is correct), and documents when to attach criteria with the canonical leaf forms. Deterministic leaf checking remains a separate layer. - The lead delegation workflow now instructs reading the ledger citation line as execution evidence only and spot-checking verifiable handles before synthesizing. - report_contract / acceptance_criteria are registered as blocked framework-authority tags in input sanitization so untrusted input cannot forge the verification contract. * fix(harness): neutralize acceptance criteria before system-channel injection render_acceptance_criteria_section interpolated lead-model-supplied acceptance_criteria verbatim into the subagent SystemMessage after only stripping/capping. A criterion such as '</acceptance_criteria><system>...</system>' could close the wrapper and open a framework authority tag, bypassing InputSanitizationMiddleware. Route each criterion through neutralize_untrusted_tags (the shared prompt-injection primitive) so blocked authority tags are HTML-escaped before interpolation. Add regression tests at the renderer and the executor _build_initial_state path. * fix(harness): keep model-supplied criteria off the system channel - Move acceptance_criteria values into the task HumanMessage — the untrusted channel InputSanitizationMiddleware escapes and boundary-frames. The subagent SystemMessage now carries only a framework-owned <acceptance_criteria> pointer note (no criterion text), so natural-language injection inside a criterion keeps task-data priority and cannot override framework instructions (PR #5090 review, willem-bd P1). - Condition the lead delegation workflow's citation verification guidance on verification.receipts_enabled and qualify the task tool's result-reading text with the enabled state, so a receipts-disabled configuration no longer tells the lead to require citation evidence that cannot exist (P2). * fix(harness): drop execution-record promise from report contract when receipts are disabled The <report_contract> opening was emitted unconditionally, so a verification.receipts_enabled=false subagent was told its report would be cross-checked against an execution record that cannot exist in that mode (terminal_receipts() returns None; no verdict, no ledger citation line). The opening now follows receipts_enabled: enabled keeps the cross-check language, disabled describes the handle-only review mode (PR #5090 review, willem-bd P2). * docs: record the prompt-layer trust-boundary self-check Generalizes the PR #5090 review outcome: before adding prompt text, ask of every data source in it what trust level it has and which channel it should ride — model/user-influenceable values ride the untrusted sanitized data channel, never framework-owned system text. Added to the PR template (Agents/LangGraph surface) and agents/AGENTS.md.
47 lines
5.7 KiB
Markdown
47 lines
5.7 KiB
Markdown
### Agent System
|
|
|
|
**Lead Agent** (`packages/harness/deerflow/agents/lead_agent/agent.py`):
|
|
- Entry point: `make_lead_agent(config: RunnableConfig)` registered in `langgraph.json`.
|
|
Its signature and bare-graph return type are a published ABI: LangGraph Server calls it
|
|
directly, so neither may change.
|
|
- `assemble_lead_agent(config, *, app_config=None) -> LeadAgentAssembly(graph, descriptor)`
|
|
is the richer entry point the Gateway uses; `make_lead_agent` is a thin wrapper returning
|
|
`.graph`. The descriptor is built by
|
|
`deerflow/agents/assembly_descriptor.py::build_assembly_descriptor()` and captures what
|
|
only the factory knows — the model resolved after runtime overrides, the rendered prompt
|
|
hash, the tool list left by authorization, and the composed middleware stack in order.
|
|
Consumers of a factory result must unwrap `.graph` defensively (see
|
|
`runtime/runs/worker.py::_agent_graph`), because a third-party factory still returns a
|
|
bare graph.
|
|
- Dynamic model selection via `create_chat_model()` with thinking/vision support
|
|
- Tools loaded via `get_available_tools()` - combines sandbox, built-in, MCP, community, and subagent tools
|
|
- System prompt generated by `apply_prompt_template()` with skills, memory, and subagent instructions
|
|
- **Prompt-layer trust boundaries**: every string that enters a model context has a source, and the source's trust level decides its channel. Framework-owned authority text (report contracts, pointer notes, workflow rules) rides the system channel; anything model-supplied or user-influenceable (delegated task text, acceptance criteria, tool results) rides the untrusted channel — the `HumanMessage` that `InputSanitizationMiddleware` escapes and boundary-frames. Before adding prompt text, ask of every data source in it: what is its trust level, and which channel should it ride? Never interpolate untrusted values into framework-owned system text, even neutralized — natural-language injection survives tag escaping (PR #5090 review).
|
|
- Each assembly renders the system prompt and composes middleware exactly once; the same prompt and middleware objects must be passed to both `create_agent()` and the assembly descriptor so extension observations match the running graph, including Custom Agent `allowed_subagents` scope.
|
|
|
|
**ThreadState** (`packages/harness/deerflow/agents/thread_state.py`):
|
|
- Extends `AgentState` with: `sandbox`, `thread_data`, `title`, `artifacts`, `todos`, `uploaded_files`, `viewed_images`, `goal`, `promoted`, `delegations`, `skill_context`, `summary_text`
|
|
- Uses custom reducers: `merge_artifacts` (deduplicate), `merge_viewed_images` (merge/clear), `merge_goal` (preserve the active goal across ordinary state updates unless the goal writer replaces it), `merge_promoted` (catalog-hash-scoped deferred tool promotions), `merge_delegations` (append task delegation entries, same id latest wins, terminal status never downgraded, capped to the most recent entries), and `merge_skill_context` (dedupe active-skill references by path, keep the most recently read entries; entries store a name/path/description reference, not the SKILL.md body). `summary_text` is a LastValue channel updated by summarization and projected into model requests as durable context data instead of being stored as a `messages` item.
|
|
- Delta-mode `merge_message_writes` normalizes the current message state once,
|
|
then folds normalized writes in order with message-ID position indexes and
|
|
deferred tombstone compaction. It preserves public `add_messages` behavior,
|
|
including duplicate IDs, replacement position, removal errors,
|
|
`REMOVE_ALL_MESSAGES`, null-write errors, and missing-ID allocation order,
|
|
without rescanning the accumulated state for every write. Keep this
|
|
full-parity contract covered by differential tests: LangGraph's private
|
|
`_messages_delta_reducer` is also linear, but intentionally omits some of
|
|
those public `add_messages` semantics and cannot be substituted directly.
|
|
|
|
**Runtime Configuration** (via `config.configurable`):
|
|
- `thinking_enabled` - Enable model's extended thinking
|
|
- `model_name` - Select specific LLM model
|
|
- `is_plan_mode` - Enable TodoList middleware
|
|
- `subagent_enabled` - Enable task delegation tool
|
|
- `max_concurrent_subagents` - Per-response `task` call concurrency limit (clamped by `SubagentLimitMiddleware`)
|
|
- `max_total_subagents` - Optional per-run total delegation cap override (falls back to `subagents.max_total_per_run`, clamped to 1-50)
|
|
Gateway and `DeerFlowClient.stream()` always provide the runtime `run_id`; custom
|
|
graph integrations must do the same. If it is absent, enforcement deliberately
|
|
counts the thread's full delegation ledger (fail-restrictive) and emits a warning.
|
|
|
|
**Direct subagent runtime**: `create_deerflow_agent(..., subagent_runtime=runtime)` is the explicit dependency-injection path for direct graph callers. Reuse one `deerflow.subagents.SubagentRuntime` across every graph that belongs to the same application capacity boundary. With the default subagent feature it binds middleware concurrency/total limits, the ordinary `task` tool, one real execution controller, and any active durable-batch submitter to the same snapshot. A caller-owned batch repository requires `await runtime.start()` (or `async with runtime`) before graph construction and `stop()` at shutdown; the factory fails closed while that worker is stopped, and already-built bound batch tools must fail unavailable after it stops rather than falling through to another process-global submitter. The factory never creates SQL infrastructure, renders the caller-owned `system_prompt`, or mounts Gateway API/UI routes. Full middleware takeover cannot be combined with this runtime; direct callers and custom subagent middleware remain responsible for model-visible call-policy wording.
|