[GH-ISSUE #397] When a sub-agent is invoked, the config.metadata.lc_agent_name isn’t being configured correctly by its main agent. #256

Closed
opened 2026-06-05 17:21:18 -04:00 by yindo · 3 comments
Owner

Originally created by @HaolongChen on GitHub (Mar 31, 2026).
Original GitHub issue: https://github.com/langchain-ai/deepagentsjs/issues/397

Case 1:

When the name of main agent is not set when calling createDeepAgent(), the lc_agent_name of all sub-agents are configured correctly though lc_agent_name field is missing in metadata of main agent.

Image

Case 2:

When the name of main agent is set when calling createDeepAgent(), the lc_agent_name of all sub-agents are misconfigured to the name of main agent just like the lc_agent_name of main agent.

Image

Related source code

  1. The name of sub-agent has been set correctly when being created. But subagent is invoked with config of main agent, which includes config.metadata.lc_agent_name.
    deepagents: ./src/middleware/subagents.ts:601
function createTaskTool(
...
) {
      ...
      const subagent = subagentGraphs[subagent_type];

      // Get current state and filter it for subagent
      const currentState = getCurrentTaskInput<Record<string, unknown>>();
      const subagentState = filterStateForSubagent(currentState);
      subagentState.messages = [new HumanMessage({ content: description })];

      // Invoke the subagent
      const result = (await subagent.invoke(subagentState, config)) as Record<
        string,
        unknown
      >;
     ...
}
  1. When sub-agent is invoked, its config is merged again with given parameter config, which is not expected to contain config.metadata.
    langchain: ./src/agents/ReactAgent.ts:1191
  /**
   * Executes the agent with the given state and returns the final state after all processing.
   *
   * This method runs the agent's entire workflow synchronously, including:
   * - Processing the input messages through any configured middleware
   * - Calling the language model to generate responses
   * - Executing any tool calls made by the model
   * - Running all middleware hooks (beforeModel, afterModel, etc.)
   *
   * @param state - The initial state for the agent execution. Can be:
   *   - An object containing `messages` array and any middleware-specific state properties
   *   - A Command object for more advanced control flow
   *
   * @param config - Optional runtime configuration including:
   * @param config.context - The context for the agent execution.
   * @param config.configurable - LangGraph configuration options like `thread_id`, `run_id`, etc.
   * @param config.store - The store for the agent execution for persisting state, see more in {@link https://docs.langchain.com/oss/javascript/langgraph/memory#memory-storage | Memory storage}.
   * @param config.signal - An optional {@link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal | `AbortSignal`} for the agent execution.
   * @param config.recursionLimit - The recursion limit for the agent execution.
   *
   * @returns A Promise that resolves to the final agent state after execution completes.
   *          The returned state includes:
   *          - a `messages` property containing an array with all messages (input, AI responses, tool calls/results)
   *          - a `structuredResponse` property containing the structured response (if configured)
   *          - all state values defined in the middleware
   *
   * @example
   * ```typescript
   * const agent = new ReactAgent({
   *   llm: myModel,
   *   tools: [calculator, webSearch],
   *   responseFormat: z.object({
   *     weather: z.string(),
   *   }),
   * });
   *
   * const result = await agent.invoke({
   *   messages: [{ role: "human", content: "What's the weather in Paris?" }]
   * });
   *
   * console.log(result.structuredResponse.weather); // outputs: "It's sunny and 75°F."
   * ```
   */
  async invoke(
    state: InvokeStateParameter<Types>,
    config?: InvokeConfiguration<
      InferContextInput<
        Types["Context"] extends AnyAnnotationRoot | InteropZodObject
          ? Types["Context"]
          : AnyAnnotationRoot
      > &
        InferMiddlewareContextInputs<Types["Middleware"]>
    >
  ) {
    type FullState = MergedAgentState<Types>;
    const mergedConfig = mergeConfigs(this.#defaultConfig, config);
    const initializedState = await this.#initializeMiddlewareStates(
      state,
      mergedConfig as RunnableConfig
    );

    return this.#graph.invoke(
      initializedState,
      mergedConfig as unknown as InferContextInput<
        Types["Context"] extends AnyAnnotationRoot | InteropZodObject
          ? Types["Context"]
          : AnyAnnotationRoot
      > &
        InferMiddlewareContextInputs<Types["Middleware"]>
    ) as Promise<FullState>;
  }
  1. When the config of sub-agent is merged, config.metadata is always overwritten once if config.metadata is defined.
    langchain-core: ./src/runnables/config.ts:19
