diff --git a/packages/@n8n/agents/src/__tests__/integration/cassettes/delegate-sub-agent/lets-a-real-parent-agent-call-delegate-subagent-and-use-its-result.json b/packages/@n8n/agents/src/__tests__/integration/cassettes/delegate-sub-agent/lets-a-real-parent-agent-call-delegate-subagent-and-use-its-result.json index 78590bea4b6..1f8f573fcda 100644 --- a/packages/@n8n/agents/src/__tests__/integration/cassettes/delegate-sub-agent/lets-a-real-parent-agent-call-delegate-subagent-and-use-its-result.json +++ b/packages/@n8n/agents/src/__tests__/integration/cassettes/delegate-sub-agent/lets-a-real-parent-agent-call-delegate-subagent-and-use-its-result.json @@ -9,7 +9,7 @@ "system": [ { "type": "text", - "text": "\n- delegate_subagent runs a focused child agent in a fresh, isolated context and returns only its final answer. Always set subAgentId. Use subAgentId: \"inline\" to run a one-off inline child that inherits your local and deferred tools after safety filtering. Inline children do not inherit provider-defined tools. The child cannot see this conversation or your memory, so everything it needs must be in the call.\nUse a configured subagent ID only when one is listed and its name/description fits the subtask better than a generic inline child.\nDELEGATION PARALLELISM:\n- Up to 1 child sub-agent run can execute at the same time.\n- This limits parallelism, not the total number of delegated tasks.\n- If more independent workstreams are useful, you may issue more delegate_subagent calls; the runtime will run them in batches.\nWHEN TO USE delegate_subagent:\n- The request decomposes into 2+ independent workstreams that can be handled separately.\n- A workstream needs substantial research, review, comparison, or analysis.\n- Doing the work inline would flood your context with intermediate findings.\n- A fresh isolated perspective would materially improve a bounded subtask.\nWHEN NOT TO USE delegate_subagent:\n- Single-step mechanical work: do it directly.\n- Trivial tasks or one/two tool calls: do them yourself.\n- Tasks that need user interaction or hidden conversation context.\n- Your core synthesis, final judgment, or recommendation.\n- The entire user request as one delegated task; that is pass-through with no value added.\nHOW TO DELEGATE:\n- Delegate bounded workstreams, not the final answer.\n- Pass all required context, constraints, language/tone, and expected output.\n- Set difficulty (low, medium, or high) when you can estimate task complexity; omit it to keep the default inline model.\n- If multiple independent workstreams exist, delegate them separately.\n- Inline children inherit your local and deferred tools after safety filtering. Inline children do not inherit provider-defined tools.\n- Inspect results and synthesize the final response yourself.\n- Verify side-effect claims before presenting them as done.\n\n\nYou are a parent test agent. This is a delegation wiring test: you must call delegate_subagent exactly once before answering. Treat the child task as a bounded independent workstream that only the child should complete. Set subAgentId to \"inline\" in that tool call. The child result will contain a sentinel token. After the tool returns, answer with PARENT_SAW and include the sentinel token from the child result. Do not add unrelated text." + "text": "\n- delegate_subagent runs a focused child agent in a fresh, isolated context and returns only its final answer. Always set subAgentId. Use subAgentId: \"inline\" to run a one-off inline child that inherits your local and deferred tools after safety filtering. Inline children do not inherit provider-defined tools. The child cannot see this conversation or your memory, so everything it needs must be in the call.\nUse a configured subagent ID only when one is listed and its name and useWhen guidance fit the subtask better than a generic inline child.\nDELEGATION PARALLELISM:\n- Up to 1 child sub-agent run can execute at the same time.\n- This limits parallelism, not the total number of delegated tasks.\n- If more independent workstreams are useful, you may issue more delegate_subagent calls; the runtime will run them in batches.\nWHEN TO USE delegate_subagent:\n- The request decomposes into 2+ independent workstreams that can be handled separately.\n- A workstream needs substantial research, review, comparison, or analysis.\n- Doing the work inline would flood your context with intermediate findings.\n- A fresh isolated perspective would materially improve a bounded subtask.\nWHEN NOT TO USE delegate_subagent:\n- Single-step mechanical work: do it directly.\n- Trivial tasks or one/two tool calls: do them yourself.\n- Tasks that need user interaction or hidden conversation context.\n- Your core synthesis, final judgment, or recommendation.\n- The entire user request as one delegated task; that is pass-through with no value added.\nHOW TO DELEGATE:\n- Delegate bounded workstreams, not the final answer.\n- Pass all required context, constraints, language/tone, and expected output.\n- Set difficulty (low, medium, or high) when you can estimate task complexity; omit it to keep the default inline model.\n- If multiple independent workstreams exist, delegate them separately.\n- Inline children inherit your local and deferred tools after safety filtering. Inline children do not inherit provider-defined tools.\n- Inspect results and synthesize the final response yourself.\n- Verify side-effect claims before presenting them as done.\n\n\nYou are a parent test agent. This is a delegation wiring test: you must call delegate_subagent exactly once before answering. Treat the child task as a bounded independent workstream that only the child should complete. Set subAgentId to \"inline\" in that tool call. The child result will contain a sentinel token. After the tool returns, answer with PARENT_SAW and include the sentinel token from the child result. Do not add unrelated text." } ], "messages": [ @@ -70,31 +70,31 @@ } }, "status": 200, - "response": "{\"model\":\"claude-sonnet-4-5-20250929\",\"id\":\"msg_013P1suGmwvtxpsF3AECFvXb\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"I'll delegate the token-production workstream to a child agent now.\"},{\"type\":\"tool_use\",\"id\":\"toolu_01P277sidjrcyB5wjHG5cbwm\",\"name\":\"delegate_subagent\",\"input\":{\"subAgentId\":\"inline\",\"taskName\":\"token_production\",\"goal\":\"Answer with exactly this token and nothing else: SUBAGENT_OK_731\",\"expectedOutput\":\"The exact token SUBAGENT_OK_731 with no additional text\"},\"caller\":{\"type\":\"direct\"}}],\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":1607,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":160,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}", + "response": "{\"model\":\"claude-sonnet-4-5-20250929\",\"id\":\"msg_017GHLkDDD1wzcCyDvgYUhJ6\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"I'll delegate the token-production workstream to a child agent now.\"},{\"type\":\"tool_use\",\"id\":\"toolu_01N4E4babpgjZVvXGb7GK17C\",\"name\":\"delegate_subagent\",\"input\":{\"subAgentId\":\"inline\",\"taskName\":\"token_production\",\"goal\":\"Answer with exactly this token and nothing else: SUBAGENT_OK_731\",\"expectedOutput\":\"The exact token: SUBAGENT_OK_731\"},\"caller\":{\"type\":\"direct\"}}],\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":1610,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":156,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}", "rawHeaders": { - "anthropic-ratelimit-input-tokens-limit": "22500000", - "anthropic-ratelimit-input-tokens-remaining": "22499000", - "anthropic-ratelimit-input-tokens-reset": "2026-06-23T12:22:59Z", - "anthropic-ratelimit-output-tokens-limit": "4500000", - "anthropic-ratelimit-output-tokens-remaining": "4500000", - "anthropic-ratelimit-output-tokens-reset": "2026-06-23T12:23:01Z", + "anthropic-ratelimit-input-tokens-limit": "15000000", + "anthropic-ratelimit-input-tokens-remaining": "14999000", + "anthropic-ratelimit-input-tokens-reset": "2026-06-24T15:23:14Z", + "anthropic-ratelimit-output-tokens-limit": "3000000", + "anthropic-ratelimit-output-tokens-remaining": "3000000", + "anthropic-ratelimit-output-tokens-reset": "2026-06-24T15:23:15Z", "anthropic-ratelimit-requests-limit": "20000", "anthropic-ratelimit-requests-remaining": "19999", - "anthropic-ratelimit-requests-reset": "2026-06-23T12:22:58Z", - "anthropic-ratelimit-tokens-limit": "27000000", - "anthropic-ratelimit-tokens-remaining": "26999000", - "anthropic-ratelimit-tokens-reset": "2026-06-23T12:22:59Z", + "anthropic-ratelimit-requests-reset": "2026-06-24T15:23:13Z", + "anthropic-ratelimit-tokens-limit": "18000000", + "anthropic-ratelimit-tokens-remaining": "17999000", + "anthropic-ratelimit-tokens-reset": "2026-06-24T15:23:14Z", "cf-cache-status": "DYNAMIC", - "cf-ray": "a10376199c775ae8-VIE", + "cf-ray": "a10cbb7dafbd9322-VIE", "connection": "keep-alive", "content-security-policy": "default-src 'none'; frame-ancestors 'none'", "content-type": "application/json", - "date": "Tue, 23 Jun 2026 12:23:01 GMT", - "request-id": "req_011CcLBefJqK86XKkob4XNjR", + "date": "Wed, 24 Jun 2026 15:23:16 GMT", + "request-id": "req_011CcNKCjKeHSTtLUm1MewR5", "server": "cloudflare", - "set-cookie": "_cfuvid=kkraulI32qHCsyrgb3I23N4BKIuSeRz9qzGDuJSr9JY-1782217378.813189-1.0.1.1-zML2VhSp2F7QvxENHSnE5G_szLqUzC8V78NbobpEmkM; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com", + "set-cookie": "_cfuvid=yRGTBHQ2AwzkE77u.SIHx9lxkQlv6m3kU6Nyo.GyYGs-1782314592.908259-1.0.1.1-pHxcgH0O61XBhCR9B6P_2QMnQqbWRA9JBOoRqWC_g3w; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com", "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "traceresponse": "00-e5784162cdaba994773d0e594981ccb3-07413057794f37a7-01", + "traceresponse": "00-982c902dd97461b6e410118578a42517-77e05094d9b1a594-01", "vary": "Accept-Encoding", "x-robots-tag": "none" }, @@ -119,38 +119,38 @@ "content": [ { "type": "text", - "text": "You are a focused subagent working on a specific delegated task.\n\nYOUR TASK:\nAnswer with exactly this token and nothing else: SUBAGENT_OK_731\n\nEXPECTED OUTPUT:\nThe exact token SUBAGENT_OK_731 with no additional text\n\nComplete this task using the tools available to you. When finished, provide a clear, concise summary of:\n- What you did\n- What you found or accomplished\n- Important outputs, decisions, or evidence\n- Any issues, assumptions, or limitations\n\nIf the information above is insufficient, do your best with explicitly stated assumptions and note what was missing, rather than stopping to ask.\n\nBe thorough but concise -- your response is returned to the parent agent as a summary." + "text": "You are a focused subagent working on a specific delegated task.\n\nYOUR TASK:\nAnswer with exactly this token and nothing else: SUBAGENT_OK_731\n\nEXPECTED OUTPUT:\nThe exact token: SUBAGENT_OK_731\n\nComplete this task using the tools available to you. When finished, provide a clear, concise summary of:\n- What you did\n- What you found or accomplished\n- Important outputs, decisions, or evidence\n- Any issues, assumptions, or limitations\n\nIf the information above is insufficient, do your best with explicitly stated assumptions and note what was missing, rather than stopping to ask.\n\nBe thorough but concise -- your response is returned to the parent agent as a summary." } ] } ] }, "status": 200, - "response": "{\"model\":\"claude-sonnet-4-5-20250929\",\"id\":\"msg_01VG1LgqUpmjnsefFbz5pFUY\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"I will complete the delegated task as specified.\\n\\n**SUBAGENT_OK_731**\\n\\n---\\n\\n**Summary:**\\n\\n**What I did:**\\n- Received a task to output a specific token exactly as specified\\n- Provided the exact token \\\"SUBAGENT_OK_731\\\" with no additional text in the response body\\n\\n**What I found or accomplished:**\\n- Successfully completed the task by outputting the required token exactly as requested\\n\\n**Important outputs:**\\n- Output token: SUBAGENT_OK_731\\n\\n**Issues, assumptions, or limitations:**\\n- None. The task was straightforward and clearly specified with no ambiguity.\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":203,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":143,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}", + "response": "{\"model\":\"claude-sonnet-4-5-20250929\",\"id\":\"msg_019vRgqfMmMDCHR2C4KUodd7\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"I have completed the delegated task.\\n\\n**What I did:**\\nI was instructed to respond with exactly one specific token and nothing else.\\n\\n**What I found or accomplished:**\\nThe task was straightforward - output the exact token \\\"SUBAGENT_OK_731\\\"\\n\\n**Important outputs:**\\nSUBAGENT_OK_731\\n\\n**Issues, assumptions, or limitations:**\\nNone. The task was clear and unambiguous. I provided the exact token as requested.\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":199,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":105,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}", "rawHeaders": { - "anthropic-ratelimit-input-tokens-limit": "22500000", - "anthropic-ratelimit-input-tokens-remaining": "22500000", - "anthropic-ratelimit-input-tokens-reset": "2026-06-23T12:23:02Z", - "anthropic-ratelimit-output-tokens-limit": "4500000", - "anthropic-ratelimit-output-tokens-remaining": "4500000", - "anthropic-ratelimit-output-tokens-reset": "2026-06-23T12:23:05Z", + "anthropic-ratelimit-input-tokens-limit": "15000000", + "anthropic-ratelimit-input-tokens-remaining": "15000000", + "anthropic-ratelimit-input-tokens-reset": "2026-06-24T15:23:17Z", + "anthropic-ratelimit-output-tokens-limit": "3000000", + "anthropic-ratelimit-output-tokens-remaining": "3000000", + "anthropic-ratelimit-output-tokens-reset": "2026-06-24T15:23:19Z", "anthropic-ratelimit-requests-limit": "20000", "anthropic-ratelimit-requests-remaining": "19999", - "anthropic-ratelimit-requests-reset": "2026-06-23T12:23:01Z", - "anthropic-ratelimit-tokens-limit": "27000000", - "anthropic-ratelimit-tokens-remaining": "27000000", - "anthropic-ratelimit-tokens-reset": "2026-06-23T12:23:02Z", + "anthropic-ratelimit-requests-reset": "2026-06-24T15:23:16Z", + "anthropic-ratelimit-tokens-limit": "18000000", + "anthropic-ratelimit-tokens-remaining": "18000000", + "anthropic-ratelimit-tokens-reset": "2026-06-24T15:23:17Z", "cf-cache-status": "DYNAMIC", - "cf-ray": "a103762a99fc5ae8-VIE", + "cf-ray": "a10cbb918d4b9322-VIE", "connection": "keep-alive", "content-security-policy": "default-src 'none'; frame-ancestors 'none'", "content-type": "application/json", - "date": "Tue, 23 Jun 2026 12:23:05 GMT", - "request-id": "req_011CcLBerz42fDSB4xcYnjCq", + "date": "Wed, 24 Jun 2026 15:23:19 GMT", + "request-id": "req_011CcNKCxxSVg3rYJmspHA4d", "server": "cloudflare", - "set-cookie": "_cfuvid=Ep_XEtlqs71Dz7xoJHhMdc679hVyO3OMA1MEEsaC3Ug-1782217381.5349274-1.0.1.1-7o3r_Ia.BXDgawKOqSGLdtYa1pTx_1LaZ3yP_SyYLz0; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com", + "set-cookie": "_cfuvid=IaP8ET5bCr1y3TJnUHe6gn65xjp0jiwGDN69jPJ1Wxs-1782314596.086939-1.0.1.1-F8tDXRqfVJTMjZrkE_p8onwQsFrC6dHAlRHBmfHA63I; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com", "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "traceresponse": "00-845146c485f6160e1e25ad7b1b8653d4-15a294a7c6af2bf1-01", + "traceresponse": "00-988a77c2d80c7b9ead2168b0364695ae-4a5ebc2ee62121af-01", "vary": "Accept-Encoding", "x-robots-tag": "none" }, @@ -166,7 +166,7 @@ "system": [ { "type": "text", - "text": "\n- delegate_subagent runs a focused child agent in a fresh, isolated context and returns only its final answer. Always set subAgentId. Use subAgentId: \"inline\" to run a one-off inline child that inherits your local and deferred tools after safety filtering. Inline children do not inherit provider-defined tools. The child cannot see this conversation or your memory, so everything it needs must be in the call.\nUse a configured subagent ID only when one is listed and its name/description fits the subtask better than a generic inline child.\nDELEGATION PARALLELISM:\n- Up to 1 child sub-agent run can execute at the same time.\n- This limits parallelism, not the total number of delegated tasks.\n- If more independent workstreams are useful, you may issue more delegate_subagent calls; the runtime will run them in batches.\nWHEN TO USE delegate_subagent:\n- The request decomposes into 2+ independent workstreams that can be handled separately.\n- A workstream needs substantial research, review, comparison, or analysis.\n- Doing the work inline would flood your context with intermediate findings.\n- A fresh isolated perspective would materially improve a bounded subtask.\nWHEN NOT TO USE delegate_subagent:\n- Single-step mechanical work: do it directly.\n- Trivial tasks or one/two tool calls: do them yourself.\n- Tasks that need user interaction or hidden conversation context.\n- Your core synthesis, final judgment, or recommendation.\n- The entire user request as one delegated task; that is pass-through with no value added.\nHOW TO DELEGATE:\n- Delegate bounded workstreams, not the final answer.\n- Pass all required context, constraints, language/tone, and expected output.\n- Set difficulty (low, medium, or high) when you can estimate task complexity; omit it to keep the default inline model.\n- If multiple independent workstreams exist, delegate them separately.\n- Inline children inherit your local and deferred tools after safety filtering. Inline children do not inherit provider-defined tools.\n- Inspect results and synthesize the final response yourself.\n- Verify side-effect claims before presenting them as done.\n\n\nYou are a parent test agent. This is a delegation wiring test: you must call delegate_subagent exactly once before answering. Treat the child task as a bounded independent workstream that only the child should complete. Set subAgentId to \"inline\" in that tool call. The child result will contain a sentinel token. After the tool returns, answer with PARENT_SAW and include the sentinel token from the child result. Do not add unrelated text." + "text": "\n- delegate_subagent runs a focused child agent in a fresh, isolated context and returns only its final answer. Always set subAgentId. Use subAgentId: \"inline\" to run a one-off inline child that inherits your local and deferred tools after safety filtering. Inline children do not inherit provider-defined tools. The child cannot see this conversation or your memory, so everything it needs must be in the call.\nUse a configured subagent ID only when one is listed and its name and useWhen guidance fit the subtask better than a generic inline child.\nDELEGATION PARALLELISM:\n- Up to 1 child sub-agent run can execute at the same time.\n- This limits parallelism, not the total number of delegated tasks.\n- If more independent workstreams are useful, you may issue more delegate_subagent calls; the runtime will run them in batches.\nWHEN TO USE delegate_subagent:\n- The request decomposes into 2+ independent workstreams that can be handled separately.\n- A workstream needs substantial research, review, comparison, or analysis.\n- Doing the work inline would flood your context with intermediate findings.\n- A fresh isolated perspective would materially improve a bounded subtask.\nWHEN NOT TO USE delegate_subagent:\n- Single-step mechanical work: do it directly.\n- Trivial tasks or one/two tool calls: do them yourself.\n- Tasks that need user interaction or hidden conversation context.\n- Your core synthesis, final judgment, or recommendation.\n- The entire user request as one delegated task; that is pass-through with no value added.\nHOW TO DELEGATE:\n- Delegate bounded workstreams, not the final answer.\n- Pass all required context, constraints, language/tone, and expected output.\n- Set difficulty (low, medium, or high) when you can estimate task complexity; omit it to keep the default inline model.\n- If multiple independent workstreams exist, delegate them separately.\n- Inline children inherit your local and deferred tools after safety filtering. Inline children do not inherit provider-defined tools.\n- Inspect results and synthesize the final response yourself.\n- Verify side-effect claims before presenting them as done.\n\n\nYou are a parent test agent. This is a delegation wiring test: you must call delegate_subagent exactly once before answering. Treat the child task as a bounded independent workstream that only the child should complete. Set subAgentId to \"inline\" in that tool call. The child result will contain a sentinel token. After the tool returns, answer with PARENT_SAW and include the sentinel token from the child result. Do not add unrelated text." } ], "messages": [ @@ -188,13 +188,13 @@ }, { "type": "tool_use", - "id": "toolu_01P277sidjrcyB5wjHG5cbwm", + "id": "toolu_01N4E4babpgjZVvXGb7GK17C", "name": "delegate_subagent", "input": { "subAgentId": "inline", "taskName": "token_production", "goal": "Answer with exactly this token and nothing else: SUBAGENT_OK_731", - "expectedOutput": "The exact token SUBAGENT_OK_731 with no additional text" + "expectedOutput": "The exact token: SUBAGENT_OK_731" }, "caller": { "type": "direct" @@ -207,8 +207,8 @@ "content": [ { "type": "tool_result", - "tool_use_id": "toolu_01P277sidjrcyB5wjHG5cbwm", - "content": "{\"status\":\"completed\",\"taskPath\":\"/root/token_production_0\",\"model\":\"anthropic/claude-sonnet-4-5\",\"answer\":\"I will complete the delegated task as specified.\\n\\n**SUBAGENT_OK_731**\\n\\n---\\n\\n**Summary:**\\n\\n**What I did:**\\n- Received a task to output a specific token exactly as specified\\n- Provided the exact token \\\"SUBAGENT_OK_731\\\" with no additional text in the response body\\n\\n**What I found or accomplished:**\\n- Successfully completed the task by outputting the required token exactly as requested\\n\\n**Important outputs:**\\n- Output token: SUBAGENT_OK_731\\n\\n**Issues, assumptions, or limitations:**\\n- None. The task was straightforward and clearly specified with no ambiguity.\",\"usage\":{\"promptTokens\":203,\"completionTokens\":143,\"totalTokens\":346,\"cost\":0.0027540000000000004},\"finishReason\":\"stop\"}" + "tool_use_id": "toolu_01N4E4babpgjZVvXGb7GK17C", + "content": "{\"status\":\"completed\",\"taskPath\":\"/root/token_production_0\",\"model\":\"anthropic/claude-sonnet-4-5\",\"answer\":\"I have completed the delegated task.\\n\\n**What I did:**\\nI was instructed to respond with exactly one specific token and nothing else.\\n\\n**What I found or accomplished:**\\nThe task was straightforward - output the exact token \\\"SUBAGENT_OK_731\\\"\\n\\n**Important outputs:**\\nSUBAGENT_OK_731\\n\\n**Issues, assumptions, or limitations:**\\nNone. The task was clear and unambiguous. I provided the exact token as requested.\",\"usage\":{\"promptTokens\":199,\"completionTokens\":105,\"totalTokens\":304,\"cost\":0.0021720000000000003},\"finishReason\":\"stop\"}" } ] } @@ -260,31 +260,31 @@ } }, "status": 200, - "response": "{\"model\":\"claude-sonnet-4-5-20250929\",\"id\":\"msg_01FfHMpBtevLA5bkBpWQhbq9\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"PARENT_SAW SUBAGENT_OK_731\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":2020,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":16,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}", + "response": "{\"model\":\"claude-sonnet-4-5-20250929\",\"id\":\"msg_01GSFoUGQDanzpR1yLRCiG5y\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"PARENT_SAW SUBAGENT_OK_731\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":1973,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":16,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}", "rawHeaders": { - "anthropic-ratelimit-input-tokens-limit": "22500000", - "anthropic-ratelimit-input-tokens-remaining": "22499000", - "anthropic-ratelimit-input-tokens-reset": "2026-06-23T12:23:06Z", - "anthropic-ratelimit-output-tokens-limit": "4500000", - "anthropic-ratelimit-output-tokens-remaining": "4500000", - "anthropic-ratelimit-output-tokens-reset": "2026-06-23T12:23:06Z", + "anthropic-ratelimit-input-tokens-limit": "15000000", + "anthropic-ratelimit-input-tokens-remaining": "14999000", + "anthropic-ratelimit-input-tokens-reset": "2026-06-24T15:23:21Z", + "anthropic-ratelimit-output-tokens-limit": "3000000", + "anthropic-ratelimit-output-tokens-remaining": "3000000", + "anthropic-ratelimit-output-tokens-reset": "2026-06-24T15:23:21Z", "anthropic-ratelimit-requests-limit": "20000", "anthropic-ratelimit-requests-remaining": "19999", - "anthropic-ratelimit-requests-reset": "2026-06-23T12:23:05Z", - "anthropic-ratelimit-tokens-limit": "27000000", - "anthropic-ratelimit-tokens-remaining": "26999000", - "anthropic-ratelimit-tokens-reset": "2026-06-23T12:23:06Z", + "anthropic-ratelimit-requests-reset": "2026-06-24T15:23:19Z", + "anthropic-ratelimit-tokens-limit": "18000000", + "anthropic-ratelimit-tokens-remaining": "17999000", + "anthropic-ratelimit-tokens-reset": "2026-06-24T15:23:21Z", "cf-cache-status": "DYNAMIC", - "cf-ray": "a10376434ca45ae8-VIE", + "cf-ray": "a10cbba74ee39322-VIE", "connection": "keep-alive", "content-security-policy": "default-src 'none'; frame-ancestors 'none'", "content-type": "application/json", - "date": "Tue, 23 Jun 2026 12:23:06 GMT", - "request-id": "req_011CcLBf9qKSCyAPps7pF1Vn", + "date": "Wed, 24 Jun 2026 15:23:21 GMT", + "request-id": "req_011CcNKDDmvG4FdfVtL1wSja", "server": "cloudflare", - "set-cookie": "_cfuvid=Y6nvwuQv2KwshVckgT1XAXy9N4gky9o4kbqnK.nLBus-1782217385.4859867-1.0.1.1-aBiH2RGHaM15RtHmeHwBFOioCVay6jtTeVibnO34w98; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com", + "set-cookie": "_cfuvid=m1K.YBZteWTIw5NA_FKTz0OHMZqIf_QpvJ3qgaCxl.4-1782314599.567124-1.0.1.1-cxItmSrWj1XVRhMdS2Ceh0_Wu46xXYxMZG3CaiUcR1Q; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com", "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "traceresponse": "00-5d615e7410ff7c79c429590b62ca0f97-115c915793c4d3d7-01", + "traceresponse": "00-ede482cf84ad9a91549945e9611c9464-ab3ae612be2c2e02-01", "vary": "Accept-Encoding", "x-robots-tag": "none" }, diff --git a/packages/@n8n/agents/src/__tests__/integration/cassettes/delegate-sub-agent/stream-emits-sub-agent-lifecycle-chunks-and-events.json b/packages/@n8n/agents/src/__tests__/integration/cassettes/delegate-sub-agent/stream-emits-sub-agent-lifecycle-chunks-and-events.json index 22625f2da8c..8e4f6305dcf 100644 --- a/packages/@n8n/agents/src/__tests__/integration/cassettes/delegate-sub-agent/stream-emits-sub-agent-lifecycle-chunks-and-events.json +++ b/packages/@n8n/agents/src/__tests__/integration/cassettes/delegate-sub-agent/stream-emits-sub-agent-lifecycle-chunks-and-events.json @@ -9,7 +9,7 @@ "system": [ { "type": "text", - "text": "\n- delegate_subagent runs a focused child agent in a fresh, isolated context and returns only its final answer. Always set subAgentId. Use subAgentId: \"inline\" to run a one-off inline child that inherits your local and deferred tools after safety filtering. Inline children do not inherit provider-defined tools. The child cannot see this conversation or your memory, so everything it needs must be in the call.\nUse a configured subagent ID only when one is listed and its name/description fits the subtask better than a generic inline child.\nDELEGATION PARALLELISM:\n- Up to 1 child sub-agent run can execute at the same time.\n- This limits parallelism, not the total number of delegated tasks.\n- If more independent workstreams are useful, you may issue more delegate_subagent calls; the runtime will run them in batches.\nWHEN TO USE delegate_subagent:\n- The request decomposes into 2+ independent workstreams that can be handled separately.\n- A workstream needs substantial research, review, comparison, or analysis.\n- Doing the work inline would flood your context with intermediate findings.\n- A fresh isolated perspective would materially improve a bounded subtask.\nWHEN NOT TO USE delegate_subagent:\n- Single-step mechanical work: do it directly.\n- Trivial tasks or one/two tool calls: do them yourself.\n- Tasks that need user interaction or hidden conversation context.\n- Your core synthesis, final judgment, or recommendation.\n- The entire user request as one delegated task; that is pass-through with no value added.\nHOW TO DELEGATE:\n- Delegate bounded workstreams, not the final answer.\n- Pass all required context, constraints, language/tone, and expected output.\n- Set difficulty (low, medium, or high) when you can estimate task complexity; omit it to keep the default inline model.\n- If multiple independent workstreams exist, delegate them separately.\n- Inline children inherit your local and deferred tools after safety filtering. Inline children do not inherit provider-defined tools.\n- Inspect results and synthesize the final response yourself.\n- Verify side-effect claims before presenting them as done.\n\n\nYou are a parent streaming test agent. You must call delegate_subagent exactly once before answering. Set subAgentId to \"inline\" in that tool call. The child result will contain a sentinel token. After the tool returns, answer with PARENT_STREAM_SAW and include the sentinel token from the child result. Do not add unrelated text." + "text": "\n- delegate_subagent runs a focused child agent in a fresh, isolated context and returns only its final answer. Always set subAgentId. Use subAgentId: \"inline\" to run a one-off inline child that inherits your local and deferred tools after safety filtering. Inline children do not inherit provider-defined tools. The child cannot see this conversation or your memory, so everything it needs must be in the call.\nUse a configured subagent ID only when one is listed and its name and useWhen guidance fit the subtask better than a generic inline child.\nDELEGATION PARALLELISM:\n- Up to 1 child sub-agent run can execute at the same time.\n- This limits parallelism, not the total number of delegated tasks.\n- If more independent workstreams are useful, you may issue more delegate_subagent calls; the runtime will run them in batches.\nWHEN TO USE delegate_subagent:\n- The request decomposes into 2+ independent workstreams that can be handled separately.\n- A workstream needs substantial research, review, comparison, or analysis.\n- Doing the work inline would flood your context with intermediate findings.\n- A fresh isolated perspective would materially improve a bounded subtask.\nWHEN NOT TO USE delegate_subagent:\n- Single-step mechanical work: do it directly.\n- Trivial tasks or one/two tool calls: do them yourself.\n- Tasks that need user interaction or hidden conversation context.\n- Your core synthesis, final judgment, or recommendation.\n- The entire user request as one delegated task; that is pass-through with no value added.\nHOW TO DELEGATE:\n- Delegate bounded workstreams, not the final answer.\n- Pass all required context, constraints, language/tone, and expected output.\n- Set difficulty (low, medium, or high) when you can estimate task complexity; omit it to keep the default inline model.\n- If multiple independent workstreams exist, delegate them separately.\n- Inline children inherit your local and deferred tools after safety filtering. Inline children do not inherit provider-defined tools.\n- Inspect results and synthesize the final response yourself.\n- Verify side-effect claims before presenting them as done.\n\n\nYou are a parent streaming test agent. You must call delegate_subagent exactly once before answering. Set subAgentId to \"inline\" in that tool call. The child result will contain a sentinel token. After the tool returns, answer with PARENT_STREAM_SAW and include the sentinel token from the child result. Do not add unrelated text." } ], "messages": [ @@ -73,34 +73,34 @@ }, "status": 200, "response": [ - "1f8b0800000000000013cd56514fdb30107ee75758f7b297746a0a65236f65420c4d036d30a1699d2c37be25a6ae9dc5175a56e5bf4f4e9ad29496215434f212e5eedcfbbeef7cd7c35b3414b1093a2712e48e444e7b529088d81ce82e4388a0e584a0f986680e132b514304b11685c48eb3c620750e3afd4eafdbeb778f7a47108092fe475cc2bbe1010ece6f7a9fd3ef93cbdee9ddcc7d3d747f4e7a10aca5820072abbd4138a71c09e3f3c6d6101a82e8c7cf001cd98ce7289c35109942eb85c9e1ef024d8c2da344124abbc65634f095c90ae264c7681c4461ffdd6100b18853e4718e829435bc1dd26dfc390ab9cdd79cf519304b7182b9d0bc3f79187fef0dd3756f19802d68d5f43e0087f9ad8a9193c21c22f0ca48914b2fb2f985b927ce13b41081b1c4c5ad505a8c344259b2e553eeed615df585a07ca46d3cde5cfb0d21553289b39af1aadf135e9c239cf9c0ea1501946b99336592b5540cbc11b6a193a8493c8eae0a69a1ab2d6d54cbb005b6b3375a33891a1341c8284556e9ddc9722b8bd8d7914d6d3e862728f862181de528268c2c132c4e95964c246888193b7dbb04b6bdae36fb57596db68269b797247ce49258ab79e1b09911febbe0ddf0fa743c1b5ca6fb1fe2a3e3abf84b32f8789d9285008c98f8734db5b82b46951055b6ac2088e6a5ef42ad7d7b2cf34895634c5096bb2b5cf8b07075ffde386b96c199c849095d19efdb60a737e85940e64370c568e0a53b934388d81094d1cae0cbdcf267611c42c08640c28dcfc5046b905567ae60fccf10f9fd8c7875c22556e85ab4817153ccd95451ca702662d2778c52e5ea39c78491cc584a9549186a8711bbfc763c383d39bfe2179ff8bbfdb0a6f62a48e12cc398505e547f8c35bdab146b5a359f2df05f0383d243d9d5940edb949a1d6d2399961356c0b796a8d571fcc455aa7cc15d6a6df909fb87e526c29b745bf54153fbbf44d869acea0a0000" + "1f8b0800000000000013cd564d73da3010bde75768f6d28be9e03494e05b32cd6492a6f4d0249db67434c2da62052139d61a4819fe7b47361f3681a487b4840be3dd95f7bdb7daf5e2180d456c84ce8901724722a3032948446c06f4902244507342b07c866806232b514304b116b9c486b3c620358e1aadc661f3b0d5ec1c76200025fd4bdc8037c3f3aebcc5cef8f20e9dba6ddfe0f1b93c3dfb0ec1462a0820b3da1b8473ca91303e6f6c0da121887efc0cc0914d7986c2590391c9b55e981cdee76862ac19259250da2d6df912be32694e9cec108d83286cb53b01c4224e90c7190a52d6f07a4873e9cf50c85dbee5599f01d304479809cd5ba3c7f16b6f986c7ae701d89caaa6e3001c6663152327851944e0959122935e64f30b334f9c0fd04204c6121763a1b4e86b84f99cad7ff383032ccbbe5094f7b58d87db8bbf25a4c826715a52aefa3de3c539c2a90f2cfe2280edf95365061b09197823ecc2285193781a631152c3585aead856610b84176fb46612350e0421a30459217b23cdaccc635f4e36b1d9700791ff04d2518662c4c832c1e24469c9c4000d3163276f6bc89eacb24d9f2bb24d2bd05efeda844f5c1b6b35cf1d2ec7867fce79333cfd7425be1e87df7e5f5eb7effb5d4a3f5c1db57c9411237f6e5939eef27ea149912dcd09a2d9dc37a6d6be635679a4ca30a67a5fbc6441c3c7052dfbfbce59b30a4e45464ae8c258e9923dc398f5c0e5fd13afe285ec41c47aa08c5606d7576ccf087b10b01e9070c3ae186109b168d715c23d03e4ebb1b1d997fb86566a37b04297ba9d1837c18c4d14250ca72226fdc02851ae9c7f4c18c98ca544990143ed30625f6e4e4fcecfbad7fcf347de7e17d6f9bd0a72384d3126949f8bef6749f33ac1925ec9eb191aaf80c8bc8ae8a58679587dd572afdb4aa9e6840a85dae2559dd77fb97ecdffe1feb5b13085adf73b2ee77aa57dac5fd507e5a93f20a2d03f260b0000" ], "rawHeaders": { - "anthropic-ratelimit-input-tokens-limit": "22500000", - "anthropic-ratelimit-input-tokens-remaining": "22499000", - "anthropic-ratelimit-input-tokens-reset": "2026-06-23T12:23:07Z", - "anthropic-ratelimit-output-tokens-limit": "4500000", - "anthropic-ratelimit-output-tokens-remaining": "4500000", - "anthropic-ratelimit-output-tokens-reset": "2026-06-23T12:23:07Z", + "anthropic-ratelimit-input-tokens-limit": "15000000", + "anthropic-ratelimit-input-tokens-remaining": "14999000", + "anthropic-ratelimit-input-tokens-reset": "2026-06-24T15:23:21Z", + "anthropic-ratelimit-output-tokens-limit": "3000000", + "anthropic-ratelimit-output-tokens-remaining": "3000000", + "anthropic-ratelimit-output-tokens-reset": "2026-06-24T15:23:21Z", "anthropic-ratelimit-requests-limit": "20000", "anthropic-ratelimit-requests-remaining": "19999", - "anthropic-ratelimit-requests-reset": "2026-06-23T12:23:07Z", - "anthropic-ratelimit-tokens-limit": "27000000", - "anthropic-ratelimit-tokens-remaining": "26999000", - "anthropic-ratelimit-tokens-reset": "2026-06-23T12:23:07Z", + "anthropic-ratelimit-requests-reset": "2026-06-24T15:23:21Z", + "anthropic-ratelimit-tokens-limit": "18000000", + "anthropic-ratelimit-tokens-remaining": "17999000", + "anthropic-ratelimit-tokens-reset": "2026-06-24T15:23:21Z", "cache-control": "no-cache", "cf-cache-status": "DYNAMIC", - "cf-ray": "a103764c7c755ae8-VIE", + "cf-ray": "a10cbbb44b4b9322-VIE", "connection": "keep-alive", "content-encoding": "gzip", "content-security-policy": "default-src 'none'; frame-ancestors 'none'", "content-type": "text/event-stream; charset=utf-8", - "date": "Tue, 23 Jun 2026 12:23:07 GMT", - "request-id": "req_011CcLBfG76zF66ECHuCY2g5", + "date": "Wed, 24 Jun 2026 15:23:23 GMT", + "request-id": "req_011CcNKDNexof1JjuwnUpcax", "server": "cloudflare", - "set-cookie": "_cfuvid=MtVZNObyTkEbb2wzRSoyfft2dtbSQha3KfKVtZsSOxA-1782217386.9553757-1.0.1.1-VREUYigDtvzuJ1ClETwzde5zxTFb2.A1Raw9lLBgjSk; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com", + "set-cookie": "_cfuvid=Qr.PoYRbsI3mPXvRsWz5EAr8goiEcRCl6ur4MPdCbHU-1782314601.6415367-1.0.1.1-QY0RriMJMRYmXRCp11XPHJ5tmveg2R.07pnr5YLNH0g; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com", "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "traceresponse": "00-70d75251771437b4f3b24c142de492d7-a75cbd79a29f1d3e-01", + "traceresponse": "00-5aa343f4916a37ed6040ba3e0c162f6c-70e0f7e5bc0a5f4d-01", "transfer-encoding": "chunked", "vary": "Accept-Encoding", "x-robots-tag": "none" @@ -133,31 +133,31 @@ ] }, "status": 200, - "response": "{\"model\":\"claude-sonnet-4-5-20250929\",\"id\":\"msg_011p8h9KpBzkDX4XqdCj8JHH\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"I have completed the delegated task.\\n\\n**What I did:**\\nI was instructed to respond with exactly one specific token and nothing else.\\n\\n**What I found or accomplished:**\\nThe task required outputting the exact token: SUBAGENT_OK_731\\n\\n**Important outputs:**\\nSUBAGENT_OK_731\\n\\n**Issues, assumptions, or limitations:**\\nNone. The task was straightforward and clearly specified. The instruction was unambiguous and I have provided the exact token as requested.\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":199,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":110,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}", + "response": "{\"model\":\"claude-sonnet-4-5-20250929\",\"id\":\"msg_01Xgado5BFBstkdYLq17RxMF\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"I have completed the delegated task.\\n\\n**What I did:**\\nI was instructed to respond with exactly one specific token and nothing else.\\n\\n**What I accomplished:**\\nI am providing the required output below.\\n\\n**Output:**\\nSUBAGENT_OK_731\\n\\n**Issues/Limitations:**\\nNone. The task was straightforward and clearly specified.\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":199,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":80,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}", "rawHeaders": { - "anthropic-ratelimit-input-tokens-limit": "22500000", - "anthropic-ratelimit-input-tokens-remaining": "22500000", - "anthropic-ratelimit-input-tokens-reset": "2026-06-23T12:23:12Z", - "anthropic-ratelimit-output-tokens-limit": "4500000", - "anthropic-ratelimit-output-tokens-remaining": "4500000", - "anthropic-ratelimit-output-tokens-reset": "2026-06-23T12:23:14Z", + "anthropic-ratelimit-input-tokens-limit": "15000000", + "anthropic-ratelimit-input-tokens-remaining": "15000000", + "anthropic-ratelimit-input-tokens-reset": "2026-06-24T15:23:27Z", + "anthropic-ratelimit-output-tokens-limit": "3000000", + "anthropic-ratelimit-output-tokens-remaining": "3000000", + "anthropic-ratelimit-output-tokens-reset": "2026-06-24T15:23:28Z", "anthropic-ratelimit-requests-limit": "20000", "anthropic-ratelimit-requests-remaining": "19999", - "anthropic-ratelimit-requests-reset": "2026-06-23T12:23:10Z", - "anthropic-ratelimit-tokens-limit": "27000000", - "anthropic-ratelimit-tokens-remaining": "27000000", - "anthropic-ratelimit-tokens-reset": "2026-06-23T12:23:12Z", + "anthropic-ratelimit-requests-reset": "2026-06-24T15:23:24Z", + "anthropic-ratelimit-tokens-limit": "18000000", + "anthropic-ratelimit-tokens-remaining": "18000000", + "anthropic-ratelimit-tokens-reset": "2026-06-24T15:23:27Z", "cf-cache-status": "DYNAMIC", - "cf-ray": "a103765c4a3b5ae8-VIE", + "cf-ray": "a10cbbc67f589322-VIE", "connection": "keep-alive", "content-security-policy": "default-src 'none'; frame-ancestors 'none'", "content-type": "application/json", - "date": "Tue, 23 Jun 2026 12:23:15 GMT", - "request-id": "req_011CcLBfU5RoDp4Rsp8ZquEP", + "date": "Wed, 24 Jun 2026 15:23:28 GMT", + "request-id": "req_011CcNKDbGF82U8woG8hHPoe", "server": "cloudflare", - "set-cookie": "_cfuvid=NX22IWXF_aORrF9rQG1U1SrSOyqNMkwWG5ZkTN_j6KE-1782217389.489297-1.0.1.1-79BtMSn230ciFI02BaFHxGe.ctKqjwTDWvOP3q__Nq0; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com", + "set-cookie": "_cfuvid=o990L1Q3rAXMmjHv0_A0tHE83jHo3gh7rLZDtOvEQug-1782314604.550718-1.0.1.1-aY88bwfzOUsBhfyYeOQ8Wxt.IlG0c1wpNa87pxLgfU8; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com", "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "traceresponse": "00-7543c2e251b5841e697cef7cb9b3ca46-bae1050a7e11d68f-01", + "traceresponse": "00-64f8bdc5a17f1072f6e47d9be48e6490-66756ef550475d8a-01", "vary": "Accept-Encoding", "x-robots-tag": "none" }, @@ -173,7 +173,7 @@ "system": [ { "type": "text", - "text": "\n- delegate_subagent runs a focused child agent in a fresh, isolated context and returns only its final answer. Always set subAgentId. Use subAgentId: \"inline\" to run a one-off inline child that inherits your local and deferred tools after safety filtering. Inline children do not inherit provider-defined tools. The child cannot see this conversation or your memory, so everything it needs must be in the call.\nUse a configured subagent ID only when one is listed and its name/description fits the subtask better than a generic inline child.\nDELEGATION PARALLELISM:\n- Up to 1 child sub-agent run can execute at the same time.\n- This limits parallelism, not the total number of delegated tasks.\n- If more independent workstreams are useful, you may issue more delegate_subagent calls; the runtime will run them in batches.\nWHEN TO USE delegate_subagent:\n- The request decomposes into 2+ independent workstreams that can be handled separately.\n- A workstream needs substantial research, review, comparison, or analysis.\n- Doing the work inline would flood your context with intermediate findings.\n- A fresh isolated perspective would materially improve a bounded subtask.\nWHEN NOT TO USE delegate_subagent:\n- Single-step mechanical work: do it directly.\n- Trivial tasks or one/two tool calls: do them yourself.\n- Tasks that need user interaction or hidden conversation context.\n- Your core synthesis, final judgment, or recommendation.\n- The entire user request as one delegated task; that is pass-through with no value added.\nHOW TO DELEGATE:\n- Delegate bounded workstreams, not the final answer.\n- Pass all required context, constraints, language/tone, and expected output.\n- Set difficulty (low, medium, or high) when you can estimate task complexity; omit it to keep the default inline model.\n- If multiple independent workstreams exist, delegate them separately.\n- Inline children inherit your local and deferred tools after safety filtering. Inline children do not inherit provider-defined tools.\n- Inspect results and synthesize the final response yourself.\n- Verify side-effect claims before presenting them as done.\n\n\nYou are a parent streaming test agent. You must call delegate_subagent exactly once before answering. Set subAgentId to \"inline\" in that tool call. The child result will contain a sentinel token. After the tool returns, answer with PARENT_STREAM_SAW and include the sentinel token from the child result. Do not add unrelated text." + "text": "\n- delegate_subagent runs a focused child agent in a fresh, isolated context and returns only its final answer. Always set subAgentId. Use subAgentId: \"inline\" to run a one-off inline child that inherits your local and deferred tools after safety filtering. Inline children do not inherit provider-defined tools. The child cannot see this conversation or your memory, so everything it needs must be in the call.\nUse a configured subagent ID only when one is listed and its name and useWhen guidance fit the subtask better than a generic inline child.\nDELEGATION PARALLELISM:\n- Up to 1 child sub-agent run can execute at the same time.\n- This limits parallelism, not the total number of delegated tasks.\n- If more independent workstreams are useful, you may issue more delegate_subagent calls; the runtime will run them in batches.\nWHEN TO USE delegate_subagent:\n- The request decomposes into 2+ independent workstreams that can be handled separately.\n- A workstream needs substantial research, review, comparison, or analysis.\n- Doing the work inline would flood your context with intermediate findings.\n- A fresh isolated perspective would materially improve a bounded subtask.\nWHEN NOT TO USE delegate_subagent:\n- Single-step mechanical work: do it directly.\n- Trivial tasks or one/two tool calls: do them yourself.\n- Tasks that need user interaction or hidden conversation context.\n- Your core synthesis, final judgment, or recommendation.\n- The entire user request as one delegated task; that is pass-through with no value added.\nHOW TO DELEGATE:\n- Delegate bounded workstreams, not the final answer.\n- Pass all required context, constraints, language/tone, and expected output.\n- Set difficulty (low, medium, or high) when you can estimate task complexity; omit it to keep the default inline model.\n- If multiple independent workstreams exist, delegate them separately.\n- Inline children inherit your local and deferred tools after safety filtering. Inline children do not inherit provider-defined tools.\n- Inspect results and synthesize the final response yourself.\n- Verify side-effect claims before presenting them as done.\n\n\nYou are a parent streaming test agent. You must call delegate_subagent exactly once before answering. Set subAgentId to \"inline\" in that tool call. The child result will contain a sentinel token. After the tool returns, answer with PARENT_STREAM_SAW and include the sentinel token from the child result. Do not add unrelated text." } ], "messages": [ @@ -195,7 +195,7 @@ }, { "type": "tool_use", - "id": "toolu_01WGkxASh3Cc9BTcQgAHWhto", + "id": "toolu_01BMLaW81YzJT7qbNtpDL45e", "name": "delegate_subagent", "input": { "subAgentId": "inline", @@ -214,8 +214,8 @@ "content": [ { "type": "tool_result", - "tool_use_id": "toolu_01WGkxASh3Cc9BTcQgAHWhto", - "content": "{\"status\":\"completed\",\"taskPath\":\"/root/token_production_0\",\"model\":\"anthropic/claude-sonnet-4-5\",\"answer\":\"I have completed the delegated task.\\n\\n**What I did:**\\nI was instructed to respond with exactly one specific token and nothing else.\\n\\n**What I found or accomplished:**\\nThe task required outputting the exact token: SUBAGENT_OK_731\\n\\n**Important outputs:**\\nSUBAGENT_OK_731\\n\\n**Issues, assumptions, or limitations:**\\nNone. The task was straightforward and clearly specified. The instruction was unambiguous and I have provided the exact token as requested.\",\"usage\":{\"promptTokens\":199,\"completionTokens\":110,\"totalTokens\":309,\"cost\":0.002247},\"finishReason\":\"stop\"}" + "tool_use_id": "toolu_01BMLaW81YzJT7qbNtpDL45e", + "content": "{\"status\":\"completed\",\"taskPath\":\"/root/token_production_0\",\"model\":\"anthropic/claude-sonnet-4-5\",\"answer\":\"I have completed the delegated task.\\n\\n**What I did:**\\nI was instructed to respond with exactly one specific token and nothing else.\\n\\n**What I accomplished:**\\nI am providing the required output below.\\n\\n**Output:**\\nSUBAGENT_OK_731\\n\\n**Issues/Limitations:**\\nNone. The task was straightforward and clearly specified.\",\"usage\":{\"promptTokens\":199,\"completionTokens\":80,\"totalTokens\":279,\"cost\":0.0017970000000000002},\"finishReason\":\"stop\"}" } ] } @@ -270,34 +270,34 @@ }, "status": 200, "response": [ - "1f8b0800000000000013bd53c18ed33010bdf72ba239a752d3dd0ac5b7aea8382c94d5b6500984acd97868adba76d69e540babfc3b72dbb04949810bf812f9bde7bc37331ada936591ec28045c930c8c9e070a1945f20cfcad2410d021216dee209e61e71419105018ac140d83b39678783d9c0cc7a3f164948f734841abf893b096a3ece3a7d7ab79582db7193eaeee6eae6f11f9fb5b48cfac2005ef4c0430041d186df42d9c65b20ce2f3971402bb527ac2e02c085b197382023d56640bea808a18b5090d5635f1b52d2b96ecb66403882cbfca5328b0d8902c3c216b676557326a784fa82e71cddbe840e58676e4d1c8c9ee57fd0b9b6dced93a0557711b1aa710c8ef754192357910103ba3d0abd864fb957c2c5caec98100eb58e21eb5c1074350d749ebd483011de77e6aa97c30aed8f64fbf4772b053f474acb9cdc7924fef989ea2f0f01100759f7da9edfacc2f8108c2a5888a0ce3ef231e249d8847a41beda7ec14f06e7a3f9b2f8f31ffbbb95c2cef67d37772315d258b0f37d337b3f952bebf95afaeb297c65d9e9a2bff343457b632f54da2d9f1de123b24b44aea2c219055922b6fe1af57b1fe87bb78b63c595e5f28b9af7f6d0e3afdfa01fc24417f31050000" + "1f8b0800000000000013b553c18ed33010bdf72ba239a75293a5407d2b520552c502dbae160921cb6b0fa9a96b67ed49d5d52aff8edc36908414f6b2b944796fecf7e64d06f76889253b0c4114c803094f232548b0e409e8b14460d021216dbe813dc1ce2934c0401a51291c07672dd2f8d5783ace27f97432cb67908256f19250f04976f7f60b3dca2bbb5d161f6eed32c89f8787af39a43d2948c13b130111820e246cd495ce125a02f6ed7b0a815cc93d8ae02c305b197386023e54682576408524b4090d5635f6b52d2be2e4b66803b06c96bd4e410ab9412e3d0ad2cef26ec9a4e13d0a75896bce46052c37b8432f0c9feefeaeffc3669b3e5ba7e02a6a43790a01fd5e4be4a4d10383988c125ec590ed0ff4b1715ea00306d611177ba18db83708759d749e7a34c2d3e4cfa1f27be3e47678fe03254741858753d76d3e367d3e47788885c71703a887e44b6d8b9e5e0211844b16151a12ffb6782ce9583c215d6bbfcbce063fcf6f16d76ba89f93d38b99e0abf5cd62fe91afe677c9eaf6ddfcfde27acd3f2df99bab6c38c0fe705cf9bff1b9b2e5ead26fd1ecfc60a31d125a8d759612d02a4e95b7f0ecd5ac5f70377bcb94cdeae18e87226c73d0cdeb17bc9ee7f841050000" ], "rawHeaders": { - "anthropic-ratelimit-input-tokens-limit": "22500000", - "anthropic-ratelimit-input-tokens-remaining": "22499000", - "anthropic-ratelimit-input-tokens-reset": "2026-06-23T12:23:15Z", - "anthropic-ratelimit-output-tokens-limit": "4500000", - "anthropic-ratelimit-output-tokens-remaining": "4500000", - "anthropic-ratelimit-output-tokens-reset": "2026-06-23T12:23:15Z", + "anthropic-ratelimit-input-tokens-limit": "15000000", + "anthropic-ratelimit-input-tokens-remaining": "14999000", + "anthropic-ratelimit-input-tokens-reset": "2026-06-24T15:23:29Z", + "anthropic-ratelimit-output-tokens-limit": "3000000", + "anthropic-ratelimit-output-tokens-remaining": "3000000", + "anthropic-ratelimit-output-tokens-reset": "2026-06-24T15:23:29Z", "anthropic-ratelimit-requests-limit": "20000", "anthropic-ratelimit-requests-remaining": "19999", - "anthropic-ratelimit-requests-reset": "2026-06-23T12:23:15Z", - "anthropic-ratelimit-tokens-limit": "27000000", - "anthropic-ratelimit-tokens-remaining": "26999000", - "anthropic-ratelimit-tokens-reset": "2026-06-23T12:23:15Z", + "anthropic-ratelimit-requests-reset": "2026-06-24T15:23:29Z", + "anthropic-ratelimit-tokens-limit": "18000000", + "anthropic-ratelimit-tokens-remaining": "17999000", + "anthropic-ratelimit-tokens-reset": "2026-06-24T15:23:29Z", "cache-control": "no-cache", "cf-cache-status": "DYNAMIC", - "cf-ray": "a103767f09d65ae8-VIE", + "cf-ray": "a10cbbe199929322-VIE", "connection": "keep-alive", "content-encoding": "gzip", "content-security-policy": "default-src 'none'; frame-ancestors 'none'", "content-type": "text/event-stream; charset=utf-8", - "date": "Tue, 23 Jun 2026 12:23:15 GMT", - "request-id": "req_011CcLBfrhiMsUWCmMQ3bwj4", + "date": "Wed, 24 Jun 2026 15:23:30 GMT", + "request-id": "req_011CcNKDw6rCzLqqZztABmB3", "server": "cloudflare", - "set-cookie": "_cfuvid=PosXDRxmYXU0hIySSq0tVPuZBeQ8yX_6muE5gzcX83A-1782217395.0444734-1.0.1.1-3pG4RyUfKc9AXwzVavWx85YhXPfMSI3DOxoZqZ8PJbk; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com", + "set-cookie": "_cfuvid=JlCLwTYCzW3qeQkENLr33Dso0fO1Rc0pWZae06tB.2I-1782314608.8987586-1.0.1.1-GBwz.c_fHEZPodxvy5pTPQREAWAv2UTftRzLKwJFaNs; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com", "strict-transport-security": "max-age=31536000; includeSubDomains; preload", - "traceresponse": "00-d666b8574e11145503acba879bff02c6-ecab3d6d17b65f5f-01", + "traceresponse": "00-8d715e9a4c968366da5f5cd1970352c4-d0165148611ef5de-01", "transfer-encoding": "chunked", "vary": "Accept-Encoding", "x-robots-tag": "none" diff --git a/packages/@n8n/agents/src/runtime/__tests__/delegate-sub-agent-tool.test.ts b/packages/@n8n/agents/src/runtime/__tests__/delegate-sub-agent-tool.test.ts index 78bbea919cc..804a608572d 100644 --- a/packages/@n8n/agents/src/runtime/__tests__/delegate-sub-agent-tool.test.ts +++ b/packages/@n8n/agents/src/runtime/__tests__/delegate-sub-agent-tool.test.ts @@ -88,6 +88,47 @@ describe('createDelegateSubAgentTool', () => { }); }); + it('renders configured sub-agent useWhen guidance in model-facing instructions', () => { + const tool = createDelegateSubAgentTool({ + availableSubAgents: [ + { + id: 'agent-billing', + name: 'Billing Agent', + useWhen: 'Use for invoice investigations and payment status checks.', + }, + { + id: 'agent-research', + name: 'Research Agent', + useWhen: 'Use for market and source research.', + }, + ], + }); + + expect(tool.systemInstruction).toContain('name and useWhen guidance'); + expect(tool.systemInstruction).toContain('- agent-billing: Billing Agent'); + expect(tool.systemInstruction).toContain( + 'Use when: Use for invoice investigations and payment status checks.', + ); + expect(tool.systemInstruction).toContain('- agent-research: Research Agent'); + expect(tool.systemInstruction).toContain('Use when: Use for market and source research.'); + expect(tool.systemInstruction).not.toContain('name/description'); + }); + + it('preserves available sub-agent useWhen guidance in delegate tool metadata', () => { + const availableSubAgents = [ + { + id: 'agent-billing', + name: 'Billing Agent', + useWhen: 'Use for invoice investigations.', + }, + ]; + const tool = createDelegateSubAgentTool({ availableSubAgents }); + + expect(getInlineDelegateSubAgentToolOptions(tool)?.availableSubAgents).toEqual( + availableSubAgents, + ); + }); + it('preserves resolveInlineSubAgentProviderTools in delegate tool metadata', () => { const resolveInlineSubAgentProviderTools = () => []; const tool = createDelegateSubAgentTool({ diff --git a/packages/@n8n/agents/src/runtime/tools/delegate-sub-agent-tool.ts b/packages/@n8n/agents/src/runtime/tools/delegate-sub-agent-tool.ts index 502e7c67ac0..2b484bcac8c 100644 --- a/packages/@n8n/agents/src/runtime/tools/delegate-sub-agent-tool.ts +++ b/packages/@n8n/agents/src/runtime/tools/delegate-sub-agent-tool.ts @@ -194,7 +194,7 @@ export interface CreateDelegateSubAgentToolOptions { * Sub-agents the model may choose between. Listed in the system prompt; the * model selects one by passing its id as `subAgentId`. */ - availableSubAgents?: Array<{ id: string; name: string; description?: string }>; + availableSubAgents?: Array<{ id: string; name: string; useWhen?: string }>; /** Parallelism limit for delegated child runs (also used as delegate_subagent batch size). */ policy?: DelegateSubAgentPolicy; /** Additional local/deferred tool names the host removes from inline children. */ @@ -280,7 +280,7 @@ export function createDelegateSubAgentTool(options: CreateDelegateSubAgentToolOp .systemInstruction( [ `delegate_subagent runs a focused child agent in a fresh, isolated context and returns only its final answer. Always set subAgentId. Use subAgentId: "inline" to run a one-off inline child that inherits your local and deferred tools after safety filtering. ${inlineProviderToolInstruction} The child cannot see this conversation or your memory, so everything it needs must be in the call.`, - 'Use a configured subagent ID only when one is listed and its name/description fits the subtask better than a generic inline child.', + 'Use a configured subagent ID only when one is listed and its name and useWhen guidance fit the subtask better than a generic inline child.', ...formatAvailableSubAgents(resolvedOptions.availableSubAgents), ...formatDelegationPolicyInstructions(resolvedOptions.policy), 'WHEN TO USE delegate_subagent:\n- The request decomposes into 2+ independent workstreams that can be handled separately.\n- A workstream needs substantial research, review, comparison, or analysis.\n- Doing the work inline would flood your context with intermediate findings.\n- A fresh isolated perspective would materially improve a bounded subtask.', @@ -348,8 +348,8 @@ function formatAvailableSubAgents( return [ 'Configured subagents are available as specialist options. Use subAgentId: "inline" for the default inline child; pass one of these exact IDs only when that specialist is a better fit:', ...availableSubAgents.map((subAgent) => { - const description = subAgent.description ? ` - ${subAgent.description}` : ''; - return `- ${subAgent.id}: ${subAgent.name}${description}`; + const useWhen = subAgent.useWhen ? `\n Use when: ${subAgent.useWhen}` : ''; + return `- ${subAgent.id}: ${subAgent.name}${useWhen}`; }), ]; } diff --git a/packages/@n8n/api-types/src/agents/__tests__/sanitize-agent-json-config.test.ts b/packages/@n8n/api-types/src/agents/__tests__/sanitize-agent-json-config.test.ts index 978dd5d2836..1c257ace3e4 100644 --- a/packages/@n8n/api-types/src/agents/__tests__/sanitize-agent-json-config.test.ts +++ b/packages/@n8n/api-types/src/agents/__tests__/sanitize-agent-json-config.test.ts @@ -50,7 +50,13 @@ describe('sanitizeAgentJsonConfig', () => { ...baseConfig, subAgents: { maxChildren: 3, - agents: [{ agentId: 'agent-2', legacyLabel: 'Research' }], + agents: [ + { + agentId: 'agent-2', + useWhen: 'Use for research tasks.', + legacyLabel: 'Research', + }, + ], legacyBudget: 99, }, config: { @@ -65,7 +71,7 @@ describe('sanitizeAgentJsonConfig', () => { ...baseConfig, subAgents: { maxChildren: 3, - agents: [{ agentId: 'agent-2' }], + agents: [{ agentId: 'agent-2', useWhen: 'Use for research tasks.' }], }, config: { toolCallConcurrency: 2, @@ -372,7 +378,7 @@ describe('sanitizeAgentJsonConfig', () => { ...baseConfig, subAgents: { maxChildren: 3, - agents: [{ agentId: 'agent-2' }], + agents: [{ agentId: 'agent-2', useWhen: 'Use for billing questions.' }], }, }); const parsed = AgentJsonConfigSchema.safeParse(sanitized); @@ -380,6 +386,35 @@ describe('sanitizeAgentJsonConfig', () => { expect(parsed.success).toBe(true); if (!parsed.success) return; expect(parsed.data.subAgents?.maxChildren).toBe(3); + expect(parsed.data.subAgents?.agents?.[0]?.useWhen).toBe('Use for billing questions.'); + }); + + it.each(['', ' ', 'Too short'])('accepts optional sub-agent useWhen value %p', (useWhen) => { + const sanitized = sanitizeAgentJsonConfig({ + ...baseConfig, + subAgents: { + agents: [{ agentId: 'agent-2', useWhen }], + }, + }); + + expect(sanitized).toEqual({ + ...baseConfig, + subAgents: { + agents: [{ agentId: 'agent-2', useWhen }], + }, + }); + expect(AgentJsonConfigSchema.safeParse(sanitized).success).toBe(true); + }); + + it('rejects sub-agent useWhen values over 512 characters', () => { + expect( + AgentJsonConfigSchema.safeParse({ + ...baseConfig, + subAgents: { + agents: [{ agentId: 'agent-2', useWhen: 'a'.repeat(513) }], + }, + }).success, + ).toBe(false); }); it.each([SUB_AGENT_MAX_CHILDREN_MIN, SUB_AGENT_MAX_CHILDREN_MAX])( @@ -411,7 +446,7 @@ describe('sanitizeAgentJsonConfig', () => { ...baseConfig, subAgents: { maxChildren: 3, - agents: [{ agentId: 'agent-2' }], + agents: [{ agentId: 'agent-2', useWhen: 'Use for billing escalations.' }], modelsByDifficulty: { low: { model: 'openai/gpt-4o-mini', credential: 'cred-openai' }, medium: { model: 'anthropic/claude-haiku-4-5', credential: 'cred-anthropic' }, @@ -533,7 +568,7 @@ describe('sanitizeAgentJsonConfig', () => { ...baseConfig, subAgents: { maxChildren: 4, - agents: [{ agentId: 'agent-2' }], + agents: [{ agentId: 'agent-2', useWhen: 'Use for support escalation.' }], modelsByDifficulty: { high: { model: 'anthropic/claude-sonnet-4-5', credential: 'cred-anthropic' }, }, @@ -544,7 +579,7 @@ describe('sanitizeAgentJsonConfig', () => { ...baseConfig, subAgents: { maxChildren: 4, - agents: [{ agentId: 'agent-2' }], + agents: [{ agentId: 'agent-2', useWhen: 'Use for support escalation.' }], modelsByDifficulty: { high: { model: 'anthropic/claude-sonnet-4-5', credential: 'cred-anthropic' }, }, diff --git a/packages/@n8n/api-types/src/agents/agent-json-config.schema.ts b/packages/@n8n/api-types/src/agents/agent-json-config.schema.ts index 67015a36745..e121b7359de 100644 --- a/packages/@n8n/api-types/src/agents/agent-json-config.schema.ts +++ b/packages/@n8n/api-types/src/agents/agent-json-config.schema.ts @@ -69,9 +69,14 @@ const WebSearchConfigSchema = z.object({ credential: z.string().optional(), }); -const SubAgentConfigSchema = z.object({ - agentId: z.string().trim().min(1), -}); +export const SUB_AGENT_USE_WHEN_MAX_LENGTH = 512; + +const SubAgentConfigSchema = z + .object({ + agentId: z.string().trim().min(1), + useWhen: z.string().trim().max(SUB_AGENT_USE_WHEN_MAX_LENGTH).optional(), + }) + .strict(); export const SUB_AGENT_TASK_DIFFICULTIES = ['low', 'medium', 'high'] as const; const SubAgentTaskDifficultySchema = z.enum(SUB_AGENT_TASK_DIFFICULTIES); diff --git a/packages/cli/src/modules/agents/__tests__/agent-config.service.test.ts b/packages/cli/src/modules/agents/__tests__/agent-config.service.test.ts index a6332ba492c..53e271686d4 100644 --- a/packages/cli/src/modules/agents/__tests__/agent-config.service.test.ts +++ b/packages/cli/src/modules/agents/__tests__/agent-config.service.test.ts @@ -278,13 +278,17 @@ describe('AgentConfigService', () => { ...baseConfig, subAgents: { maxChildren: 3, - agents: [{ agentId: 'missing-agent' }, { agentId: 'agent-2' }, { agentId: 'agent-2' }], + agents: [ + { agentId: 'missing-agent', useWhen: 'Use for missing work.' }, + { agentId: 'agent-2', useWhen: 'Use for billing escalations.' }, + { agentId: 'agent-2', useWhen: 'Use for duplicate work.' }, + ], }, }); expect((agentRepository.save.mock.calls[0][0] as Agent).schema?.subAgents).toEqual({ maxChildren: 3, - agents: [{ agentId: 'agent-2' }], + agents: [{ agentId: 'agent-2', useWhen: 'Use for billing escalations.' }], }); expect( agentRepository.findByIdAndProjectId.mock.calls.filter(([id]) => id === 'agent-2'), @@ -293,14 +297,16 @@ describe('AgentConfigService', () => { await expect( service.updateConfig(agentId, projectId, { ...baseConfig, - subAgents: { agents: [{ agentId: 'agent-3' }] }, + subAgents: { + agents: [{ agentId: 'agent-3', useWhen: 'Use for unpublished work.' }], + }, }), ).rejects.toThrow('must be published'); await expect( service.updateConfig(agentId, projectId, { ...baseConfig, - subAgents: { agents: [{ agentId }] }, + subAgents: { agents: [{ agentId, useWhen: 'Use for self-delegation.' }] }, }), ).rejects.toThrow('cannot use itself'); }); diff --git a/packages/cli/src/modules/agents/__tests__/agent-json-config.test.ts b/packages/cli/src/modules/agents/__tests__/agent-json-config.test.ts index 9f0b494c35f..f0b3f0f8393 100644 --- a/packages/cli/src/modules/agents/__tests__/agent-json-config.test.ts +++ b/packages/cli/src/modules/agents/__tests__/agent-json-config.test.ts @@ -36,7 +36,7 @@ describe('AgentJsonConfigSchema — skill refs', () => { }); describe('AgentJsonConfigSchema — subAgents', () => { - it('accepts saved agent references', () => { + it('accepts legacy saved agent references without useWhen routing guidance', () => { const parsed = AgentJsonConfigSchema.safeParse({ ...baseConfig, subAgents: { agents: [{ agentId: 'agent-1' }] }, @@ -44,6 +44,40 @@ describe('AgentJsonConfigSchema — subAgents', () => { expect(parsed.success).toBe(true); }); + it('accepts saved agent references with useWhen routing guidance', () => { + const parsed = AgentJsonConfigSchema.safeParse({ + ...baseConfig, + subAgents: { + agents: [ + { + agentId: 'agent-1', + useWhen: 'Use for billing-policy questions and invoice investigations.', + }, + ], + }, + }); + + expect(parsed.success).toBe(true); + }); + + it.each(['', ' ', 'Too short'])('accepts optional useWhen value %p', (useWhen) => { + const parsed = AgentJsonConfigSchema.safeParse({ + ...baseConfig, + subAgents: { agents: [{ agentId: 'agent-1', useWhen }] }, + }); + + expect(parsed.success).toBe(true); + }); + + it('rejects useWhen values over 512 characters', () => { + const parsed = AgentJsonConfigSchema.safeParse({ + ...baseConfig, + subAgents: { agents: [{ agentId: 'agent-1', useWhen: 'a'.repeat(513) }] }, + }); + + expect(parsed.success).toBe(false); + }); + it('accepts an empty saved-agent reference list', () => { expect( AgentJsonConfigSchema.safeParse({ ...baseConfig, subAgents: { agents: [] } }).success, diff --git a/packages/cli/src/modules/agents/__tests__/agent-telemetry.test.ts b/packages/cli/src/modules/agents/__tests__/agent-telemetry.test.ts index 35df8ad454d..5d7289b6aa3 100644 --- a/packages/cli/src/modules/agents/__tests__/agent-telemetry.test.ts +++ b/packages/cli/src/modules/agents/__tests__/agent-telemetry.test.ts @@ -41,7 +41,7 @@ describe('agent telemetry', () => { 'openai.web_search': { enabled: true }, }, subAgents: { - agents: [{ agentId: 'agent-2' }], + agents: [{ agentId: 'agent-2', useWhen: 'Use for research tasks.' }], }, config: { webSearch: { enabled: true }, diff --git a/packages/cli/src/modules/agents/__tests__/agents-service-reconstruct-gating.test.ts b/packages/cli/src/modules/agents/__tests__/agents-service-reconstruct-gating.test.ts index 26c9a6f1094..16eb41dff88 100644 --- a/packages/cli/src/modules/agents/__tests__/agents-service-reconstruct-gating.test.ts +++ b/packages/cli/src/modules/agents/__tests__/agents-service-reconstruct-gating.test.ts @@ -87,6 +87,7 @@ function makeReconstructionService( modules: string[] = [], overrides: { logger?: Logger; + agentRepository?: AgentRepository; agentsConfig?: Partial; n8nCheckpointStorage?: N8NCheckpointStorage; } = {}, @@ -99,7 +100,7 @@ function makeReconstructionService( outboundHttp.transport.mockReturnValue(transport); return new AgentRuntimeReconstructionService( overrides.logger ?? mock(), - mock(), + overrides.agentRepository ?? mock(), mock(), mock(), mock(), @@ -222,7 +223,9 @@ describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — sub-a }, { name: 'saved-agent references', - subAgents: { agents: [{ agentId: 'agent-2' }] }, + subAgents: { + agents: [{ agentId: 'agent-2', useWhen: 'Use for research tasks.' }], + }, }, ])('always injects delegation tools for $name', async ({ subAgents }) => { const { service, credentialProvider } = setup(); @@ -247,6 +250,18 @@ describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — sub-a return undefined; } + function getInjectedAvailableSubAgents() { + for (const call of builtAgent.tool.mock.calls) { + for (const item of Array.isArray(call[0]) ? call[0] : [call[0]]) { + const tool = item as BuiltTool; + if (tool.name === DELEGATE_SUB_AGENT_TOOL_NAME) { + return getInlineDelegateSubAgentToolOptions(tool)?.availableSubAgents; + } + } + } + return undefined; + } + function getInjectedInlineSubAgentModelsByDifficulty() { for (const call of builtAgent.tool.mock.calls) { for (const item of Array.isArray(call[0]) ? call[0] : [call[0]]) { @@ -298,6 +313,37 @@ describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — sub-a }); }); + it('passes saved sub-agent useWhen guidance into delegate tool metadata', async () => { + const credentialProvider = mock(); + const agentRepository = mock(); + agentRepository.findByIdAndProjectId.mockResolvedValue({ + id: 'agent-billing', + name: 'Billing Agent', + activeVersionId: 'version-billing', + } as Agent); + const service = makeReconstructionService([], { agentRepository }); + const entity = makeAgentEntity(undefined, { + subAgents: { + agents: [ + { + agentId: 'agent-billing', + useWhen: 'Use for invoice investigations and payment status checks.', + }, + ], + }, + }); + + await service.reconstructFromAgentEntity(entity, credentialProvider, 'user-1'); + + expect(getInjectedAvailableSubAgents()).toEqual([ + { + id: 'agent-billing', + name: 'Billing Agent', + useWhen: 'Use for invoice investigations and payment status checks.', + }, + ]); + }); + it('resolves subAgents.modelsByDifficulty into delegate tool metadata', async () => { const credentialProvider = mock(); credentialProvider.resolve.mockImplementation(async (credentialId: string) => { diff --git a/packages/cli/src/modules/agents/agent-config.service.ts b/packages/cli/src/modules/agents/agent-config.service.ts index e5e665bbd23..11a6b4ffdfa 100644 --- a/packages/cli/src/modules/agents/agent-config.service.ts +++ b/packages/cli/src/modules/agents/agent-config.service.ts @@ -23,7 +23,7 @@ import { AgentTaskRepository } from './repositories/agent-task.repository'; import { AgentRepository } from './repositories/agent.repository'; import { createAgentCredentialProvider } from './utils/agent-credential-provider'; import { markAgentDraftDirty } from './utils/agent-draft.utils'; -import { resolveUniqueSubAgents } from './utils/sub-agent-resolver'; +import { resolveUniqueSubAgents, type ResolvedSubAgentRef } from './utils/sub-agent-resolver'; @Service() export class AgentConfigService { @@ -256,7 +256,7 @@ export class AgentConfigService { config: AgentJsonConfig, entity: Agent, existingTaskIds: ReadonlySet, - ): Promise> { + ): Promise { if (config.skills !== undefined) { const skills = entity.skills ?? {}; config.skills = config.skills.filter((ref) => Boolean(skills[ref.id])); @@ -277,22 +277,19 @@ export class AgentConfigService { projectId: entity.projectId, agentRepository: this.agentRepository, }); - const existingSubAgentIds = new Set( - resolvedSubAgents.filter(({ agent }) => agent !== null).map(({ agentId }) => agentId), - ); config.subAgents.agents = resolvedSubAgents - .filter(({ agentId }) => existingSubAgentIds.has(agentId)) - .map(({ agentId }) => ({ agentId })); + .filter(({ agent }) => agent !== null) + .map(({ agentId, useWhen }) => ({ + agentId, + ...(useWhen ? { useWhen } : {}), + })); return resolvedSubAgents; } return []; } - private validateSubAgentRefs( - resolvedSubAgents: Array<{ agentId: string; agent: Agent | null }>, - entity: Agent, - ) { + private validateSubAgentRefs(resolvedSubAgents: ResolvedSubAgentRef[], entity: Agent) { for (const { agentId, agent } of resolvedSubAgents) { if (!agent) continue; if (agentId === entity.id) { diff --git a/packages/cli/src/modules/agents/agent-runtime-reconstruction.service.ts b/packages/cli/src/modules/agents/agent-runtime-reconstruction.service.ts index caef5554f9d..d1f5e5b4914 100644 --- a/packages/cli/src/modules/agents/agent-runtime-reconstruction.service.ts +++ b/packages/cli/src/modules/agents/agent-runtime-reconstruction.service.ts @@ -69,7 +69,7 @@ export type AgentRuntimeProfile = 'top-level' | 'sub-agent'; export interface SubAgentDelegationConfig { sourcesById: Record; - availableSubAgents: Array<{ id: string; name: string }>; + availableSubAgents: Array<{ id: string; name: string; useWhen?: string }>; } export interface ReconstructAgentRuntimeParams { @@ -286,7 +286,7 @@ export class AgentRuntimeReconstructionService { const sourcesById: Record = {}; const availableSubAgents: SubAgentDelegationConfig['availableSubAgents'] = []; - for (const { agentId, agent } of await resolveUniqueSubAgents({ + for (const { agentId, agent, useWhen } of await resolveUniqueSubAgents({ refs: configuredAgents, projectId, agentRepository: this.agentRepository, @@ -297,6 +297,7 @@ export class AgentRuntimeReconstructionService { availableSubAgents.push({ id: agentId, name: agent.name, + ...(useWhen ? { useWhen } : {}), }); } diff --git a/packages/cli/src/modules/agents/builder/__tests__/agents-builder-prompts.test.ts b/packages/cli/src/modules/agents/builder/__tests__/agents-builder-prompts.test.ts index ec629781927..e0d8ee7a368 100644 --- a/packages/cli/src/modules/agents/builder/__tests__/agents-builder-prompts.test.ts +++ b/packages/cli/src/modules/agents/builder/__tests__/agents-builder-prompts.test.ts @@ -43,6 +43,13 @@ describe('sub-agent skill availability', () => { expect(skill?.instructions).toContain('`allowMultiple: true`'); expect(skill?.instructions).toContain('subAgentId: "inline"'); expect(skill?.instructions).toContain('`subAgents.maxChildren`'); + expect(skill?.instructions).toContain( + '{ "agentId": "", "useWhen": "Use for ..." }', + ); + expect(skill?.instructions).toContain( + 'If it is unclear when a selected saved subagent should be used, ask the user', + ); + expect(skill?.instructions).toContain('Do not write vague values'); expect(skill?.instructions).toContain( `from ${SUB_AGENT_MAX_CHILDREN_MIN} to ${SUB_AGENT_MAX_CHILDREN_MAX}`, ); diff --git a/packages/cli/src/modules/agents/builder/agents-builder-tools.service.ts b/packages/cli/src/modules/agents/builder/agents-builder-tools.service.ts index a0d66593d10..1ea45daca30 100644 --- a/packages/cli/src/modules/agents/builder/agents-builder-tools.service.ts +++ b/packages/cli/src/modules/agents/builder/agents-builder-tools.service.ts @@ -592,7 +592,8 @@ export class AgentsBuilderToolsService { .description( 'List published agents in the same project that can be added to the target agent as subagents. ' + 'Excludes the target agent itself and unpublished agents. Use before asking the user which ' + - 'subagents to add. Returned `agentId` values are the only valid values to write into `subAgents.agents[].agentId`.', + 'subagents to add. Returned `agentId` values are the only valid values to write into `subAgents.agents[].agentId`; ' + + 'write parent-owned routing guidance into `subAgents.agents[].useWhen`; ask a follow-up first when it is unclear when that parent should use the subagent.', ) .input(z.object({})) .handler(async () => { diff --git a/packages/cli/src/modules/agents/builder/skills/sub-agents.skill.ts b/packages/cli/src/modules/agents/builder/skills/sub-agents.skill.ts index eaea8327fc0..99f51fe2102 100644 --- a/packages/cli/src/modules/agents/builder/skills/sub-agents.skill.ts +++ b/packages/cli/src/modules/agents/builder/skills/sub-agents.skill.ts @@ -48,30 +48,47 @@ subagent. added. Do not write agent ids from memory, prose, or user-entered free text. 2. If published agents are available and the user has not named exact agents, call \`ask_question\` with \`allowMultiple: true\`. Use each option's - \`value\` as the returned \`agentId\`, and include descriptions when present. + \`value\` as the returned \`agentId\`. 3. If no published agents are available, do not configure saved subagents. Inline delegation still works without saved-agent refs. -4. Call \`read_config\`. -5. Patch selected saved agents into \`subAgents.agents\` as - \`{ "agentId": "" }\`. Avoid duplicates. +4. Determine the parent-owned routing guidance for each selected saved + subagent. Store it as \`useWhen\`, for example + \`{ "agentId": "", "useWhen": "Use for billing-policy questions and invoice investigations." }\`. +5. If it is unclear when a selected saved subagent should be used, ask the user + a follow-up before patching \`subAgents.agents\`. Do not invent vague routing + guidance. +6. Call \`read_config\`. +7. Patch selected saved agents into \`subAgents.agents\`. Avoid duplicates. Example patch flow: 1. \`list_sub_agents()\`. 2. If it returns one or more agents and the user has not named exact ones, call \`ask_question({ allowMultiple: true, ... })\` with those agents as options. -3. \`read_config()\`. -4. \`patch_config(...)\` adding selected \`{ "agentId": "" }\` - refs to \`/subAgents/agents\`. +3. If the user's request does not make the routing rule clear, ask when each + selected saved subagent should be used. +4. \`read_config()\`. +5. \`patch_config(...)\` adding selected + \`{ "agentId": "", "useWhen": "Use for ..." }\` refs to + \`/subAgents/agents\`. ## Rules - If the resumed values include text that is not one of the listed agent ids, do not persist it as an agent id; ask a follow-up. +- \`useWhen\` is relationship-specific routing guidance owned by this parent + config. It is not the child agent's global description. +- Write \`useWhen\` only when the intended routing is clear from the user's + request or an explicit follow-up answer. +- Every new or updated saved subagent ref must include \`useWhen\`. +- Good \`useWhen\` values are concrete and intent-oriented, such as + "Use for invoice investigations and payment status checks." +- Do not write vague values such as "Use when helpful", "Use for tasks", or + "Use for this agent." - Do not add custom tools, custom instructions, or custom schema fields to simulate subagents. - Preserve existing \`subAgents.agents\` refs unless the user explicitly asks to - change saved subagents. + change saved subagents. Preserve existing \`useWhen\` values when keeping refs. ## Inline model mappings @@ -99,6 +116,7 @@ Example shape: - Inline delegation still works even when \`subAgents.agents\` is absent. - Saved refs use only returned same-project published agent ids. +- New or updated saved refs include concrete \`useWhen\` guidance. - Any \`subAgents.maxChildren\` value stays within ${SUB_AGENT_MAX_CHILDREN_MIN} to ${SUB_AGENT_MAX_CHILDREN_MAX}.`, }; } diff --git a/packages/cli/src/modules/agents/sub-agents/__tests__/delegate-sub-agent-tool.test.ts b/packages/cli/src/modules/agents/sub-agents/__tests__/delegate-sub-agent-tool.test.ts index 9cc9eb63cc8..95023809e77 100644 --- a/packages/cli/src/modules/agents/sub-agents/__tests__/delegate-sub-agent-tool.test.ts +++ b/packages/cli/src/modules/agents/sub-agents/__tests__/delegate-sub-agent-tool.test.ts @@ -186,7 +186,13 @@ describe('createN8nDelegateSubAgentTool', () => { sourcesById: { 'agent-2': selectedSource, }, - availableSubAgents: [{ id: 'agent-2', name: 'Research Agent' }], + availableSubAgents: [ + { + id: 'agent-2', + name: 'Research Agent', + useWhen: 'Use for research tasks.', + }, + ], projectId, userId, credentialProvider, diff --git a/packages/cli/src/modules/agents/sub-agents/delegate-sub-agent-tool.ts b/packages/cli/src/modules/agents/sub-agents/delegate-sub-agent-tool.ts index aca7ffddb4d..90218e28739 100644 --- a/packages/cli/src/modules/agents/sub-agents/delegate-sub-agent-tool.ts +++ b/packages/cli/src/modules/agents/sub-agents/delegate-sub-agent-tool.ts @@ -18,7 +18,7 @@ import type { export interface CreateN8nDelegateSubAgentToolOptions extends SubAgentForegroundRunContext { runner: SubAgentForegroundRunner; sourcesById: Record; - availableSubAgents?: Array<{ id: string; name: string }>; + availableSubAgents?: Array<{ id: string; name: string; useWhen?: string }>; policy?: SubAgentRunPolicy; inlineSubAgentModelsByDifficulty?: Partial>; resolveInlineSubAgentProviderTools?: InlineSubAgentProviderToolsResolver; diff --git a/packages/cli/src/modules/agents/utils/sub-agent-resolver.ts b/packages/cli/src/modules/agents/utils/sub-agent-resolver.ts index 5c7b04072a9..f57a119c292 100644 --- a/packages/cli/src/modules/agents/utils/sub-agent-resolver.ts +++ b/packages/cli/src/modules/agents/utils/sub-agent-resolver.ts @@ -1,24 +1,33 @@ import type { Agent } from '../entities/agent.entity'; import type { AgentRepository } from '../repositories/agent.repository'; +export type SubAgentConfigRef = { agentId: string; useWhen?: string }; + +export type ResolvedSubAgentRef = SubAgentConfigRef & { agent: Agent | null }; + export async function resolveUniqueSubAgents({ refs, projectId, agentRepository, }: { - refs: Array<{ agentId: string }>; + refs: SubAgentConfigRef[]; projectId: string; agentRepository: Pick; -}): Promise> { +}): Promise { const seen = new Set(); - const resolved: Array<{ agentId: string; agent: Agent | null }> = []; - for (const { agentId } of refs) { + const resolved: ResolvedSubAgentRef[] = []; + for (const ref of refs) { + const { agentId } = ref; if (seen.has(agentId)) continue; seen.add(agentId); - resolved.push({ + const resolvedRef: ResolvedSubAgentRef = { agentId, agent: await agentRepository.findByIdAndProjectId(agentId, projectId), - }); + }; + if (ref.useWhen !== undefined) { + resolvedRef.useWhen = ref.useWhen; + } + resolved.push(resolvedRef); } return resolved; } diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index 2e61f4cab99..dfee4089edf 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -6711,11 +6711,18 @@ "agents.builder.files.size.bytes": "{bytes} B", "agents.builder.files.size.kilobytes": "{kilobytes} KB", "agents.builder.files.size.megabytes": "{megabytes} MB", - "agents.builder.subAgents.title": "Sub-agents", + "agents.builder.subAgents.title": "Agents", "agents.builder.subAgents.description": "This agent can delegate focused subtasks. Add published agents from this project when you want reusable specialists.", + "agents.builder.subAgents.settings.title": "Sub-agent settings", + "agents.builder.subAgents.settings.description": "Configure delegation limits and inline sub-agent model choices.", "agents.builder.subAgents.add": "Add agent", "agents.builder.subAgents.loadError": "Could not load project agents", "agents.builder.subAgents.remove": "Remove {name}", + "agents.builder.subAgents.useWhen.label": "When should this agent be used?", + "agents.builder.subAgents.useWhen.hint": "Tell the parent agent when to delegate work to this agent.", + "agents.builder.subAgents.useWhen.placeholder": "Use for billing questions, invoice investigations, and payment status checks.", + "agents.builder.subAgents.useWhen.characterCount": "{count}/{max}", + "agents.builder.subAgents.useWhen.validation.maxLength": "Use when must be {max} characters or less.", "agents.builder.subAgents.maxChildren.label": "Max parallel sub-agents", "agents.builder.subAgents.maxChildren.hint": "How many sub-agents this agent can run at the same time ({min}-{max}).", "agents.builder.subAgents.modelsByDifficulty.title": "Models by difficulty", @@ -6727,13 +6734,17 @@ "agents.builder.subAgents.modelsByDifficulty.high.label": "High", "agents.builder.subAgents.modelsByDifficulty.high.description": "Complex tasks that need deeper reasoning or detailed synthesis.", "agents.builder.subAgents.modelsByDifficulty.clear": "Use parent model", - "agents.builder.subAgents.modal.title": "Add sub-agents", + "agents.builder.subAgents.modal.title": "Sub-agents", "agents.builder.subAgents.modal.description": "Select published agents from this project to let this agent delegate work to them.", "agents.builder.subAgents.modal.selectAgent": "Select {name}", "agents.builder.subAgents.modal.empty.title": "No agents to add", "agents.builder.subAgents.modal.empty.description": "Published agents from this project show here. Already added agents are hidden.", + "agents.builder.subAgents.modal.search.placeholder": "Search agents", + "agents.builder.subAgents.modal.noResults.title": "No matching agents", + "agents.builder.subAgents.modal.noResults.description": "Try another search term.", "agents.builder.subAgents.modal.cancel": "Cancel", - "agents.builder.subAgents.modal.add": "Add agents", + "agents.builder.subAgents.modal.add": "Add agent", + "agents.builder.subAgents.modal.remove": "Remove sub agent", "agents.builder.memory.recallModel.label": "Memory model", "agents.builder.memory.recallModel.hint": "Choose the model that creates, reviews, and retrieves memories. Uses the agent model by default.", "agents.builder.episodicMemoryCredentialModal.title": "Episodic Memory", diff --git a/packages/frontend/editor-ui/src/features/agents/__tests__/AgentCapabilitiesSection.test.ts b/packages/frontend/editor-ui/src/features/agents/__tests__/AgentCapabilitiesSection.test.ts index f1fafc36c17..0e7b6864a82 100644 --- a/packages/frontend/editor-ui/src/features/agents/__tests__/AgentCapabilitiesSection.test.ts +++ b/packages/frontend/editor-ui/src/features/agents/__tests__/AgentCapabilitiesSection.test.ts @@ -1,11 +1,12 @@ import { flushPromises, mount } from '@vue/test-utils'; import type { AgentJsonTaskConfig, AgentTaskDto } from '@n8n/api-types'; +import { ref } from 'vue'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { SimplifiedNodeType } from '@/Interface'; import AgentCapabilitiesSection from '../components/AgentCapabilitiesSection.vue'; -import type { AgentJsonConfig, AgentJsonToolRef, CustomToolEntry } from '../types'; -import { AGENT_TASK_MODAL_KEY } from '../constants'; +import type { AgentJsonConfig, AgentJsonToolRef, AgentResource, CustomToolEntry } from '../types'; +import { AGENT_SUB_AGENTS_MODAL_KEY, AGENT_TASK_MODAL_KEY } from '../constants'; const getNodeType = vi.fn<(type: string, version?: number) => SimplifiedNodeType | null>( () => null, @@ -42,6 +43,20 @@ vi.mock('@/app/stores/ui.store', () => ({ useUIStore: () => ({ openModalWithData: openModalWithDataSpy }), })); +const showErrorSpy = vi.fn(); +vi.mock('@/app/composables/useToast', () => ({ + useToast: () => ({ showError: showErrorSpy }), +})); + +const projectAgentsListRef = ref([]); +const ensureProjectAgentsLoadedSpy = vi.fn(); +vi.mock('../composables/useProjectAgentsList', () => ({ + useProjectAgentsList: () => ({ + list: projectAgentsListRef, + ensureLoaded: ensureProjectAgentsLoadedSpy, + }), +})); + const getAgentTasksSpy = vi.fn(); vi.mock('../composables/useAgentApi', () => ({ getAgentTasks: (...args: unknown[]) => getAgentTasksSpy(...args), @@ -64,7 +79,10 @@ function mountSection( customTools: Record = {}, config: AgentJsonConfig | null = null, taskRefs: AgentJsonTaskConfig[] = [], + projectAgents: AgentResource[] = [], ) { + projectAgentsListRef.value = projectAgents; + return mount(AgentCapabilitiesSection, { props: { config, @@ -112,6 +130,24 @@ function makeTask(overrides: Partial = {}): AgentTaskDto { }; } +function makeAgent(overrides: Partial = {}): AgentResource { + return { + id: 'agent-2', + name: 'Helper Agent', + projectId: 'project-id', + resourceType: 'agent', + isCompiled: true, + createdAt: '2026-01-01T00:00:00.000Z', + updatedAt: '2026-01-01T00:00:00.000Z', + versionId: 'version-2', + activeVersionId: 'version-2', + tools: {}, + skills: {}, + activeVersion: null, + ...overrides, + }; +} + function taskRef(id = 'task-1', enabled = true): AgentJsonTaskConfig { return { type: 'task', id, enabled }; } @@ -132,6 +168,8 @@ describe('AgentCapabilitiesSection', () => { beforeEach(() => { vi.clearAllMocks(); getAgentTasksSpy.mockResolvedValue([]); + projectAgentsListRef.value = []; + ensureProjectAgentsLoadedSpy.mockResolvedValue([]); }); it('formats node and custom tool chip labels for display', () => { @@ -307,6 +345,202 @@ describe('AgentCapabilitiesSection', () => { expect(wrapper.findAll('[data-testid="agent-capabilities-tool-row"]').length).toBe(1); }); + it('renders selected sub-agents as chips and opens the add modal from capabilities', async () => { + const config: AgentJsonConfig = { + name: 'Test Agent', + model: '', + instructions: '', + tools: [], + subAgents: { + maxChildren: 7, + agents: [{ agentId: 'agent-2', useWhen: 'Use for billing support requests.' }], + }, + }; + const wrapper = mountSection( + [], + {}, + config, + [], + [ + makeAgent(), + makeAgent({ id: 'agent-3', name: 'Research Agent', versionId: 'version-3' }), + makeAgent({ + id: 'agent-4', + name: 'Draft Agent', + versionId: 'version-4', + activeVersionId: null, + }), + makeAgent({ id: 'agent-id', name: 'Current Agent', versionId: 'version-current' }), + ], + ); + await flushPromises(); + + expect(wrapper.text()).toContain('Helper Agent'); + expect(wrapper.findAll('[data-testid="agent-capabilities-sub-agent-row"]').length).toBe(1); + + await wrapper.find('[data-testid="agent-capabilities-add-sub-agent"]').trigger('click'); + await flushPromises(); + + expect(openModalWithDataSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: AGENT_SUB_AGENTS_MODAL_KEY, + data: expect.objectContaining({ + agents: [{ id: 'agent-3', name: 'Research Agent' }], + }), + }), + ); + + const modalCall = openModalWithDataSpy.mock.calls[0]?.[0] as { + data: { onConfirm: (payload: { agentId: string; useWhen?: string }) => void }; + }; + modalCall.data.onConfirm({ + agentId: 'agent-3', + useWhen: 'Use for research requests.', + }); + + expect(wrapper.emitted('update:config')?.[0]).toEqual([ + { + subAgents: { + maxChildren: 7, + agents: [ + { agentId: 'agent-2', useWhen: 'Use for billing support requests.' }, + { agentId: 'agent-3', useWhen: 'Use for research requests.' }, + ], + }, + }, + ]); + }); + + it('opens an existing sub-agent chip for editing and removal', async () => { + const config: AgentJsonConfig = { + name: 'Test Agent', + model: '', + instructions: '', + tools: [], + subAgents: { + maxChildren: 7, + agents: [ + { agentId: 'agent-2', useWhen: 'Use for billing support requests.' }, + { agentId: 'agent-3', useWhen: 'Use for research tasks.' }, + ], + }, + }; + const wrapper = mountSection( + [], + {}, + config, + [], + [makeAgent(), makeAgent({ id: 'agent-3', name: 'Research Agent', versionId: 'version-3' })], + ); + await flushPromises(); + + await wrapper.findAll('[data-testid="agent-capabilities-sub-agent-row"]')[0].trigger('click'); + + expect(openModalWithDataSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: AGENT_SUB_AGENTS_MODAL_KEY, + data: expect.objectContaining({ + selectedAgent: { id: 'agent-2', name: 'Helper Agent' }, + useWhen: 'Use for billing support requests.', + }), + }), + ); + + const modalCall = openModalWithDataSpy.mock.calls[0]?.[0] as { + data: { + onConfirm: (payload: { agentId: string; useWhen?: string }) => void; + onRemove: (agentId: string) => void; + }; + }; + modalCall.data.onConfirm({ + agentId: 'agent-2', + }); + + expect(wrapper.emitted('update:config')?.[0]).toEqual([ + { + subAgents: { + maxChildren: 7, + agents: [ + { agentId: 'agent-2' }, + { agentId: 'agent-3', useWhen: 'Use for research tasks.' }, + ], + }, + }, + ]); + + modalCall.data.onRemove('agent-2'); + + expect(wrapper.emitted('update:config')?.[1]).toEqual([ + { + subAgents: { + maxChildren: 7, + agents: [{ agentId: 'agent-3', useWhen: 'Use for research tasks.' }], + }, + }, + ]); + }); + + it('keeps legacy sub-agent refs without useWhen editable and removable', async () => { + const config: AgentJsonConfig = { + name: 'Test Agent', + model: '', + instructions: '', + tools: [], + subAgents: { + maxChildren: 7, + agents: [{ agentId: 'agent-2' }], + }, + }; + const wrapper = mountSection([], {}, config, [], [makeAgent()]); + await flushPromises(); + + expect(wrapper.text()).toContain('Helper Agent'); + expect(wrapper.findAll('[data-testid="agent-capabilities-sub-agent-row"]').length).toBe(1); + + await wrapper.find('[data-testid="agent-capabilities-sub-agent-row"]').trigger('click'); + + expect(openModalWithDataSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: AGENT_SUB_AGENTS_MODAL_KEY, + data: expect.objectContaining({ + selectedAgent: { id: 'agent-2', name: 'Helper Agent' }, + useWhen: '', + }), + }), + ); + + const modalCall = openModalWithDataSpy.mock.calls[0]?.[0] as { + data: { + onConfirm: (payload: { agentId: string; useWhen?: string }) => void; + onRemove: (agentId: string) => void; + }; + }; + modalCall.data.onConfirm({ + agentId: 'agent-2', + useWhen: 'Use for billing support requests.', + }); + + expect(wrapper.emitted('update:config')?.[0]).toEqual([ + { + subAgents: { + maxChildren: 7, + agents: [{ agentId: 'agent-2', useWhen: 'Use for billing support requests.' }], + }, + }, + ]); + + modalCall.data.onRemove('agent-2'); + + expect(wrapper.emitted('update:config')?.[1]).toEqual([ + { + subAgents: { + maxChildren: 7, + agents: [], + }, + }, + ]); + }); + it('renders task chips from task refs and fetched bodies', async () => { getAgentTasksSpy.mockResolvedValue([makeTask()]); diff --git a/packages/frontend/editor-ui/src/features/agents/__tests__/AgentSubAgentsModal.spec.ts b/packages/frontend/editor-ui/src/features/agents/__tests__/AgentSubAgentsModal.spec.ts new file mode 100644 index 00000000000..ee1d3f398f8 --- /dev/null +++ b/packages/frontend/editor-ui/src/features/agents/__tests__/AgentSubAgentsModal.spec.ts @@ -0,0 +1,256 @@ +/* eslint-disable import-x/no-extraneous-dependencies -- test-only Vue mounting */ +import { mount } from '@vue/test-utils'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import AgentSubAgentsModal from '../components/AgentSubAgentsModal.vue'; + +const closeModalMock = vi.fn(); + +vi.mock('@n8n/i18n', () => ({ + useI18n: () => ({ + baseText: (key: string, options?: { interpolate?: Record }) => + ({ + 'agents.builder.subAgents.modal.title': 'Sub-agents', + 'agents.builder.subAgents.modal.description': 'Select published agents', + 'agents.builder.subAgents.modal.selectAgent': `Select ${options?.interpolate?.name ?? ''}`, + 'agents.builder.subAgents.modal.empty.title': 'No agents to add', + 'agents.builder.subAgents.modal.empty.description': 'Published agents show here', + 'agents.builder.subAgents.modal.search.placeholder': 'Search agents', + 'agents.builder.subAgents.modal.noResults.title': 'No matching agents', + 'agents.builder.subAgents.modal.noResults.description': 'Try another search term.', + 'agents.builder.subAgents.modal.add': 'Add agent', + 'agents.builder.subAgents.modal.remove': 'Remove sub agent', + 'agents.builder.subAgents.useWhen.label': 'When should this agent be used?', + 'agents.builder.subAgents.useWhen.hint': 'Tell the parent agent when to delegate work.', + 'agents.builder.subAgents.useWhen.placeholder': 'Use for billing questions', + 'agents.builder.subAgents.useWhen.characterCount': '0/512', + 'agents.builder.subAgents.useWhen.validation.maxLength': `Use when must be ${options?.interpolate?.max ?? ''} characters or less`, + 'generic.back': 'Back', + 'generic.cancel': 'Cancel', + 'generic.save': 'Save', + })[key] ?? key, + }), +})); + +vi.mock('@/app/stores/ui.store', () => ({ + useUIStore: () => ({ closeModal: closeModalMock }), +})); + +vi.mock('@/app/components/Modal.vue', () => ({ + default: { + name: 'Modal', + props: ['name', 'width', 'customClass'], + template: + '
', + }, +})); + +vi.mock('@n8n/design-system', () => ({ + N8nActionBox: { + props: ['heading', 'description'], + template: '
{{ heading }} {{ description }}
', + }, + N8nButton: { + props: ['variant', 'size', 'disabled'], + emits: ['click'], + template: + '', + }, + N8nHeading: { template: '

', props: ['tag', 'size'] }, + N8nIcon: { template: '', props: ['icon', 'size'] }, + N8nInput: { + props: ['modelValue', 'placeholder', 'clearable', 'size'], + emits: ['update:modelValue'], + template: + '', + }, + N8nMarkdownEditor: { + props: ['modelValue'], + emits: ['update:modelValue'], + template: + '