From 04cc3fc26001ee566ed94de44c2dda2cf6adecc4 Mon Sep 17 00:00:00 2001 From: Hunter Lovell <40191806+hntrl@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:54:59 -0700 Subject: [PATCH] fix(deepagents): propagate subagent lc_agent_name for delegated tasks (#566) ## Summary Fixes #206. This updates deepagents subagent delegation so tool executions can reliably identify the active subagent via `config.metadata.lc_agent_name` instead of inheriting the parent agent name. It also adds focused regression tests that validate both compiled subagents and standard subagent specs follow the same metadata behavior. ## Changes ### `libs/deepagents` subagent metadata propagation - Updated `createTaskTool` subagent invocation config to explicitly set: - `metadata.lc_agent_name = subagent_type` - existing `configurable.ls_agent_type = "subagent"` behavior remains unchanged - Added new regression coverage in `subagent.test.ts`: - compiled subagent (`runnable`) path - standard subagent spec (`systemPrompt/tools/model`) path - Tests assert tool-time metadata receives the delegated subagent name (`worker`), preventing parent-name leakage. ### `libs/deepagents` dependency range update - Bumped `langsmith` peer dependency range in `libs/deepagents/package.json` from `>=0.6.0 <1.0.0` to `^0.7.1`. - Updated `pnpm-lock.yaml` accordingly. --- .changeset/subagent-agent-name-metadata.md | 9 + .../src/middleware/subagent.test.ts | 169 ++++++++++++++++++ libs/deepagents/src/middleware/subagents.ts | 4 + 3 files changed, 182 insertions(+) create mode 100644 .changeset/subagent-agent-name-metadata.md diff --git a/.changeset/subagent-agent-name-metadata.md b/.changeset/subagent-agent-name-metadata.md new file mode 100644 index 00000000..e27b05b4 --- /dev/null +++ b/.changeset/subagent-agent-name-metadata.md @@ -0,0 +1,9 @@ +--- +"deepagents": patch +--- + +fix(deepagents): propagate subagent `lc_agent_name` during task delegation + +- Ensure `task` tool subagent invocations override `metadata.lc_agent_name` with the selected `subagent_type`. +- Add regression coverage for both compiled subagents (`runnable`) and standard subagent specs to verify tool-time metadata reflects the active subagent. +- Update the `langsmith` peer dependency range in `deepagents` to `^0.7.1`. diff --git a/libs/deepagents/src/middleware/subagent.test.ts b/libs/deepagents/src/middleware/subagent.test.ts index 95f76502..8b15fa69 100644 --- a/libs/deepagents/src/middleware/subagent.test.ts +++ b/libs/deepagents/src/middleware/subagent.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, vi } from "vitest"; import { MemorySaver } from "@langchain/langgraph-checkpoint"; import { FakeListChatModel } from "@langchain/core/utils/testing"; +import { createAgent, tool } from "langchain"; import { AIMessage, BaseMessage, @@ -9,6 +10,7 @@ import { ToolMessage, } from "@langchain/core/messages"; import { RunnableLambda } from "@langchain/core/runnables"; +import { z } from "zod/v4"; import { CallbackManager } from "@langchain/core/callbacks/manager"; import { BaseCallbackHandler } from "@langchain/core/callbacks/base"; import { LangChainTracer } from "@langchain/core/tracers/tracer_langchain"; @@ -834,3 +836,170 @@ describe("ls_agent_type tracing metadata", () => { expect(subagentRuns.length).toBeGreaterThan(0); }); }); + +describe("lc_agent_name propagation for subagents", () => { + it("should pass subagent name for compiled subagents", async () => { + let capturedSubagentAgentName: string | undefined; + + const identifyCaller = tool( + (_input, config) => { + capturedSubagentAgentName = config.metadata?.lc_agent_name as + | string + | undefined; + return "captured"; + }, + { + name: "identify_caller", + description: "Capture lc_agent_name from metadata", + schema: z.object({}), + }, + ); + + const compiledSubagentModel = new FakeListChatModel({ + responses: [ + new AIMessage({ + content: "", + tool_calls: [ + { + id: "compiled_tool_call", + name: "identify_caller", + args: {}, + }, + ], + }) as unknown as string, + "Subagent done", + ], + }); + + const compiledSubagent = createAgent({ + model: compiledSubagentModel, + systemPrompt: + "Use identify_caller to capture who invoked this subagent, then finish.", + tools: [identifyCaller], + name: "compiled-worker-inner", + }); + + const parentModel = new FakeListChatModel({ + responses: [ + new AIMessage({ + content: "", + tool_calls: [ + { + id: "task_call_compiled", + name: "task", + args: { + description: "Do work", + subagent_type: "worker", + }, + }, + ], + }) as unknown as string, + "Done", + ], + }); + + const agent = createDeepAgent({ + model: parentModel, + name: "main-agent", + subagents: [ + { + name: "worker", + description: "A worker agent", + runnable: compiledSubagent, + }, + ], + }); + + await agent.invoke( + { messages: [new HumanMessage("Test")] }, + { + configurable: { + thread_id: `test-lc-agent-name-compiled-${Date.now()}`, + }, + recursionLimit: 50, + }, + ); + + expect(capturedSubagentAgentName).toBe("worker"); + }); + + it("should pass subagent name for standard subagent specs", async () => { + let capturedSubagentAgentName: string | undefined; + + const identifyCaller = tool( + (_input, config) => { + capturedSubagentAgentName = config.metadata?.lc_agent_name as + | string + | undefined; + return "captured"; + }, + { + name: "identify_caller", + description: "Capture lc_agent_name from metadata", + schema: z.object({}), + }, + ); + + const standardSubagentModel = new FakeListChatModel({ + responses: [ + new AIMessage({ + content: "", + tool_calls: [ + { + id: "standard_tool_call", + name: "identify_caller", + args: {}, + }, + ], + }) as unknown as string, + "Subagent done", + ], + }); + + const parentModel = new FakeListChatModel({ + responses: [ + new AIMessage({ + content: "", + tool_calls: [ + { + id: "task_call_standard", + name: "task", + args: { + description: "Do work", + subagent_type: "worker", + }, + }, + ], + }) as unknown as string, + "Done", + ], + }); + + const agent = createDeepAgent({ + model: parentModel, + name: "main-agent", + subagents: [ + { + name: "worker", + description: "A worker agent", + systemPrompt: + "Use identify_caller to capture who invoked this subagent, then finish.", + tools: [identifyCaller], + model: standardSubagentModel, + }, + ], + }); + + await agent.invoke( + { messages: [new HumanMessage("Test")] }, + { + configurable: { + thread_id: `test-lc-agent-name-standard-${Date.now()}`, + }, + recursionLimit: 50, + }, + ); + + expect(capturedSubagentAgentName).toBe("worker"); + }); +}); diff --git a/libs/deepagents/src/middleware/subagents.ts b/libs/deepagents/src/middleware/subagents.ts index 21654bac..84fae4a1 100644 --- a/libs/deepagents/src/middleware/subagents.ts +++ b/libs/deepagents/src/middleware/subagents.ts @@ -627,6 +627,10 @@ function createTaskTool(options: { // Invoke the subagent with ls_agent_type metadata for LangSmith tracing const subagentConfig = { ...config, + metadata: { + ...config.metadata, + lc_agent_name: subagent_type, + }, configurable: { ...config.configurable, ls_agent_type: "subagent",