createReactAgent enters infinite loop with prompt function and fails to return structured response with stateSchema defined #263

Open
opened 2026-02-15 18:15:12 -05:00 by yindo · 1 comment
Owner

Originally created by @shahafa on GitHub (May 22, 2025).

I'm working on building a ReactAgent that accepts a function as the prompt and uses a stateSchema for managing the state. Here's the basic setup that works:

I define an OpenAI llm with gpt-4o-mini
I use a DynamicStructuredTool with a Zod schema to simulate a weather lookup
I specify a responseFormat to get structured output

This setup works fine when I just send a message like "what's the weather in sf?".

const llm = new ChatOpenAI({
  model: "gpt-4o-mini",
  temperature: 0,
});

const weatherToolSchema = z.object({
  city: z.string().describe("The city to get the weather of"),
});

const weatherTool = new DynamicStructuredTool({
  name: "get_weather",
  description: "A tool to get the weather of a city",
  schema: weatherToolSchema,
  func: async ({ city }: z.infer<typeof weatherToolSchema>) => {
    return `the weather in ${city} is sunny`;
  },
});

const responseFormat = z.object({
  "weather condition": z.string().describe("The weather condition"),
});

const agent = createReactAgent({
  llm,
  tools: [weatherTool],
  responseFormat,
});

const response = await agent.invoke({
  messages: [new HumanMessage("what's the weather in sf?")],
});

Problem 1: Endless Tool Loop with prompt Function

When I try to dynamically build the prompt using a function, the agent gets stuck in an infinite loop, repeatedly calling the tool.

const llm = new ChatOpenAI({
  model: "gpt-4o-mini",
  temperature: 0,
});

const weatherToolSchema = z.object({
  city: z.string().describe("The city to get the weather of"),
});

const weatherTool = new DynamicStructuredTool({
  name: "get_weather",
  description: "A tool to get the weather of a city",
  schema: weatherToolSchema,
  func: async ({ city }: z.infer<typeof weatherToolSchema>) => {
    return `the weather in ${city} is sunny`;
  },
});

const prompt = async (): Promise<BaseMessageLike[]> => {
  const city = "San Francisco";

  const promptTemplate = ChatPromptTemplate.fromMessages([
    ["user", `What's the weather in ${city}`],
  ]);

  const promptValue = await promptTemplate.invoke({
    city,
  });

  return promptValue.messages;
};

const responseFormat = z.object({
  "weather condition": z.string().describe("The weather condition"),
});

const agent = createReactAgent({
  llm,
  tools: [weatherTool],
  prompt,
  responseFormat,
});

const response = await agent.invoke({
  messages: [new HumanMessage("what's the weather in sf?")],
});

Problem 2: No Structured Response with stateSchema

When I introduce a stateSchema using Annotation, the agent doesn't return the structured response

const llm = new ChatOpenAI({
  model: "gpt-4o-mini",
  temperature: 0,
});

const weatherToolSchema = z.object({
  city: z.string().describe("The city to get the weather of"),
});

const weatherTool = new DynamicStructuredTool({
  name: "get_weather",
  description: "A tool to get the weather of a city",
  schema: weatherToolSchema,
  func: async ({ city }: z.infer<typeof weatherToolSchema>) => {
    return `the weather in ${city} is sunny`;
  },
});

const stateSchema = Annotation.Root({
  ...MessagesAnnotation.spec,
  city: Annotation<string>,
});

const responseFormat = z.object({
  "weather condition": z.string().describe("The weather condition"),
});

const agent = createReactAgent({
  llm,
  tools: [weatherTool],
  stateSchema,
  responseFormat,
});

const response = await agent.invoke({
  city: "sf",
});

I would appreciate any help