export function mergeConfigs<CallOptions extends RunnableConfig>(
  ...configs: (CallOptions | RunnableConfig | undefined | null)[]
): Partial<CallOptions> {
  // We do not want to call ensureConfig on the empty state here as this may cause
  // double loading of callbacks if async local storage is being used.
  const copy: Partial<CallOptions> = {};
  for (const options of configs.filter((c): c is CallOptions => !!c)) {
    for (const key of Object.keys(options)) {
      if (key === "metadata") {
        copy[key] = { ...copy[key], ...options[key] };
      } else if (key === "tags") {
        const baseKeys: string[] = copy[key] ?? [];
        copy[key] = [...new Set(baseKeys.concat(options[key] ?? []))];
      } else if (key === "configurable") {
        copy[key] = { ...copy[key], ...options[key] };
      } else if (key === "timeout") {
        if (copy.timeout === undefined) {
          copy.timeout = options.timeout;
        } else if (options.timeout !== undefined) {
          copy.timeout = Math.min(copy.timeout, options.timeout);
        }
      } else if (key === "signal") {
        if (copy.signal === undefined) {
          copy.signal = options.signal;
        } else if (options.signal !== undefined) {
          if ("any" in AbortSignal) {
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            copy.signal = (AbortSignal as any).any([
              copy.signal,
              options.signal,
            ]);
          } else {
            copy.signal = options.signal;
          }
        }
      } else if (key === "callbacks") {
        const baseCallbacks = copy.callbacks;
        const providedCallbacks = options.callbacks;
        // callbacks can be either undefined, Array<handler> or manager
        // so merging two callbacks values has 6 cases
        if (Array.isArray(providedCallbacks)) {
          if (!baseCallbacks) {
            copy.callbacks = providedCallbacks;
          } else if (Array.isArray(baseCallbacks)) {
            copy.callbacks = baseCallbacks.concat(providedCallbacks);
          } else {
            // baseCallbacks is a manager
            const manager = baseCallbacks.copy();
            for (const callback of providedCallbacks) {
              manager.addHandler(ensureHandler(callback), true);
            }
            copy.callbacks = manager;
          }
        } else if (providedCallbacks) {
          // providedCallbacks is a manager
          if (!baseCallbacks) {
            copy.callbacks = providedCallbacks;
          } else if (Array.isArray(baseCallbacks)) {
            const manager = providedCallbacks.copy();
            for (const callback of baseCallbacks) {
              manager.addHandler(ensureHandler(callback), true);
            }
            copy.callbacks = manager;
          } else {
            // baseCallbacks is also a manager
            copy.callbacks = new CallbackManager(
              providedCallbacks._parentRunId,
              {
                handlers: baseCallbacks.handlers.concat(
                  providedCallbacks.handlers
                ),
                inheritableHandlers: baseCallbacks.inheritableHandlers.concat(
                  providedCallbacks.inheritableHandlers
                ),
                tags: Array.from(
                  new Set(baseCallbacks.tags.concat(providedCallbacks.tags))
                ),
                inheritableTags: Array.from(
                  new Set(
                    baseCallbacks.inheritableTags.concat(
                      providedCallbacks.inheritableTags
                    )
                  )
                ),
                metadata: {
                  ...baseCallbacks.metadata,
                  ...providedCallbacks.metadata,
                },
              }
            );
          }
        }
      } else {
        const typedKey = key as keyof CallOptions;
        copy[typedKey] = options[typedKey] ?? copy[typedKey];
      }
    }
  }
  return copy as Partial<CallOptions>;
}

Conclusion:

As a result, config.metadata.lc_agent_name is always replaced with valid lc_agent_name of main agent when being invoked. This also perfectly match the situation I mentioned.

Solution:

deepagents: ./src/middleware/subagents.ts:601

      const {metadata: _, ...subagentConfig} = config;
      // Invoke the subagent
      const result = (await subagent.invoke(subagentState, subagentConfig)) as Record<
        string,
        unknown
      >;
