mirror of
https://github.com/langchain-ai/deepagentsjs.git
synced 2026-07-21 03:45:22 -04:00
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:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user