Implementation Question: How to pass state between agents with supervisor #205

Closed
opened 2026-02-15 17:17:19 -05:00 by yindo · 5 comments
Owner

Originally created by @cwoolum on GitHub (Mar 18, 2025).

I'm implementing an agent orchestration and I have a supervisor delegating work to a number of agents that are each specialized. What I'd like to do is use the outputs of some of the agents for input to the other agents. All of my agents are built with createReactAgent with responseFormat and I'm using createSupervisor to define the supervisor node. The part I'm struggling with is how to take the output of the agent and get it added to the state so that I can then include that in the initial prompts for the agents. Is it possible to do this with the createReactAgent and createSupervisor helpers or do I need to get more low level than that?

Here's an example. Ideally, we'd want the output of the party ideas expert to be passed to the party events expert

export const PartyPlanningAnnotation = Annotation.Root({
  messages: Annotation<BaseMessage[], Messages>({
    reducer: (current, next) => {
      return messagesStateReducer(current, next);
    },
    default: () => [],
  }),
  partyIdeas: Annotation<string[]>,
  foodIdeas: Annotation<string[]>,
});

const partyIdeasExpert = createReactAgent({
    llm,
    tools: [...toolDefs],
    name: 'party_ideas_expert',
    prompt: partyIdeasExpertPromptTemplate,
    stateSchema: PartyPlanningAnnotation,
    // or something more complex
    responseFormat: z.array(z.string())
});

const partyEventsExpert = createReactAgent({
    llm,
    tools: [...toolDefs],
    name: 'party_events_expert',
    prompt: (state) => partyEventsExpertPromptTemplate.format({ partyIdeas: state.partyIdeas }),
    stateSchema: PartyPlanningAnnotation,
    responseFormat: z.array(z.string())
});

const workflow = createSupervisor({
    agents: [
      partyIdeasExpert,
      partyEventsExpert,
      partyFoodExpert
    ],
    llm,
    stateSchema: PartyPlanningAnnotation ,
  });
Originally created by @cwoolum on GitHub (Mar 18, 2025). I'm implementing an agent orchestration and I have a supervisor delegating work to a number of agents that are each specialized. What I'd like to do is use the outputs of some of the agents for input to the other agents. All of my agents are built with `createReactAgent` with `responseFormat` and I'm using `createSupervisor` to define the supervisor node. The part I'm struggling with is how to take the output of the agent and get it added to the state so that I can then include that in the initial prompts for the agents. Is it possible to do this with the `createReactAgent` and `createSupervisor` helpers or do I need to get more low level than that? Here's an example. Ideally, we'd want the output of the party ideas expert to be passed to the party events expert ```ts export const PartyPlanningAnnotation = Annotation.Root({ messages: Annotation<BaseMessage[], Messages>({ reducer: (current, next) => { return messagesStateReducer(current, next); }, default: () => [], }), partyIdeas: Annotation<string[]>, foodIdeas: Annotation<string[]>, }); const partyIdeasExpert = createReactAgent({ llm, tools: [...toolDefs], name: 'party_ideas_expert', prompt: partyIdeasExpertPromptTemplate, stateSchema: PartyPlanningAnnotation, // or something more complex responseFormat: z.array(z.string()) }); const partyEventsExpert = createReactAgent({ llm, tools: [...toolDefs], name: 'party_events_expert', prompt: (state) => partyEventsExpertPromptTemplate.format({ partyIdeas: state.partyIdeas }), stateSchema: PartyPlanningAnnotation, responseFormat: z.array(z.string()) }); const workflow = createSupervisor({ agents: [ partyIdeasExpert, partyEventsExpert, partyFoodExpert ], llm, stateSchema: PartyPlanningAnnotation , }); ```
yindo closed this issue 2026-02-15 17:17:19 -05:00
Author
Owner

@vbarda commented on GitHub (Mar 18, 2025):

@cwoolum there are 2 options:

1/ with the existing high-level prebuilts (createReactAgent / createSupervisor) the only way you can update non-message state in the ReAct agent is via tools updating the graph state. check out this how-to guide for that https://langchain-ai.github.io/langgraphjs/how-tos/update-state-from-tools/
2/ go more low level, as you suggested - this would allow you to explicitly modify the state returned from your individual agents, and then you can still add them to the supervisor and use createSupervisor (as it simply takes CompiledStateGraph objects)

i would recommend going w/ (1) first and if that is not sufficient, go more low-level