Originally created by @HaolongChen on GitHub (Mar 31, 2026). Original GitHub issue: https://github.com/langchain-ai/deepagentsjs/issues/397 ## Case 1: When the name of main agent is not set when calling createDeepAgent(), the `lc_agent_name` of all sub-agents are configured correctly though `lc_agent_name` field is missing in metadata of main agent. <img width="962" height="624" alt="Image" src="https://github.com/user-attachments/assets/26e9ca9d-5493-4069-9a8a-479ef49901ff" /> ## Case 2: When the name of main agent is set when calling createDeepAgent(), the `lc_agent_name` of all sub-agents are misconfigured to the name of main agent just like the `lc_agent_name` of main agent. <img width="890" height="602" alt="Image" src="https://github.com/user-attachments/assets/d965b681-c9f2-42d8-8355-557eec28c4d3" /> # Related source code 1. The name of sub-agent has been set correctly when being created. But subagent is invoked with config of main agent, which includes `config.metadata.lc_agent_name`. [deepagents: ./src/middleware/subagents.ts:601](https://github.com/langchain-ai/deepagentsjs/blob/main/libs/deepagents/src/middleware/subagents.ts#L601) ```typescript function createTaskTool( ... ) { ... const subagent = subagentGraphs[subagent_type]; // Get current state and filter it for subagent const currentState = getCurrentTaskInput<Record<string, unknown>>(); const subagentState = filterStateForSubagent(currentState); subagentState.messages = [new HumanMessage({ content: description })]; // Invoke the subagent const result = (await subagent.invoke(subagentState, config)) as Record< string, unknown >; ... } ``` 2. When sub-agent is invoked, its config is merged again with given parameter `config`, which is not expected to contain `config.metadata`. [langchain: ./src/agents/ReactAgent.ts:1191](https://github.com/langchain-ai/langchainjs/blob/main/libs/langchain/src/agents/ReactAgent.ts#L1191) ```typescript /** * Executes the agent with the given state and returns the final state after all processing. * * This method runs the agent's entire workflow synchronously, including: * - Processing the input messages through any configured middleware * - Calling the language model to generate responses * - Executing any tool calls made by the model * - Running all middleware hooks (beforeModel, afterModel, etc.) * * @param state - The initial state for the agent execution. Can be: * - An object containing `messages` array and any middleware-specific state properties * - A Command object for more advanced control flow * * @param config - Optional runtime configuration including: * @param config.context - The context for the agent execution. * @param config.configurable - LangGraph configuration options like `thread_id`, `run_id`, etc. * @param config.store - The store for the agent execution for persisting state, see more in {@link https://docs.langchain.com/oss/javascript/langgraph/memory#memory-storage | Memory storage}. * @param config.signal - An optional {@link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal | `AbortSignal`} for the agent execution. * @param config.recursionLimit - The recursion limit for the agent execution. * * @returns A Promise that resolves to the final agent state after execution completes. * The returned state includes: * - a `messages` property containing an array with all messages (input, AI responses, tool calls/results) * - a `structuredResponse` property containing the structured response (if configured) * - all state values defined in the middleware * * @example * ```typescript * const agent = new ReactAgent({ * llm: myModel, * tools: [calculator, webSearch], * responseFormat: z.object({ * weather: z.string(), * }), * }); * * const result = await agent.invoke({ * messages: [{ role: "human", content: "What's the weather in Paris?" }] * }); * * console.log(result.structuredResponse.weather); // outputs: "It's sunny and 75°F." * ``` */ async invoke( state: InvokeStateParameter<Types>, config?: InvokeConfiguration< InferContextInput< Types["Context"] extends AnyAnnotationRoot | InteropZodObject ? Types["Context"] : AnyAnnotationRoot > & InferMiddlewareContextInputs<Types["Middleware"]> > ) { type FullState = MergedAgentState<Types>; const mergedConfig = mergeConfigs(this.#defaultConfig, config); const initializedState = await this.#initializeMiddlewareStates( state, mergedConfig as RunnableConfig ); return this.#graph.invoke( initializedState, mergedConfig as unknown as InferContextInput< Types["Context"] extends AnyAnnotationRoot | InteropZodObject ? Types["Context"] : AnyAnnotationRoot > & InferMiddlewareContextInputs<Types["Middleware"]> ) as Promise<FullState>; } ``` 3. When the config of sub-agent is merged, `config.metadata` is always overwritten once if `config.metadata` is defined. [langchain-core: ./src/runnables/config.ts:19](https://github.com/langchain-ai/langchainjs/blob/main/libs/langchain-core/src/runnables/config.ts#L19) ```typescript export function mergeConfigs<CallOptions extends RunnableConfig>( ...configs: (CallOptions | RunnableConfig | undefined | null)[] ): Partial<CallOptions> { // We do not want to call ensureConfig on the empty state here as this may cause // double loading of callbacks if async local storage is being used. const copy: Partial<CallOptions> = {}; for (const options of configs.filter((c): c is CallOptions => !!c)) { for (const key of Object.keys(options)) { if (key === "metadata") { copy[key] = { ...copy[key], ...options[key] }; } else if (key === "tags") { const baseKeys: string[] = copy[key] ?? []; copy[key] = [...new Set(baseKeys.concat(options[key] ?? []))]; } else if (key === "configurable") { copy[key] = { ...copy[key], ...options[key] }; } else if (key === "timeout") { if (copy.timeout === undefined) { copy.timeout = options.timeout; } else if (options.timeout !== undefined) { copy.timeout = Math.min(copy.timeout, options.timeout); } } else if (key === "signal") { if (copy.signal === undefined) { copy.signal = options.signal; } else if (options.signal !== undefined) { if ("any" in AbortSignal) { // eslint-disable-next-line @typescript-eslint/no-explicit-any copy.signal = (AbortSignal as any).any([ copy.signal, options.signal, ]); } else { copy.signal = options.signal; } } } else if (key === "callbacks") { const baseCallbacks = copy.callbacks; const providedCallbacks = options.callbacks; // callbacks can be either undefined, Array<handler> or manager // so merging two callbacks values has 6 cases if (Array.isArray(providedCallbacks)) { if (!baseCallbacks) { copy.callbacks = providedCallbacks; } else if (Array.isArray(baseCallbacks)) { copy.callbacks = baseCallbacks.concat(providedCallbacks); } else { // baseCallbacks is a manager const manager = baseCallbacks.copy(); for (const callback of providedCallbacks) { manager.addHandler(ensureHandler(callback), true); } copy.callbacks = manager; } } else if (providedCallbacks) { // providedCallbacks is a manager if (!baseCallbacks) { copy.callbacks = providedCallbacks; } else if (Array.isArray(baseCallbacks)) { const manager = providedCallbacks.copy(); for (const callback of baseCallbacks) { manager.addHandler(ensureHandler(callback), true); } copy.callbacks = manager; } else { // baseCallbacks is also a manager copy.callbacks = new CallbackManager( providedCallbacks._parentRunId, { handlers: baseCallbacks.handlers.concat( providedCallbacks.handlers ), inheritableHandlers: baseCallbacks.inheritableHandlers.concat( providedCallbacks.inheritableHandlers ), tags: Array.from( new Set(baseCallbacks.tags.concat(providedCallbacks.tags)) ), inheritableTags: Array.from( new Set( baseCallbacks.inheritableTags.concat( providedCallbacks.inheritableTags ) ) ), metadata: { ...baseCallbacks.metadata, ...providedCallbacks.metadata, }, } ); } } } else { const typedKey = key as keyof CallOptions; copy[typedKey] = options[typedKey] ?? copy[typedKey]; } } } return copy as Partial<CallOptions>; } ``` # Conclusion: As a result, `config.metadata.lc_agent_name` is always replaced with valid `lc_agent_name` of main agent when being invoked. This also perfectly match the situation I mentioned. # Solution: [deepagents: ./src/middleware/subagents.ts:601](https://github.com/langchain-ai/deepagentsjs/blob/main/libs/deepagents/src/middleware/subagents.ts#L601) ```typescript const {metadata: _, ...subagentConfig} = config; // Invoke the subagent const result = (await subagent.invoke(subagentState, subagentConfig)) as Record< string, unknown >; ```
yindo closed this issue 2026-06-05 17:21:18 -04:00
Author
Owner