Originally created by @shahafa on GitHub (May 22, 2025). I'm working on building a ReactAgent that accepts a function as the prompt and uses a stateSchema for managing the state. Here's the basic setup that works: I define an OpenAI llm with gpt-4o-mini I use a DynamicStructuredTool with a Zod schema to simulate a weather lookup I specify a responseFormat to get structured output This setup works fine when I just send a message like "what's the weather in sf?". ``` const llm = new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0, }); const weatherToolSchema = z.object({ city: z.string().describe("The city to get the weather of"), }); const weatherTool = new DynamicStructuredTool({ name: "get_weather", description: "A tool to get the weather of a city", schema: weatherToolSchema, func: async ({ city }: z.infer<typeof weatherToolSchema>) => { return `the weather in ${city} is sunny`; }, }); const responseFormat = z.object({ "weather condition": z.string().describe("The weather condition"), }); const agent = createReactAgent({ llm, tools: [weatherTool], responseFormat, }); const response = await agent.invoke({ messages: [new HumanMessage("what's the weather in sf?")], }); ``` ### Problem 1: Endless Tool Loop with prompt Function When I try to dynamically build the prompt using a function, the agent gets stuck in an infinite loop, repeatedly calling the tool. ``` const llm = new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0, }); const weatherToolSchema = z.object({ city: z.string().describe("The city to get the weather of"), }); const weatherTool = new DynamicStructuredTool({ name: "get_weather", description: "A tool to get the weather of a city", schema: weatherToolSchema, func: async ({ city }: z.infer<typeof weatherToolSchema>) => { return `the weather in ${city} is sunny`; }, }); const prompt = async (): Promise<BaseMessageLike[]> => { const city = "San Francisco"; const promptTemplate = ChatPromptTemplate.fromMessages([ ["user", `What's the weather in ${city}`], ]); const promptValue = await promptTemplate.invoke({ city, }); return promptValue.messages; }; const responseFormat = z.object({ "weather condition": z.string().describe("The weather condition"), }); const agent = createReactAgent({ llm, tools: [weatherTool], prompt, responseFormat, }); const response = await agent.invoke({ messages: [new HumanMessage("what's the weather in sf?")], }); ``` ### Problem 2: No Structured Response with stateSchema When I introduce a stateSchema using Annotation, the agent doesn't return the structured response ``` const llm = new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0, }); const weatherToolSchema = z.object({ city: z.string().describe("The city to get the weather of"), }); const weatherTool = new DynamicStructuredTool({ name: "get_weather", description: "A tool to get the weather of a city", schema: weatherToolSchema, func: async ({ city }: z.infer<typeof weatherToolSchema>) => { return `the weather in ${city} is sunny`; }, }); const stateSchema = Annotation.Root({ ...MessagesAnnotation.spec, city: Annotation<string>, }); const responseFormat = z.object({ "weather condition": z.string().describe("The weather condition"), }); const agent = createReactAgent({ llm, tools: [weatherTool], stateSchema, responseFormat, }); const response = await agent.invoke({ city: "sf", }); ``` I would appreciate any help
Author
Owner

@john01kpeazu commented on GitHub (Sep 5, 2025):

I was able to debug this issue! You are correct - the behavior flips when you pass in a pure string vs when you wrap that string in a function/RunnableLambda. Langgraph uses these outputs to construct the final LLM input in two completely different ways:

  • When you give it a pure string as a prompt, Langgraph takes that prompt and prepends it to the existing value in your messages state, which gets passed to the model.
  • When you give it a function/RunnableLambda, Langgraph ignores all other state and only passes up the result of your function.

Hence, when using a function/runnablelamda as a prompt, you must construct your complete LLM input from state, and attach the chat history manually.

The loop is happening because your previous tool calls/plans are not being sent back to the LLM, so the LLM is sending out these calls/plans over and over again as if it's the first time receiving the request.

@john01kpeazu commented on GitHub (Sep 5, 2025): I was able to debug this issue! You are correct - the behavior flips when you pass in a pure string vs when you wrap that string in a function/RunnableLambda. Langgraph uses these outputs to construct the final LLM input in two completely different ways: - When you give it a pure `string` as a prompt, Langgraph takes that prompt and _prepends it_ to the existing value in your `messages` state, which gets passed to the model. - When you give it a function/RunnableLambda, Langgraph _ignores_ all other state and _only_ passes up the result of your function. Hence, when using a function/runnablelamda as a prompt, you must construct your _complete_ LLM input from state, and attach the chat history manually. The loop is happening because your previous tool calls/plans are not being sent back to the LLM, so the LLM is sending out these calls/plans over and over again as if it's the first time receiving the request.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraphjs#263