Lesson 6: Module 06: Building a ReAct Agent
Dieser Inhalt ist noch nicht in deiner Sprache verfügbar.
Combine reasoning, tools, loop accumulators, and iteration limits into a full Reason-Act-Observe agent.
Learning Objectives
Section titled “Learning Objectives”- Understand the ReAct (Reason-Act-Observe — a pattern where the AI reasons, takes an action, observes the result, and repeats) loop
- Build an agent with explicit tool dispatch (routing the AI’s chosen action to the right tool machine) using
match - Carry working memory across iterations with loop accumulators
- Control loop termination
Complexity Ladder: Level 5 (Agentic) — the full ReAct pattern with tools, accumulators, and iteration.
The Concept: The Reason-Act-Observe Loop
Section titled “The Concept: The Reason-Act-Observe Loop”In Module 03, you learned that an ask step with tools IS an agent loop. That’s the simple version — mashin handles everything inside one step. But sometimes you want the loop in your own hands:
- Explicit working memory between iterations
- Custom tool dispatch (routing the AI’s chosen action to the right tool machine) logic
- Observation processing before the next reasoning round
- Iteration limits and loop detection
Think of a ReAct agent like a detective solving a case. The detective reasons about what they know, decides to investigate a lead (act), reviews what they found (observe), then reasons again about the next step. This continues until they have enough evidence to present their conclusion.
The ReAct pattern (Reason-Act-Observe) makes this explicit:
- Reason — The LLM analyzes the situation and decides what to do
- Act — Execute the chosen tool
- Observe — Process the result and update the loop’s working memory
- Loop — Go back to Reason, or respond if done
This is what MashinClaw uses internally — mashin’s built-in AI coding assistant, itself a machine written in mashin.
The whole loop is one while step. Its working memory lives in accumulators: named values seeded before the first iteration and updated by the loop body’s outputs each time around.
while explore loop.done == false┌─────────────────────────────────────────────────┐│ ││ REASON DISPATCH OBSERVE ││ ┌──────────┐ ┌──────────┐ ┌──────────┐ ││ │ LLM sees │───────►│ match │──►│ update │ ││ │ question │ │ routes │ │ history, │ ││ │ history │ │ to tool │ │ files, │ ││ │ tools │ │ machines │ │ done flag│ ││ └──────────┘ └──────────┘ └────┬─────┘ ││ ▲ │ ││ └──────── loop.* accumulators ◄────┘ ││ │└─────────────────────────────────────────────────┘Building It: File Explorer Agent
Section titled “Building It: File Explorer Agent”The executing version uses the runtime-native loop from Module 03: an ask step with tools IS the Reason-Act-Observe loop, run and governed by the runtime:
machine file_explorer "File Explorer Agent"
accepts question as string, is required
responds with answer as string files_read as list
behaves ask explore, using: "anthropic:claude-sonnet-4" with role "You are a code exploration agent. Find relevant files, read them, then answer. List every file you read." with task "Answer this question about the codebase:\n\n${input.question}" tools glob: "@mashin/actions/tools/glob" read_file: "@mashin/actions/tools/read_file" returns answer as string, is required files_read as list, is required
verifies test "explores and answers" assuming explore {answer: "The parser lives in lang/parser3.", files_read: ["lang/parser3/parser.ex"]} given {question: "Where does the parser live?"} expect {answer: "The parser lives in lang/parser3.", files_read: ["lang/parser3/parser.ex"]}To understand what that one step is doing for you, here is the same loop written by hand — and it runs. One rule shapes the code: an inline loop body takes exactly ONE element, so a multi-step body lives in a named flow that the loop runs each iteration:
machine file_explorer_by_hand "File Explorer, loop by hand"
accepts question as string, is required // The user's question max_iterations as integer, default: 8 // Safety limit on loop iterations
responds with answer as string files_read as list // Files the agent read along the way iterations as integer // How many loops it took
behaves while explore loop.done == false accumulate done from false accumulate history from [] accumulate files from [] accumulate answer from "" accumulate count from 0 run think_act
// Runs once, after the loop finishes; loop.* holds the final accumulator values compute finish {answer: loop.answer, files_read: loop.files, iterations: loop.count}
flows flow think_act // REASON: the LLM sees the question, the history, and the available actions ask reason, using: "anthropic:claude-sonnet-4" with role "You are a code exploration agent. Available actions: glob (find files by pattern), read_file (read a file), respond (give your final answer). Be systematic: find relevant files, read them, then answer." with task "Question: ${input.question}\nActions so far: ${loop.history}\nIteration ${loop.count} of ${input.max_iterations}.\n\nWhat should you do next?" returns action as string, is required action_input as map thought as string response as string
// ACT: route the chosen action to the right tool machine match steps.reason.action when "glob" ask tool_glob, from: "@mashin/actions/tools/glob" pattern: steps.reason.action_input.pattern when "read_file" ask tool_read, from: "@mashin/actions/tools/read_file" path: steps.reason.action_input.path otherwise compute no_tool {skipped: true}
// OBSERVE: update the loop's working memory; the output keys update the // accumulators of the same name, and `done` decides whether to loop again compute observe let acted = steps.reason.action != "respond" let hit_limit = loop.count + 1 >= input.max_iterations { done: !acted || hit_limit, answer: acted ? loop.answer : steps.reason.response, history: loop.history.concat([{action: steps.reason.action, thought: steps.reason.thought}]), files: steps.reason.action == "read_file" ? loop.files.concat([steps.reason.action_input.path]) : loop.files, count: loop.count + 1 }
verifies test "responds directly when the model already knows" assuming reason {action: "respond", action_input: {}, thought: "I can answer this", response: "The parser lives in lang/parser3."} given {question: "Where does the parser live?"} expect {answer: "The parser lives in lang/parser3.", iterations: 1, files_read: []}Phase-by-Phase Breakdown
Section titled “Phase-by-Phase Breakdown”Reason
Section titled “Reason”The ask step is the brain. It sees the question, what it has already done (loop.history), and what actions are available. returns constrains the response to a structured shape, so the dispatch step can rely on it.
match routes to the right tool machine based on the chosen action. Each tool is an ask ... from: step invoking a stdlib machine. The otherwise arm handles the “respond” case (no tool needed).
Observe
Section titled “Observe”A pure compute step builds the next round’s working memory. Its output keys (done, history, files, answer, count) update the accumulators of the same names. Appending both the action and the thought gives the LLM its full reasoning trace next round.
Loop or finish
Section titled “Loop or finish”The while condition (loop.done == false) re-checks after every iteration. When the agent chooses “respond”, or the iteration limit is hit, done becomes true and the loop exits. The step after the loop is a sibling: it runs once, reading the final accumulator values via loop.*.
One grammar rule to remember: an inline loop body is exactly one element (plus its accumulate seeds). Writing several steps under a loop does not nest them — they become siblings that run after the loop. For a multi-step body, put the steps in a named flow and run it, as think_act does here.
Key Design Patterns
Section titled “Key Design Patterns”Accumulators as working memory: accumulate history from [] seeds a value before the first iteration; the body’s output updates it each round; loop.history reads it. The entire reasoning trace accumulates without any mutable state outside the loop.
Bounded by construction: the done flag folds the iteration limit into the loop condition itself. A confused LLM cannot loop forever; the resource budget on every run is a second, independent backstop.
Tool steps stay governed: every arm of the dispatch is an ordinary ask ... from: step, so each tool call passes through permissions and lands in the behavioral ledger like any other action.
Key Syntax
Section titled “Key Syntax”// ReAct agent structure: the loop runs a named flow each iterationbehaves while explore loop.done == false accumulate done from false // Seeded before the first iteration accumulate history from [] run think_act // Inline bodies are ONE element; multi-step bodies are a flow
compute finish // Sibling: runs once after the loop {answer: loop.answer} // Final accumulator values via loop.*
flows flow think_act ask reason, using: "anthropic:claude-sonnet-4" with task "...${loop.history}..." // Read accumulators via loop.* returns action as string, is required
match steps.reason.action // Tool dispatch when "tool_name" ask tool_run, from: "@mashin/actions/tools/tool_name" arg: steps.reason.action_input.arg otherwise compute no_tool {skipped: true}
compute observe // Output keys update the accumulators {done: steps.reason.action == "respond", history: loop.history.concat([steps.reason.action])}Common Mistakes
Section titled “Common Mistakes”-
Not tracking action history. Without the history accumulator in the prompt, the LLM doesn’t know what it’s already tried. It will repeat the same actions. Always pass
${loop.history}in the task. -
Missing iteration limits. Fold the limit into the loop’s
doneflag (loop.count + 1 >= input.max_iterations). The runtime’s resource budget will stop a runaway loop eventually, but your agent should stop itself first, with a graceful answer. -
A condition that never changes. The
whilecondition must read something the body updates (an accumulator). A condition over a pre-loop step’s output never changes, so the loop would spin until the resource cap kills it.
What’s Next
Section titled “What’s Next”In Module 07, you’ll learn how to compose multiple machines together — building specialist agents that a coordinator orchestrates.