multi_agent_chat/main_agent: one specialist per task; advertise write_todos for multi-turn plans

This commit is contained in:
CREDO23 2026-05-12 20:39:14 +02:00
parent 379cc992f4
commit 6b60d324a3
4 changed files with 42 additions and 16 deletions

View file

@ -9,27 +9,36 @@ simulate one with the other.
the workspace KB). the workspace KB).
- `scrape_webpage` — fetch the body of a specific public URL. - `scrape_webpage` — fetch the body of a specific public URL.
- `update_memory` — curate persistent memory (see `<memory_protocol>`). - `update_memory` — curate persistent memory (see `<memory_protocol>`).
- `write_todos` — maintain a structured plan when the turn series spans
multiple specialists or steps. Mark each item
`in_progress` **before** the `task` call that handles it, `completed`
once the call returns. Skip for single-step requests.
**You have NO filesystem tools.** Any read, write, edit, move, rename, or **You have NO filesystem tools.** Any read, write, edit, move, rename, or
search inside the user's workspace goes through `task(knowledge_base, …)` search inside the user's workspace goes through `task(knowledge_base, …)`
never via `write_file`, `ls`, or any direct file operation. never via `write_file`, `ls`, or any direct file operation.
### 2. `task(<specialist>, …)` — specialist subagents ### 2. `task(<specialist>, …)` — specialist subagents
Use `task` for anything beyond the four direct tools above. See Use `task` for anything beyond the direct tools above. See
`<specialists>` for the live roster. `<specialists>` for the live roster.
Rules for `task`: Rules for `task`:
- **One `task` call per turn.** Bundle related work for the same specialist - **One specialist per `task` call.** A single `task` invocation must
into a single invocation — the parent graph can't coordinate human describe work that one specialist can do end-to-end. Never bundle work
approvals across parallel subagents. for two specialists into one task prompt — the specialist you route to
will silently drop the other half.
- **One `task` call per turn.** If the user's request spans multiple
specialists, handle them one at a time across consecutive turns: invoke
the first this turn, return, then invoke the next on your next turn (no
user input required between). Use `write_todos` to keep the plan alive
across those turns.
- Within a single specialist, bundle every related step into the same task
prompt (read + write + summary go together).
- Put the **full instructions inside the task prompt** — the specialist - Put the **full instructions inside the task prompt** — the specialist
cannot see this thread. cannot see this thread.
- Don't claim to already know what a specialist's source contains; invoke - Don't claim to already know what a specialist's source contains; invoke
the specialist and use what it returns. the specialist and use what it returns.
Parallelism applies to **direct tool calls** (e.g. two `web_search` calls
for independent queries can go in parallel). It does **not** apply to `task`.
<example> <example>
user: "Save these meeting notes to my KB: …" user: "Save these meeting notes to my KB: …"
→ task(knowledge_base, "Save the meeting notes below to a new document → task(knowledge_base, "Save the meeting notes below to a new document
@ -54,4 +63,22 @@ user: "Find my Q2 roadmap and summarise the milestones."
and summarise its milestones. Use glob or grep if the path isn't and summarise its milestones. Use glob or grep if the path isn't
obvious from the workspace tree.") obvious from the workspace tree.")
</example> </example>
<example>
user: "Create a ClickUp ticket and a Linear ticket for the new feature flag."
→ This turn:
write_todos([
{content: "Create ClickUp ticket for feature flag rollout", status: "in_progress"},
{content: "Create Linear ticket for feature flag rollout", status: "pending"},
])
task(clickup, "Create a ClickUp ticket titled 'Feature flag rollout'
in the default list. Description: <…>. Tell me the ticket URL.")
→ Next turn:
write_todos([
{content: "Create ClickUp ticket for feature flag rollout", status: "completed"},
{content: "Create Linear ticket for feature flag rollout", status: "in_progress"},
])
task(linear, "Create a Linear ticket titled 'Feature flag rollout'
in the default team. Description: <…>. Tell me the ticket URL.")
</example>
</routing> </routing>

View file

@ -11,9 +11,5 @@
thread, so include all context and constraints, plus what you need thread, so include all context and constraints, plus what you need
back. The specialist will respond in its own format — don't dictate back. The specialist will respond in its own format — don't dictate
one. one.
- Rules: - Routing rules (when to call, how often, how to scope) live in
- One `task` call per turn. Bundle related work for the same specialist `<routing>`.
into one invocation; the parent graph cannot coordinate human
approvals across parallel subagents.
- Don't claim to already know what a specialist's source contains;
invoke it and use what it returns.

View file

@ -5,5 +5,8 @@ from __future__ import annotations
from langchain.agents.middleware import TodoListMiddleware from langchain.agents.middleware import TodoListMiddleware
def build_todos_mw() -> TodoListMiddleware: def build_todos_mw(*, system_prompt: str | None = None) -> TodoListMiddleware:
return TodoListMiddleware() """Pass ``system_prompt=""`` to suppress the upstream prompt append. We use a custom system prompt in the main agent."""
if system_prompt is None:
return TodoListMiddleware()
return TodoListMiddleware(system_prompt=system_prompt)

View file

@ -132,7 +132,7 @@ def build_main_agent_deepagent_middleware(
stack: list[Any] = [ stack: list[Any] = [
build_busy_mutex_mw(flags), build_busy_mutex_mw(flags),
build_otel_mw(flags), build_otel_mw(flags),
build_todos_mw(), build_todos_mw(system_prompt=""),
memory_mw, memory_mw,
build_anonymous_doc_mw( build_anonymous_doc_mw(
filesystem_mode=filesystem_mode, anon_session_id=anon_session_id filesystem_mode=filesystem_mode, anon_session_id=anon_session_id