@vbarda commented on GitHub (Mar 18, 2025): @cwoolum there are 2 options: 1/ with the existing high-level prebuilts (`createReactAgent` / `createSupervisor`) the only way you can update non-message state in the ReAct agent is via tools updating the graph state. check out this how-to guide for that https://langchain-ai.github.io/langgraphjs/how-tos/update-state-from-tools/ 2/ go more low level, as you suggested - this would allow you to explicitly modify the state returned from your individual agents, and then you can still add them to the supervisor and use `createSupervisor` (as it simply takes `CompiledStateGraph` objects) i would recommend going w/ (1) first and if that is not sufficient, go more low-level
Author
Owner

@cwoolum commented on GitHub (Mar 19, 2025):

Thanks! I went with route 1 but it feels a bit clunky. I removed responseFormat from my agent and instead added a new tool

tool(
  async (data, config) =>
    new Command({
      update: {
        partyIdeas: data,
        messages: [
          new ToolMessage({
            content: 'Successfully saved party ideas information',
            tool_call_id: config.toolCall.id,
          }),
        ],
      },
    }),
  {
    name: 'Response',
    description: 'Always respond to the user using this tool.',
    schema:  z.array(z.string()),
  },
),

It would be a nice addition to have the ability to map an agent response directly to a state value. Thinking about it more though, I think it would be better to have subgraphs so that all the data for my agents are isolated. I don't think this approach would work in that case, would it? Looking at the docs, it doesn't really even seem possible to use subgraphs with createSupervisor?

It would probably make more sense to go this route?
https://langchain-ai.github.io/langgraphjs/how-tos/multi-agent-network-functional/#travel-agent-example

@cwoolum commented on GitHub (Mar 19, 2025): Thanks! I went with route 1 but it feels a bit clunky. I removed `responseFormat` from my agent and instead added a new tool ```ts tool( async (data, config) => new Command({ update: { partyIdeas: data, messages: [ new ToolMessage({ content: 'Successfully saved party ideas information', tool_call_id: config.toolCall.id, }), ], }, }), { name: 'Response', description: 'Always respond to the user using this tool.', schema: z.array(z.string()), }, ), ``` It would be a nice addition to have the ability to map an agent response directly to a state value. Thinking about it more though, I think it would be better to have subgraphs so that all the data for my agents are isolated. I don't think this approach would work in that case, would it? Looking at the docs, it doesn't really even seem possible to use subgraphs with `createSupervisor`? It would probably make more sense to go this route? https://langchain-ai.github.io/langgraphjs/how-tos/multi-agent-network-functional/#travel-agent-example
Author
Owner

@vbarda commented on GitHub (Mar 19, 2025):

it feels a bit clunky

I agree - i think the cleanest route here is to allow users to define their own handoff tools - in that case you can pass any arbitrary state updates during the handoffs without requiring a separate tool. i plan to add support for that shortly

Thinking about it more though, I think it would be better to have subgraphs so that all the data for my agents are isolated

when you add agents to createSupervisor they're already added as subgraphs. you can definitely have isolated state for subgraphs, but I don't think it will work for what you're trying to do (since agents always return back to the supervisor, so the state needs to be shared at least with the supervisor, or with other agents)

Looking at the docs, it doesn't really even seem possible to use subgraphs with createSupervisor?

