diff --git a/agent-schema.json b/agent-schema.json index 48325f409..9cbf56adc 100644 --- a/agent-schema.json +++ b/agent-schema.json @@ -801,7 +801,8 @@ "lsp", "user_prompt", "openapi", - "model_picker" + "model_picker", + "background_agents" ] }, "instruction": { @@ -998,7 +999,8 @@ "a2a", "lsp", "user_prompt", - "model_picker" + "model_picker", + "background_agents" ] } } @@ -1082,6 +1084,13 @@ ] } ] + }, + { + "properties": { + "type": { + "const": "background_agents" + } + } } ] }, diff --git a/docs/concepts/multi-agent/index.md b/docs/concepts/multi-agent/index.md index 6e16ca684..05562a74e 100644 --- a/docs/concepts/multi-agent/index.md +++ b/docs/concepts/multi-agent/index.md @@ -45,6 +45,50 @@ transfer_task( +## Parallel Delegation with Background Agents + +`transfer_task` is **sequential** β€” the coordinator waits for the sub-agent to finish before continuing. When you need to fan out work to multiple agents at the same time, use the `background_agents` toolset instead. + +Add it to your coordinator’s toolsets: + +```yaml +agents: + root: + model: anthropic/claude-sonnet-4-0 + description: Research coordinator + sub_agents: [researcher, analyst, writer] + toolsets: + - type: think + - type: background_agents +``` + +The coordinator can then: + +1. **Dispatch** several tasks at once with `run_background_agent` β€” each returns a task ID immediately +2. **Monitor** progress with `list_background_agents` or `view_background_agent` +3. **Collect** results once tasks complete +4. **Cancel** tasks that are no longer needed with `stop_background_agent` + +```bash +# Start two tasks in parallel +run_background_agent(agent="researcher", task="Find recent papers on LLM agents") +run_background_agent(agent="analyst", task="Analyze our current architecture") + +# Check on all tasks +list_background_agents() + +# Read results when ready +view_background_agent(task_id="agent_task_abc123") +``` + +
+
πŸ’‘ When to use which +
+

transfer_task β€” simple, sequential delegation. Best when the coordinator needs the result before deciding what to do next.

+

background_agents β€” parallel, async delegation. Best when multiple independent tasks can run simultaneously.

+ +
+ ## Example: Development Team ```yaml diff --git a/docs/configuration/tools/index.md b/docs/configuration/tools/index.md index d7e7668ee..7bf4491d5 100644 --- a/docs/configuration/tools/index.md +++ b/docs/configuration/tools/index.md @@ -180,6 +180,31 @@ toolsets: The `transfer_task` tool is automatically available when an agent has `sub_agents`. Allows delegating tasks to sub-agents. No configuration needed β€” it's enabled implicitly. +### Background Agents + +Dispatch work to sub-agents concurrently and collect results asynchronously. Unlike `transfer_task` (which blocks until the sub-agent finishes), background agent tasks run in parallel β€” the orchestrator can start several tasks, do other work, and check on them later. + +```yaml +toolsets: + - type: background_agents +``` + +| Operation | Description | +| ------------------------ | ------------------------------------------------------------------------------------------------ | +| `run_background_agent` | Start a sub-agent task in the background; returns a task ID immediately | +| `list_background_agents` | List all background tasks with their status and runtime | +| `view_background_agent` | View live output or final result of a task by ID | +| `stop_background_agent` | Cancel a running task by ID | + +No configuration options. Requires the agent to have `sub_agents` configured so the background tasks have agents to dispatch to. + +
+
πŸ’‘ Tip +
+

Use background_agents when your orchestrator needs to fan out work to multiple specialists in parallel β€” for example, researching several topics simultaneously or running independent code analyses side by side.

+ +
+ ### LSP (Language Server Protocol) Connect to language servers for code intelligence: go-to-definition, find references, diagnostics, and more. diff --git a/e2e/testdata/cassettes/TestA2AServer_MultiAgent.yaml b/e2e/testdata/cassettes/TestA2AServer_MultiAgent.yaml index e9dc79ed9..2b76595c8 100644 --- a/e2e/testdata/cassettes/TestA2AServer_MultiAgent.yaml +++ b/e2e/testdata/cassettes/TestA2AServer_MultiAgent.yaml @@ -8,7 +8,7 @@ interactions: proto_minor: 1 content_length: 0 host: api.openai.com - body: '{"input":[{"content":[{"text":"You are a multi-agent system, make sure to answer the user query in the most helpful way possible. You have access to these sub-agents:\nName: web | Description: \n\nIMPORTANT: You can ONLY transfer tasks to the agents listed above using their ID. The valid agent names are: web. You MUST NOT attempt to transfer to any other agent IDs - doing so will cause system errors.\n\nIf you are the best to answer the question according to your description, you can answer it.\n\nIf another agent is better for answering the question according to its description, call `transfer_task` function to transfer the question to that agent using the agent''s ID. When transferring, do not generate any text other than the function call.\n\n","type":"input_text"}],"role":"system"},{"content":[{"text":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n","type":"input_text"}],"role":"system"},{"content":"Say hello.","role":"user"}],"model":"gpt-5-mini","tools":[{"strict":true,"parameters":{"additionalProperties":false,"properties":{"agent":{"description":"The name of the agent to transfer the task to.","type":"string"},"expected_output":{"description":"The expected output from the member (optional).","type":"string"},"task":{"description":"A clear and concise description of the task the member should achieve.","type":"string"}},"required":["agent","expected_output","task"],"type":"object"},"name":"transfer_task","description":"Use this function to transfer a task to the selected team member.\n You must provide a clear and concise description of the task the member should achieve AND the expected output.","type":"function"},{"strict":true,"parameters":{"additionalProperties":false,"properties":{"agent":{"description":"The name of the sub-agent to run in the background.","type":"string"},"expected_output":{"description":"The expected output from the agent (optional).","type":["string","null"]},"task":{"description":"A clear and concise description of the task the agent should achieve.","type":"string"}},"required":["agent","expected_output","task"],"type":"object"},"name":"run_background_agent","description":"Start a sub-agent task in the background and return immediately with a task ID.\nUse this to dispatch work to multiple sub-agents concurrently. The sub-agent runs with all tools\npre-approved β€” use only with trusted sub-agents and well-scoped tasks. Check progress with\nview_background_agent and collect results once the task is complete.","type":"function"},{"strict":true,"parameters":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_background_agents","description":"List all background agent tasks with their status and runtime.","type":"function"},{"strict":true,"parameters":{"additionalProperties":false,"properties":{"task_id":{"description":"The ID of the background agent task to view.","type":"string"}},"required":["task_id"],"type":"object"},"name":"view_background_agent","description":"View the output and status of a specific background agent task by task ID. Returns live buffered output if still running, or the final result if complete.","type":"function"},{"strict":true,"parameters":{"additionalProperties":false,"properties":{"task_id":{"description":"The ID of the background agent task to stop.","type":"string"}},"required":["task_id"],"type":"object"},"name":"stop_background_agent","description":"Stop a running background agent task by task ID.","type":"function"}],"stream":true}' + body: '{"max_output_tokens":128000,"input":[{"content":[{"text":"You are a multi-agent system, make sure to answer the user query in the most helpful way possible. You have access to these sub-agents:\nName: web | Description: \n\nIMPORTANT: You can ONLY transfer tasks to the agents listed above using their ID. The valid agent names are: web. You MUST NOT attempt to transfer to any other agent IDs - doing so will cause system errors.\n\nIf you are the best to answer the question according to your description, you can answer it.\n\nIf another agent is better for answering the question according to its description, call `transfer_task` function to transfer the question to that agent using the agent''s ID. When transferring, do not generate any text other than the function call.\n\n","type":"input_text"}],"role":"system"},{"content":[{"text":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n","type":"input_text"}],"role":"system"},{"content":"Say hello.","role":"user"}],"model":"gpt-5-mini","tools":[{"strict":true,"parameters":{"additionalProperties":false,"properties":{"agent":{"description":"The name of the agent to transfer the task to.","type":"string"},"expected_output":{"description":"The expected output from the member (optional).","type":"string"},"task":{"description":"A clear and concise description of the task the member should achieve.","type":"string"}},"required":["agent","expected_output","task"],"type":"object"},"name":"transfer_task","description":"Use this function to transfer a task to the selected team member.\n You must provide a clear and concise description of the task the member should achieve AND the expected output.","type":"function"}],"stream":true}' url: https://api.openai.com/v1/responses method: POST response: @@ -18,63 +18,63 @@ interactions: content_length: -1 body: |+ event: response.created - data: {"type":"response.created","response":{"id":"resp_017a0be17f6092e100696f428b44dc8190abcb4e6563147c7b","object":"response","created_at":1768899211,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-5-mini-2025-08-07","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"medium","summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"function","description":"Use this function to transfer a task to the selected team member.\n You must provide a clear and concise description of the task the member should achieve AND the expected output.","name":"transfer_task","parameters":{"additionalProperties":false,"properties":{"agent":{"description":"The name of the agent to transfer the task to.","type":"string"},"expected_output":{"description":"The expected output from the member (optional).","type":"string"},"task":{"description":"A clear and concise description of the task the member should achieve.","type":"string"}},"required":["agent","expected_output","task"],"type":"object"},"strict":true}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0} + data: {"type":"response.created","response":{"id":"resp_0dd13fe7861c42e30069b02f38d60c819b9f07278d4ddf02c7","object":"response","created_at":1773154104,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":128000,"max_tool_calls":null,"model":"gpt-5-mini-2025-08-07","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"medium","summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"function","description":"Use this function to transfer a task to the selected team member.\n You must provide a clear and concise description of the task the member should achieve AND the expected output.","name":"transfer_task","parameters":{"additionalProperties":false,"properties":{"agent":{"description":"The name of the agent to transfer the task to.","type":"string"},"expected_output":{"description":"The expected output from the member (optional).","type":"string"},"task":{"description":"A clear and concise description of the task the member should achieve.","type":"string"}},"required":["agent","expected_output","task"],"type":"object"},"strict":true}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0} event: response.in_progress - data: {"type":"response.in_progress","response":{"id":"resp_017a0be17f6092e100696f428b44dc8190abcb4e6563147c7b","object":"response","created_at":1768899211,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-5-mini-2025-08-07","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"medium","summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"function","description":"Use this function to transfer a task to the selected team member.\n You must provide a clear and concise description of the task the member should achieve AND the expected output.","name":"transfer_task","parameters":{"additionalProperties":false,"properties":{"agent":{"description":"The name of the agent to transfer the task to.","type":"string"},"expected_output":{"description":"The expected output from the member (optional).","type":"string"},"task":{"description":"A clear and concise description of the task the member should achieve.","type":"string"}},"required":["agent","expected_output","task"],"type":"object"},"strict":true}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1} + data: {"type":"response.in_progress","response":{"id":"resp_0dd13fe7861c42e30069b02f38d60c819b9f07278d4ddf02c7","object":"response","created_at":1773154104,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":128000,"max_tool_calls":null,"model":"gpt-5-mini-2025-08-07","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"medium","summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"function","description":"Use this function to transfer a task to the selected team member.\n You must provide a clear and concise description of the task the member should achieve AND the expected output.","name":"transfer_task","parameters":{"additionalProperties":false,"properties":{"agent":{"description":"The name of the agent to transfer the task to.","type":"string"},"expected_output":{"description":"The expected output from the member (optional).","type":"string"},"task":{"description":"A clear and concise description of the task the member should achieve.","type":"string"}},"required":["agent","expected_output","task"],"type":"object"},"strict":true}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1} event: response.output_item.added - data: {"type":"response.output_item.added","item":{"id":"rs_017a0be17f6092e100696f428bbcb881908bae5905d653b14b","type":"reasoning","summary":[]},"output_index":0,"sequence_number":2} + data: {"type":"response.output_item.added","item":{"id":"rs_0dd13fe7861c42e30069b02f3949fc819bbbd8cb15ef144304","type":"reasoning","summary":[]},"output_index":0,"sequence_number":2} event: response.output_item.done - data: {"type":"response.output_item.done","item":{"id":"rs_017a0be17f6092e100696f428bbcb881908bae5905d653b14b","type":"reasoning","summary":[]},"output_index":0,"sequence_number":3} + data: {"type":"response.output_item.done","item":{"id":"rs_0dd13fe7861c42e30069b02f3949fc819bbbd8cb15ef144304","type":"reasoning","summary":[]},"output_index":0,"sequence_number":3} event: response.output_item.added - data: {"type":"response.output_item.added","item":{"id":"msg_017a0be17f6092e100696f428c75448190b0c22eb9177b49c6","type":"message","status":"in_progress","content":[],"role":"assistant"},"output_index":1,"sequence_number":4} + data: {"type":"response.output_item.added","item":{"id":"msg_0dd13fe7861c42e30069b02f3b2fd4819b809a239129f08d99","type":"message","status":"in_progress","content":[],"role":"assistant"},"output_index":1,"sequence_number":4} event: response.content_part.added - data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_017a0be17f6092e100696f428c75448190b0c22eb9177b49c6","output_index":1,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""},"sequence_number":5} + data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_0dd13fe7861c42e30069b02f3b2fd4819b809a239129f08d99","output_index":1,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""},"sequence_number":5} event: response.output_text.delta - data: {"type":"response.output_text.delta","content_index":0,"delta":"Hello","item_id":"msg_017a0be17f6092e100696f428c75448190b0c22eb9177b49c6","logprobs":[],"obfuscation":"vxXNOb950tO","output_index":1,"sequence_number":6} + data: {"type":"response.output_text.delta","content_index":0,"delta":"Hello","item_id":"msg_0dd13fe7861c42e30069b02f3b2fd4819b809a239129f08d99","logprobs":[],"obfuscation":"I3WHiKZ0kIW","output_index":1,"sequence_number":6} event: response.output_text.delta - data: {"type":"response.output_text.delta","content_index":0,"delta":"!","item_id":"msg_017a0be17f6092e100696f428c75448190b0c22eb9177b49c6","logprobs":[],"obfuscation":"BYwakiuYNI7rVIB","output_index":1,"sequence_number":7} + data: {"type":"response.output_text.delta","content_index":0,"delta":"!","item_id":"msg_0dd13fe7861c42e30069b02f3b2fd4819b809a239129f08d99","logprobs":[],"obfuscation":"IMGSZBvG6XOtn2N","output_index":1,"sequence_number":7} event: response.output_text.delta - data: {"type":"response.output_text.delta","content_index":0,"delta":" How","item_id":"msg_017a0be17f6092e100696f428c75448190b0c22eb9177b49c6","logprobs":[],"obfuscation":"fiGOlgM1BX7M","output_index":1,"sequence_number":8} + data: {"type":"response.output_text.delta","content_index":0,"delta":" How","item_id":"msg_0dd13fe7861c42e30069b02f3b2fd4819b809a239129f08d99","logprobs":[],"obfuscation":"RS105oCXZGyR","output_index":1,"sequence_number":8} event: response.output_text.delta - data: {"type":"response.output_text.delta","content_index":0,"delta":" can","item_id":"msg_017a0be17f6092e100696f428c75448190b0c22eb9177b49c6","logprobs":[],"obfuscation":"QUMjcLMovDA3","output_index":1,"sequence_number":9} + data: {"type":"response.output_text.delta","content_index":0,"delta":" can","item_id":"msg_0dd13fe7861c42e30069b02f3b2fd4819b809a239129f08d99","logprobs":[],"obfuscation":"6Pmqi8l1renh","output_index":1,"sequence_number":9} event: response.output_text.delta - data: {"type":"response.output_text.delta","content_index":0,"delta":" I","item_id":"msg_017a0be17f6092e100696f428c75448190b0c22eb9177b49c6","logprobs":[],"obfuscation":"MLwWX19sdoThLv","output_index":1,"sequence_number":10} + data: {"type":"response.output_text.delta","content_index":0,"delta":" I","item_id":"msg_0dd13fe7861c42e30069b02f3b2fd4819b809a239129f08d99","logprobs":[],"obfuscation":"AC9F80vlBA7zUT","output_index":1,"sequence_number":10} event: response.output_text.delta - data: {"type":"response.output_text.delta","content_index":0,"delta":" help","item_id":"msg_017a0be17f6092e100696f428c75448190b0c22eb9177b49c6","logprobs":[],"obfuscation":"zeTGmLYmrfa","output_index":1,"sequence_number":11} + data: {"type":"response.output_text.delta","content_index":0,"delta":" help","item_id":"msg_0dd13fe7861c42e30069b02f3b2fd4819b809a239129f08d99","logprobs":[],"obfuscation":"z74fRJnF9yH","output_index":1,"sequence_number":11} event: response.output_text.delta - data: {"type":"response.output_text.delta","content_index":0,"delta":" you","item_id":"msg_017a0be17f6092e100696f428c75448190b0c22eb9177b49c6","logprobs":[],"obfuscation":"ksloLAJCqJ9h","output_index":1,"sequence_number":12} + data: {"type":"response.output_text.delta","content_index":0,"delta":" you","item_id":"msg_0dd13fe7861c42e30069b02f3b2fd4819b809a239129f08d99","logprobs":[],"obfuscation":"rwQKzdw2KvCO","output_index":1,"sequence_number":12} event: response.output_text.delta - data: {"type":"response.output_text.delta","content_index":0,"delta":" today","item_id":"msg_017a0be17f6092e100696f428c75448190b0c22eb9177b49c6","logprobs":[],"obfuscation":"AO7ffFSpli","output_index":1,"sequence_number":13} + data: {"type":"response.output_text.delta","content_index":0,"delta":" today","item_id":"msg_0dd13fe7861c42e30069b02f3b2fd4819b809a239129f08d99","logprobs":[],"obfuscation":"JIhkFLxDnq","output_index":1,"sequence_number":13} event: response.output_text.delta - data: {"type":"response.output_text.delta","content_index":0,"delta":"?","item_id":"msg_017a0be17f6092e100696f428c75448190b0c22eb9177b49c6","logprobs":[],"obfuscation":"4tlo7VhS2dnEuHy","output_index":1,"sequence_number":14} + data: {"type":"response.output_text.delta","content_index":0,"delta":"?","item_id":"msg_0dd13fe7861c42e30069b02f3b2fd4819b809a239129f08d99","logprobs":[],"obfuscation":"IFHv0djKG4QQqAb","output_index":1,"sequence_number":14} event: response.output_text.done - data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_017a0be17f6092e100696f428c75448190b0c22eb9177b49c6","logprobs":[],"output_index":1,"sequence_number":15,"text":"Hello! How can I help you today?"} + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_0dd13fe7861c42e30069b02f3b2fd4819b809a239129f08d99","logprobs":[],"output_index":1,"sequence_number":15,"text":"Hello! How can I help you today?"} event: response.content_part.done - data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_017a0be17f6092e100696f428c75448190b0c22eb9177b49c6","output_index":1,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"Hello! How can I help you today?"},"sequence_number":16} + data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_0dd13fe7861c42e30069b02f3b2fd4819b809a239129f08d99","output_index":1,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"Hello! How can I help you today?"},"sequence_number":16} event: response.output_item.done - data: {"type":"response.output_item.done","item":{"id":"msg_017a0be17f6092e100696f428c75448190b0c22eb9177b49c6","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"Hello! How can I help you today?"}],"role":"assistant"},"output_index":1,"sequence_number":17} + data: {"type":"response.output_item.done","item":{"id":"msg_0dd13fe7861c42e30069b02f3b2fd4819b809a239129f08d99","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"Hello! How can I help you today?"}],"role":"assistant"},"output_index":1,"sequence_number":17} event: response.completed - data: {"type":"response.completed","response":{"id":"resp_017a0be17f6092e100696f428b44dc8190abcb4e6563147c7b","object":"response","created_at":1768899211,"status":"completed","background":false,"completed_at":1768899212,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-5-mini-2025-08-07","output":[{"id":"rs_017a0be17f6092e100696f428bbcb881908bae5905d653b14b","type":"reasoning","summary":[]},{"id":"msg_017a0be17f6092e100696f428c75448190b0c22eb9177b49c6","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"Hello! How can I help you today?"}],"role":"assistant"}],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"medium","summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"function","description":"Use this function to transfer a task to the selected team member.\n You must provide a clear and concise description of the task the member should achieve AND the expected output.","name":"transfer_task","parameters":{"additionalProperties":false,"properties":{"agent":{"description":"The name of the agent to transfer the task to.","type":"string"},"expected_output":{"description":"The expected output from the member (optional).","type":"string"},"task":{"description":"A clear and concise description of the task the member should achieve.","type":"string"}},"required":["agent","expected_output","task"],"type":"object"},"strict":true}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":292,"input_tokens_details":{"cached_tokens":0},"output_tokens":15,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":307},"user":null,"metadata":{}},"sequence_number":18} + data: {"type":"response.completed","response":{"id":"resp_0dd13fe7861c42e30069b02f38d60c819b9f07278d4ddf02c7","object":"response","created_at":1773154104,"status":"completed","background":false,"completed_at":1773154107,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":128000,"max_tool_calls":null,"model":"gpt-5-mini-2025-08-07","output":[{"id":"rs_0dd13fe7861c42e30069b02f3949fc819bbbd8cb15ef144304","type":"reasoning","summary":[]},{"id":"msg_0dd13fe7861c42e30069b02f3b2fd4819b809a239129f08d99","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"Hello! How can I help you today?"}],"role":"assistant"}],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"medium","summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"function","description":"Use this function to transfer a task to the selected team member.\n You must provide a clear and concise description of the task the member should achieve AND the expected output.","name":"transfer_task","parameters":{"additionalProperties":false,"properties":{"agent":{"description":"The name of the agent to transfer the task to.","type":"string"},"expected_output":{"description":"The expected output from the member (optional).","type":"string"},"task":{"description":"A clear and concise description of the task the member should achieve.","type":"string"}},"required":["agent","expected_output","task"],"type":"object"},"strict":true}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":292,"input_tokens_details":{"cached_tokens":0},"output_tokens":166,"output_tokens_details":{"reasoning_tokens":128},"total_tokens":458},"user":null,"metadata":{}},"sequence_number":18} headers: {} status: 200 OK code: 200 - duration: 773.113833ms + duration: 470.361084ms diff --git a/examples/README.md b/examples/README.md index 013166a6a..61acdb803 100644 --- a/examples/README.md +++ b/examples/README.md @@ -56,3 +56,4 @@ These examples are groups of agents working together. Each of them is specialize | [finance.yaml](finance.yaml) | Financial research and analysis | | | | βœ“ | | [duckduckgo](https://hub.docker.com/mcp/server/duckduckgo/overview) | βœ“ | | [shared-todo.yaml](shared-todo.yaml) | Shared todo item manager | | | βœ“ | | | | βœ“ | | [pr-reviewer-bedrock.yaml](pr-reviewer-bedrock.yaml) | PR review toolkit (Bedrock) | βœ“ | βœ“ | | | | | βœ“ | +| [background_agents.yaml](background_agents.yaml) | Parallel research with background agents | | | | βœ“ | | [duckduckgo](https://hub.docker.com/mcp/server/duckduckgo/overview) | βœ“ | diff --git a/examples/background_agents.yaml b/examples/background_agents.yaml new file mode 100644 index 000000000..55fd90ef4 --- /dev/null +++ b/examples/background_agents.yaml @@ -0,0 +1,49 @@ +#!/usr/bin/env docker agent run + +# This example demonstrates the background_agents toolset, which lets a +# coordinator dispatch work to multiple sub-agents in parallel rather than +# waiting for each one sequentially. +# +# The coordinator fans out research tasks to two specialists at the same time, +# monitors their progress, and synthesises a final answer once both are done. + +agents: + root: + model: anthropic/claude-sonnet-4-0 + description: Research coordinator that dispatches work in parallel + instruction: | + You are a research coordinator. When the user asks a question: + + 1. Break the question into independent research tasks. + 2. Dispatch each task to the appropriate sub-agent using + `run_background_agent` so they run in parallel. + 3. Use `list_background_agents` or `view_background_agent` to check + progress and collect results. + 4. Synthesise the findings into a clear, well-structured answer. + + Always run independent tasks concurrently β€” do not wait for one to + finish before starting the next. + sub_agents: [web_researcher, code_analyst] + toolsets: + - type: think + - type: background_agents + + web_researcher: + model: openai/gpt-4o + description: Searches the web for information and summarises findings + instruction: | + You are a web researcher. Use the search tools to find relevant, + up-to-date information. Provide concise summaries with sources. + toolsets: + - type: mcp + ref: docker:duckduckgo + + code_analyst: + model: anthropic/claude-sonnet-4-0 + description: Analyses local code and provides technical insights + instruction: | + You are a code analyst. Read the codebase to answer technical + questions. Reference specific files and line numbers in your answers. + toolsets: + - type: filesystem + - type: shell diff --git a/pkg/config/latest/validate.go b/pkg/config/latest/validate.go index d9f4cf310..87f4fecc5 100644 --- a/pkg/config/latest/validate.go +++ b/pkg/config/latest/validate.go @@ -152,6 +152,8 @@ func (t *Toolset) validate() error { if len(t.Models) == 0 { return errors.New("model_picker toolset requires at least one model in the 'models' list") } + case "background_agents": + // no additional validation needed } return nil diff --git a/pkg/runtime/loop.go b/pkg/runtime/loop.go index 488e20b43..638672bb6 100644 --- a/pkg/runtime/loop.go +++ b/pkg/runtime/loop.go @@ -24,16 +24,8 @@ import ( "github.com/docker/docker-agent/pkg/telemetry" "github.com/docker/docker-agent/pkg/tools" "github.com/docker/docker-agent/pkg/tools/builtin" - agenttool "github.com/docker/docker-agent/pkg/tools/builtin/agent" ) -// bgAgentHandler wraps a background-agent method as a ToolHandlerFunc. -func (r *LocalRuntime) bgAgentHandler(fn func(context.Context, *session.Session, tools.ToolCall) (*tools.ToolCallResult, error)) ToolHandlerFunc { - return func(ctx context.Context, sess *session.Session, tc tools.ToolCall, _ chan Event) (*tools.ToolCallResult, error) { - return fn(ctx, sess, tc) - } -} - // registerDefaultTools wires up the built-in tool handlers (delegation, // background agents, model switching) into the runtime's tool dispatch map. func (r *LocalRuntime) registerDefaultTools() { @@ -41,10 +33,12 @@ func (r *LocalRuntime) registerDefaultTools() { r.toolMap[builtin.ToolNameHandoff] = r.handleHandoff r.toolMap[builtin.ToolNameChangeModel] = r.handleChangeModel r.toolMap[builtin.ToolNameRevertModel] = r.handleRevertModel - r.toolMap[agenttool.ToolNameRunBackgroundAgent] = r.bgAgentHandler(r.bgAgents.HandleRun) - r.toolMap[agenttool.ToolNameListBackgroundAgents] = r.bgAgentHandler(r.bgAgents.HandleList) - r.toolMap[agenttool.ToolNameViewBackgroundAgent] = r.bgAgentHandler(r.bgAgents.HandleView) - r.toolMap[agenttool.ToolNameStopBackgroundAgent] = r.bgAgentHandler(r.bgAgents.HandleStop) + + r.bgAgents.RegisterHandlers(func(name string, fn func(context.Context, *session.Session, tools.ToolCall) (*tools.ToolCallResult, error)) { + r.toolMap[name] = func(ctx context.Context, sess *session.Session, tc tools.ToolCall, _ chan Event) (*tools.ToolCallResult, error) { + return fn(ctx, sess, tc) + } + }) } // finalizeEventChannel performs cleanup at the end of a RunStream goroutine: diff --git a/pkg/teamloader/registry.go b/pkg/teamloader/registry.go index 983ba5250..d9275991e 100644 --- a/pkg/teamloader/registry.go +++ b/pkg/teamloader/registry.go @@ -20,6 +20,7 @@ import ( "github.com/docker/docker-agent/pkg/tools" "github.com/docker/docker-agent/pkg/tools/a2a" "github.com/docker/docker-agent/pkg/tools/builtin" + agenttool "github.com/docker/docker-agent/pkg/tools/builtin/agent" "github.com/docker/docker-agent/pkg/tools/mcp" ) @@ -77,6 +78,7 @@ func NewDefaultToolsetRegistry() *ToolsetRegistry { r.Register("user_prompt", createUserPromptTool) r.Register("openapi", createOpenAPITool) r.Register("model_picker", createModelPickerTool) + r.Register("background_agents", createBackgroundAgentsTool) return r } @@ -347,3 +349,7 @@ func createModelPickerTool(_ context.Context, toolset latest.Toolset, _ string, } return builtin.NewModelPickerTool(toolset.Models), nil } + +func createBackgroundAgentsTool(_ context.Context, _ latest.Toolset, _ string, _ *config.RuntimeConfig, _ string) (tools.ToolSet, error) { + return agenttool.NewToolSet(), nil +} diff --git a/pkg/teamloader/teamloader.go b/pkg/teamloader/teamloader.go index adf6bfffd..f18d1d531 100644 --- a/pkg/teamloader/teamloader.go +++ b/pkg/teamloader/teamloader.go @@ -27,7 +27,6 @@ import ( "github.com/docker/docker-agent/pkg/team" "github.com/docker/docker-agent/pkg/tools" "github.com/docker/docker-agent/pkg/tools/builtin" - agenttool "github.com/docker/docker-agent/pkg/tools/builtin/agent" "github.com/docker/docker-agent/pkg/tools/codemode" ) @@ -490,7 +489,7 @@ func getToolsForAgent(ctx context.Context, a *latest.AgentConfig, parentDir stri } if len(a.SubAgents) > 0 { - toolSets = append(toolSets, builtin.NewTransferTaskTool(), agenttool.NewToolSet()) + toolSets = append(toolSets, builtin.NewTransferTaskTool()) } if len(a.Handoffs) > 0 { toolSets = append(toolSets, builtin.NewHandoffTool()) diff --git a/pkg/tools/builtin/agent/agent.go b/pkg/tools/builtin/agent/agent.go index fa6f0ea16..811d1c6fb 100644 --- a/pkg/tools/builtin/agent/agent.go +++ b/pkg/tools/builtin/agent/agent.go @@ -384,23 +384,40 @@ func (h *Handler) StopAll() { h.wg.Wait() } -// toolSet is a lightweight ToolSet that returns just the tool definitions -// without requiring a Runner. Used by teamloader to register tool schemas. -type toolSet struct{} +// RegisterHandlers adds all background agent tool handlers to the given +// dispatch map, keyed by tool name. +func (h *Handler) RegisterHandlers(register func(name string, fn func(context.Context, *session.Session, tools.ToolCall) (*tools.ToolCallResult, error))) { + register(ToolNameRunBackgroundAgent, h.HandleRun) + register(ToolNameListBackgroundAgents, h.HandleList) + register(ToolNameViewBackgroundAgent, h.HandleView) + register(ToolNameStopBackgroundAgent, h.HandleStop) +} -// NewToolSet returns a ToolSet for registering background agent tool definitions. -// This does not require a Runner and is suitable for use in teamloader. +// NewToolSet returns a lightweight ToolSet for registering background agent +// tool definitions and instructions. It does not require a Runner and is +// suitable for use in the teamloader registry. func NewToolSet() tools.ToolSet { return &toolSet{} } -func (t *toolSet) Tools(ctx context.Context) ([]tools.Tool, error) { +// toolSet provides tool definitions and instructions without a Runner. +type toolSet struct{} + +func (t *toolSet) Tools(context.Context) ([]tools.Tool, error) { return backgroundAgentTools() } -// Tools returns the four background agent tool definitions. -func (h *Handler) Tools(ctx context.Context) ([]tools.Tool, error) { - return backgroundAgentTools() +func (t *toolSet) Instructions() string { + return `# Background Agent Tasks + +Use background agent tasks to dispatch work to sub-agents concurrently. + +- **run_background_agent**: Start a command, returns task ID. The sub-agent runs with all tools pre-approved β€” use only with trusted sub-agents and well-scoped tasks. +- **list_background_agents**: Show all tasks with status and runtime +- **view_background_agent**: Get output and status of a task by task_id +- **stop_background_agent**: Terminate a task by task_id + +**Notes**: Output capped at 10MB per task. All tasks auto-terminate when the agent stops.` } func backgroundAgentTools() ([]tools.Tool, error) { diff --git a/pkg/tools/builtin/agent/agent_test.go b/pkg/tools/builtin/agent/agent_test.go index 00340ceb2..afe54964f 100644 --- a/pkg/tools/builtin/agent/agent_test.go +++ b/pkg/tools/builtin/agent/agent_test.go @@ -538,9 +538,9 @@ func TestHandler_ConcurrentAccess(t *testing.T) { // --- Tools --- -func TestTools_ReturnsFourTools(t *testing.T) { - h := NewHandler(&mockRunner{}) - toolsList, err := h.Tools(t.Context()) +func TestNewToolSet_ReturnsFourTools(t *testing.T) { + ts := NewToolSet() + toolsList, err := ts.Tools(t.Context()) require.NoError(t, err) assert.Len(t, toolsList, 4) @@ -553,3 +553,16 @@ func TestTools_ReturnsFourTools(t *testing.T) { assert.Contains(t, names, ToolNameViewBackgroundAgent) assert.Contains(t, names, ToolNameStopBackgroundAgent) } + +func TestNewToolSet_Instructions(t *testing.T) { + ts := NewToolSet() + instructable, ok := ts.(tools.Instructable) + require.True(t, ok, "NewToolSet should implement Instructable") + + instructions := instructable.Instructions() + assert.NotEmpty(t, instructions) + assert.Contains(t, instructions, "run_background_agent") + assert.Contains(t, instructions, "list_background_agents") + assert.Contains(t, instructions, "view_background_agent") + assert.Contains(t, instructions, "stop_background_agent") +}