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.
This commit is contained in:
Hunter Lovell
2026-06-01 12:54:59 -07:00
committed by GitHub
parent 70521d2e30
commit 04cc3fc260
3 changed files with 182 additions and 0 deletions
@@ -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`.
@@ -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");
});
});
@@ -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",