withReActLoop
Inserts a built-in ReAct (Reason + Act) agent loop into the flow. Automatically handles the think → tool calls → observation → repeat cycle until the agent signals it's done or the iteration limit is reached.
Setup
typescript
import { FlowBuilder } from "flowneer";
import { withReActLoop } from "flowneer/presets/agent";
import { withTools } from "flowneer/plugins/tools";
const AppFlow = FlowBuilder.extend([withTools, withReActLoop]);Usage
typescript
import { z } from "zod";
interface AgentState {
question: string;
history: string[];
__toolResults?: any[];
__reactOutput?: string;
__reactExhausted?: boolean;
}
const calculatorTool = {
name: "calculator",
description: "Evaluate a math expression",
params: {
expression: {
type: "string" as const,
description: "The expression to evaluate",
},
},
execute: ({ expression }: { expression: string }) => eval(expression),
};
const flow = new AppFlow<AgentState>()
.withTools([calculatorTool])
.withReActLoop({
think: async (s) => {
const response = await callLlm(buildReActPrompt(s));
// Parse tool calls from LLM response
const toolCalls = parseToolCalls(response);
if (toolCalls.length > 0) {
return { action: "tool", calls: toolCalls };
}
return { action: "finish", output: response };
},
maxIterations: 10,
onObservation: async (results, s) => {
s.history.push(`Tool results: ${JSON.stringify(results)}`);
},
})
.then((s) => {
if (s.__reactExhausted) {
console.log("Agent hit iteration limit");
} else {
console.log("Answer:", s.__reactOutput);
}
});Options
| Option | Type | Default | Description |
|---|---|---|---|
think | (shared, params) => ThinkResult | Promise<ThinkResult> | — | The reasoning step |
maxIterations | number | 10 | Maximum think→act cycles |
onObservation | (results, shared, params) => void | Promise<void> | — | Called after each tool execution round |
ThinkResult Type
typescript
type ThinkResult =
| { action: "finish"; output?: unknown } // done — stop the loop
| { action: "tool"; calls: ToolCall[] }; // call these tools and loopState Keys
| Key | Direction | Description |
|---|---|---|
__tools | Set by withTools | The ToolRegistry — required |
__toolResults | Read in think | Results from the last tool round |
__reactIterations | Internal | Running iteration count — reset each .run() call |
__reactOutput | Read after loop | The output from the final { action: "finish" } |
__reactExhausted | Read after loop | true if maxIterations was reached |
How It Works
Behind the scenes, withReActLoop compiles down to:
.loop(
(s) => !s.__reactFinished && (s.__reactIterations ?? 0) < maxIterations,
(b) => b
.startWith(increment __reactIterations; think → set __pendingToolCalls or __reactFinished)
.then(execute tools from __pendingToolCalls → set __toolResults)
)
.then(mark __reactExhausted if needed; delete __reactIterations)The iteration counter lives on shared.__reactIterations (not in a closure), so the loop resets correctly when .run() is called multiple times on the same flow instance.
Requires withTools
withReActLoop expects shared.__tools to be a ToolRegistry. Always call .withTools([...]) before .withReActLoop().
See Also
withTools— tool registry and execution- Agent Patterns — multi-agent orchestration