From 94b0cf6073fb48a28287562b3676f4518a52c096 Mon Sep 17 00:00:00 2001 From: Liran Sharir Date: Sun, 17 Aug 2025 12:36:41 +0300 Subject: [PATCH 1/2] supervisor: merge child bound config before invoke + test --- libs/langgraph-supervisor/src/supervisor.ts | 13 +- .../tests/supervisor_tags.propagation.test.ts | 114 ++++++++++++++++++ 2 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 libs/langgraph-supervisor/src/tests/supervisor_tags.propagation.test.ts diff --git a/libs/langgraph-supervisor/src/supervisor.ts b/libs/langgraph-supervisor/src/supervisor.ts index 6a6a4d462..df7a13d81 100644 --- a/libs/langgraph-supervisor/src/supervisor.ts +++ b/libs/langgraph-supervisor/src/supervisor.ts @@ -1,6 +1,10 @@ import { LanguageModelLike } from "@langchain/core/language_models/base"; import { StructuredToolInterface, DynamicTool } from "@langchain/core/tools"; -import { RunnableToolLike } from "@langchain/core/runnables"; +import { + RunnableConfig, + RunnableToolLike, + mergeConfigs, +} from "@langchain/core/runnables"; import { InteropZodType } from "@langchain/core/utils/types"; import { START, @@ -71,8 +75,11 @@ const makeCallAgent = ( ); } - return async (state: Record) => { - const output = await agent.invoke(state); + return async (state: Record, config?: RunnableConfig) => { + // Merge the child agent's bound config with the supervisor-provided config + const mergedConfig = mergeConfigs(agent?.config, config); + + const output = await agent.invoke(state, mergedConfig); let { messages } = output; if (outputMode === "last_message") { diff --git a/libs/langgraph-supervisor/src/tests/supervisor_tags.propagation.test.ts b/libs/langgraph-supervisor/src/tests/supervisor_tags.propagation.test.ts new file mode 100644 index 000000000..25a525ee0 --- /dev/null +++ b/libs/langgraph-supervisor/src/tests/supervisor_tags.propagation.test.ts @@ -0,0 +1,114 @@ +import { describe, it, expect } from "vitest"; +import { AIMessage } from "@langchain/core/messages"; +import { BaseCallbackHandler } from "@langchain/core/callbacks/base"; +import { createReactAgent } from "@langchain/langgraph/prebuilt"; +import { Serialized } from "@langchain/core/load/serializable"; +import { ChainValues } from "@langchain/core/utils/types"; +import { createSupervisor } from "../supervisor.js"; +import { FakeToolCallingChatModel } from "./utils.js"; + +describe("supervisor preserves child agent bound tags", () => { + it("agent.withConfig({ tags }) appears on agent span when invoked by supervisor", async () => { + const TAG = "child_agent_tag"; + + const seen: { + runType: string; + runName?: string; + tags?: string[]; + metadata?: Record; + }[] = []; + + // Spy handler to capture tags at run starts + class SpyHandler extends BaseCallbackHandler { + name = "SpyHandler"; + + // Manager invokes "handle*" methods on handlers. Capture tags from the appropriate position. + async handleChainStart( + _chain: Serialized, + _inputs: ChainValues, + _runId?: string, + _runType?: string, + tags?: string[], + metadata?: Record, + runName?: string + ) { + seen.push({ runType: "chain", runName, tags, metadata }); + } + + async handleToolStart( + _tool: Serialized, + _input: string, + _runId?: string, + _parentRunId?: string, + tags?: string[], + metadata?: Record, + runName?: string + ) { + seen.push({ runType: "tool", runName, tags, metadata }); + } + + async handleLLMStart( + _llm: Serialized, + _prompts: string[], + _runId?: string, + _parentRunId?: string, + _extraParams?: Record, + tags?: string[], + metadata?: Record, + runName?: string + ) { + seen.push({ runType: "llm", runName, tags, metadata }); + } + } + const spy = new SpyHandler(); + + // Supervisor model: tool-calls into our agent, then final answer + const supervisorModel = new FakeToolCallingChatModel({ + responses: [ + new AIMessage({ + content: "", + tool_calls: [ + { + name: "transfer_to_childagent", + args: {}, + id: "call_handoff", + type: "tool_call", + }, + ], + }), + new AIMessage({ content: "done" }), + ], + }); + + // Child agent model: produce a single final message + const agentModel = new FakeToolCallingChatModel({ + responses: [new AIMessage({ content: "classified" })], + }); + + const worker = createReactAgent({ + llm: agentModel, + tools: [], + name: "childAgent", + prompt: "you are terse", + }).withConfig({ tags: [TAG], runName: "agent:child_agent" }); + + const sup = createSupervisor({ agents: [worker], llm: supervisorModel }); + + const app = sup.compile(); + await app.invoke( + { messages: [{ role: "user", content: "hi" }] }, + { callbacks: [spy] } + ); + + const agentChain = seen.find( + (e) => + e.runType === "chain" && + Array.isArray(e.tags) && + e.tags.includes(TAG) && + (e.metadata as { langgraph_node?: string } | undefined) + ?.langgraph_node === "childAgent" + ); + expect(agentChain).toBeTruthy(); + expect(agentChain?.tags).toEqual(expect.arrayContaining([TAG])); + }); +}); From 3a6380e75a0ac0ec7cf8b058e14663d624dfecdb Mon Sep 17 00:00:00 2001 From: Liran Sharir Date: Sun, 17 Aug 2025 13:07:00 +0300 Subject: [PATCH 2/2] changeset --- .changeset/tidy-days-end.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tidy-days-end.md diff --git a/.changeset/tidy-days-end.md b/.changeset/tidy-days-end.md new file mode 100644 index 000000000..928b8dec0 --- /dev/null +++ b/.changeset/tidy-days-end.md @@ -0,0 +1,5 @@ +--- +"@langchain/langgraph-supervisor": patch +--- + +supervisor: merge child agent's bound config with invocation config on handoff