@MrQuartz99 commented on GitHub (Apr 24, 2026):

any updates to this?

<!-- gh-comment-id:4314332585 --> @MrQuartz99 commented on GitHub (Apr 24, 2026): any updates to this?
Author
Owner

@HaolongChen commented on GitHub (Apr 25, 2026):

nah im still waiting for maintainers’ approval of my solutions before opening a pr. and that’s what Contributing.md requires.

however, I’ve provided a solution, and my testing suggests it should fix the bug.

If you really need the correct lc_agent_name for subagents, I believe you could fork this repo and apply this patch.

<!-- gh-comment-id:4318085630 --> @HaolongChen commented on GitHub (Apr 25, 2026): nah im still waiting for maintainers’ approval of my solutions before opening a pr. and that’s what [Contributing.md](https://github.com/langchain-ai/deepagentsjs?tab=contributing-ov-file) requires. however, I’ve provided a solution, and my testing suggests it should fix the bug. If you really need the correct `lc_agent_name` for subagents, I believe you could fork this repo and apply this patch.
Author
Owner

@hntrl commented on GitHub (Jun 2, 2026):

Hi all! Apologies for the delay in response. This is being remediated here: #566

<!-- gh-comment-id:4605799548 --> @hntrl commented on GitHub (Jun 2, 2026): Hi all! Apologies for the delay in response. This is being remediated here: #566
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/deepagentsjs#256