not sure what you mean by this - you can add any compiled state graph objects as agents, and they are added as subgraphs. however, if you want to wrap an agent in another graph (let's call it X) and add graph X as an agent, it won't work, since Command.PARENT used in handoffs navigates to closest parent, which would break handoffs. hope that maeks sense

@vbarda commented on GitHub (Mar 19, 2025): > it feels a bit clunky I agree - i think the cleanest route here is to allow users to define their own handoff tools - in that case you can pass any arbitrary state updates during the handoffs without requiring a separate tool. i plan to add support for that shortly > Thinking about it more though, I think it would be better to have subgraphs so that all the data for my agents are isolated when you add agents to `createSupervisor` they're already added as subgraphs. you can definitely have isolated state for subgraphs, but I don't think it will work for what you're trying to do (since agents always return back to the `supervisor`, so the state needs to be shared at least with the supervisor, or with other agents) > Looking at the docs, it doesn't really even seem possible to use subgraphs with createSupervisor? not sure what you mean by this - you can add any compiled state graph objects as agents, and they are added as subgraphs. however, if you want to wrap an agent in another graph (let's call it X) and add graph X as an agent, it won't work, since `Command.PARENT` used in handoffs navigates to closest parent, which would break handoffs. hope that maeks sense
Author
Owner

@cwoolum commented on GitHub (Mar 19, 2025):

if you want to wrap an agent in another graph (let's call it X) and add graph X as an agent, it won't work, since Command.PARENT used in handoffs navigates to closest parent, which would break handoffs. hope that maeks sense

Yeah, this is what I was referring to. My only choice right now seems to be to share the state graph because I can't be explicit about what state I want passed from the supervisor.

If I do something similar to this in the docs, I could use different state for the child agent and only pass the state it needs to operate.

const callTravelAdvisor = task("callTravelAdvisor", async (messages: BaseMessage[]) => {
  const response = travelAdvisor.invoke({ messages });
  return response.messages;
});
@cwoolum commented on GitHub (Mar 19, 2025): > if you want to wrap an agent in another graph (let's call it X) and add graph X as an agent, it won't work, since Command.PARENT used in handoffs navigates to closest parent, which would break handoffs. hope that maeks sense Yeah, this is what I was referring to. My only choice right now seems to be to share the state graph because I can't be explicit about what state I want passed from the supervisor. If I do something similar to this in the docs, I could use different state for the child agent and only pass the state it needs to operate. ``` const callTravelAdvisor = task("callTravelAdvisor", async (messages: BaseMessage[]) => { const response = travelAdvisor.invoke({ messages }); return response.messages; }); ```
Author
Owner

@cwoolum commented on GitHub (Mar 20, 2025):

I was able to get this working in a minimally invasive way. I reused most of the code within createSupervisor but then replaced this code

for (const agent of agents) {
    builder = builder.addNode(
      agent.name!,
      makeCallAgent(agent, outputMode, addHandoffBackMessages, supervisorName),
      {
        subgraphs: [agent],
      }
    );
    builder = builder.addEdge(agent.name!, supervisorAgent.name!);
  }

with explicit implementations for each of my agents

builder = builder.addNode(
    childAgent.name!,
    async (state: SupervisorState) => {
      const inputMessages =
        await childAgentPromptTemplate.formatMessages({
          someRequiredProp: JSON.stringify(state.somethingThatNeedMapped, null, 2),          
        });

      const output = await childAgent.invoke({
        messages: inputMessages,
      });
      let { messages } = output;

      messages = messages.slice(-1);

      messages.push(
        ...createHandoffBackMessages(childAgent.name!, 'supervisor'),
      );

      return new Command({
        update: {
          childAgentOutput: output.structuredResponse,
          messages: messages,
        },
      });
    },
    {
      subgraphs: [childAgent],
    },
  );

  builder = builder.addEdge(childAgent.name!, supervisorAgent.name!);

I did try to do it in a generic way but it was going to take me a while to figure out the proper types. I also noticed makeCallAgent isn't typed properly so I wasn't the only one struggling with it 😂.

This approach let me keep my child agents defined as specified in the docs for the supervisor pattern. I set the resposneFormat on the agent and then just take that output and map it to the parent state with Command.

@cwoolum commented on GitHub (Mar 20, 2025): I was able to get this working in a minimally invasive way. I reused most of the code within `createSupervisor` but then replaced this code ```ts for (const agent of agents) { builder = builder.addNode( agent.name!, makeCallAgent(agent, outputMode, addHandoffBackMessages, supervisorName), { subgraphs: [agent], } ); builder = builder.addEdge(agent.name!, supervisorAgent.name!); } ``` with explicit implementations for each of my agents ```ts builder = builder.addNode( childAgent.name!, async (state: SupervisorState) => { const inputMessages = await childAgentPromptTemplate.formatMessages({ someRequiredProp: JSON.stringify(state.somethingThatNeedMapped, null, 2), }); const output = await childAgent.invoke({ messages: inputMessages, }); let { messages } = output; messages = messages.slice(-1); messages.push( ...createHandoffBackMessages(childAgent.name!, 'supervisor'), ); return new Command({ update: { childAgentOutput: output.structuredResponse, messages: messages, }, }); }, { subgraphs: [childAgent], }, ); builder = builder.addEdge(childAgent.name!, supervisorAgent.name!); ``` I did try to do it in a generic way but it was going to take me a while to figure out the proper types. I also noticed `makeCallAgent ` isn't typed properly so I wasn't the only one struggling with it 😂. This approach let me keep my child agents defined as specified in the docs for the supervisor pattern. I set the `resposneFormat` on the agent and then just take that output and map it to the parent state with `Command`.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraphjs#205