Original Korean article ReAct planning source page (Korean) This English page is published as a child page of the English PART 1 page to preserve the original series structure. |
If PART 1 used the Akka actor tree to establish order among AI terminals, this part explains how to turn a general LLM into a working agent on top of that runtime. Date: 2026-04-14. Audience: .NET and server developers who have read PART 1.
![DevBegin > How Do You Teach an LLM to Wait? - An Introduction to ReAct Actor Planning [PART 2] > 2026-04-14-react-part2-hero-waiting-room.png](/download/attachments/125731621/2026-04-14-react-part2-hero-waiting-room.png?version=1&modificationDate=1776167880198&api=v2)
In PART 1, we built the control room. StageActor, WorkspaceActor, TerminalActor, and AgentBotActor each got their own place, and multiple AI terminals stopped crashing into each other.
But one thing was still missing. The moderator LLM could talk well, yet it could not wait.
When it sent a command and the result came back late, it grew impatient, called the same tool again, re-checked work that was still in progress, and once the loop had ended it could no longer receive the eventual completion signal. In other words, it had become a clever chatterbox, but not a working agent.
What this part covers
|
![DevBegin > How Do You Teach an LLM to Wait? - An Introduction to ReAct Actor Planning [PART 2] > 2026-04-14-react-part2-sync-loop-trap.png](/download/attachments/125731621/2026-04-14-react-part2-sync-loop-trap.png?version=1&modificationDate=1776167880882&api=v2)
The old RunFunctionCallLoopAsync looks plausible at first glance.
User input -> call the LLM -> execute tool calls -> feed results back to the LLM -> return when there is nothing left to do |
The problem is that the door closes inside that loop. Imagine the moderator sending npm test to a terminal.
Action: term_send("npm test")
Observation: "Sent to term-0" |
At that point it looks like success, but the real test run is still happening. The result may arrive 20 seconds later. During that gap, the loop only has two options.
Choice | Why it is a problem |
|---|---|
Polling | You keep rereading the same state and waste context |
Exit | You lose the path that would receive the later completion signal |
The synchronous loop works in a world where answers come back immediately. It struggles in the real tool world where the answer is often, "I will come back later with the result."
![DevBegin > How Do You Teach an LLM to Wait? - An Introduction to ReAct Actor Planning [PART 2] > 2026-04-14-react-part2-react-cycle.png](/download/attachments/125731621/2026-04-14-react-part2-react-cycle.png?version=1&modificationDate=1776167881552&api=v2)
ReAct stands for Reasoning + Acting, but it is easier to understand as a scene before it is a paper term. Picture the control room from Inside Out.
That is the basic rhythm of ReAct.
Thought -> Action -> Observation -> Thought -> ... |
In AgentZero terms it becomes even simpler.
Think -> call a tool -> decide whether the result is immediate or delayed -> wait or think again -> report back to the user when the work is done |
The original ReAct paper mostly focuses on scenes where observations come back immediately. Real applications are different. Tests can run for seconds or minutes. Another AI terminal may finish at an unpredictable time. File operations and external calls are also asynchronous. That is why AgentZero added a realistic intermediate state called Waiting to the original ReAct pattern.
Become() Turns a Loop into a State Machine![DevBegin > How Do You Teach an LLM to Wait? - An Introduction to ReAct Actor Planning [PART 2] > 2026-04-14-react-part2-state-machine.png](/download/attachments/125731621/2026-04-14-react-part2-state-machine.png?version=1&modificationDate=1776167882104&api=v2)
This is where Akka Become() starts to shine. One actor can swap its handler set depending on the current state.
State | What it does |
|---|---|
| Decides what should happen next |
| Executes a tool action |
| Waits for an asynchronous result |
| Produces the final response |
The flow is straightforward.
StartReAct -> Thinking -> Acting -> Waiting -> Thinking -> Complete |
That distinction matters because a normal loop dies once it hits return, but an actor survives in the Waiting state. When an external signal arrives, it can wake back up into Thinking. We did not make the LLM itself "smarter." We wrapped it in an execution environment that knows how to wait.
![DevBegin > How Do You Teach an LLM to Wait? - An Introduction to ReAct Actor Planning [PART 2] > 2026-04-14-react-part2-signal-awakening.png](/download/attachments/125731621/2026-04-14-react-part2-signal-awakening.png?version=1&modificationDate=1776167882615&api=v2)
term_send("npm test")
-> "sent"
-> term_read
-> term_read
-> term_read
-> ...
-> consume 10 rounds
-> exit |
term_send("npm test")
-> "sent"
-> Become(Waiting)
-> receive completion signal
-> Become(Thinking)
-> "Now read the result"
-> term_read
-> Complete |
The difference becomes obvious in a side-by-side table.
Scene | Synchronous loop | ReActActor |
|---|---|---|
Right after sending a command | It keeps checking again | It transitions into waiting |
Asynchronous completion | The receive path is weak | It arrives through |
Context usage | Grows because of polling | Only thinks again when necessary |
Failure recovery | The loop dies when it ends | Can re-evaluate after a timeout |
Waiting is not just a pause. It is the mechanism that lets the agent choose order over impatience.
A state machine only works when the message contract is explicit. AgentZero defined separate messages for its ReAct runtime.
Message | Role |
|---|---|
| Starts a session |
| Sends intermediate progress to the UI |
| Delivers an external completion signal |
| Lets the user cancel |
The actor-tree placement also mattered.
ActorSystem("AgentZero")
\-- /user/stage
|-- /bot
| \-- /react <- ReActActor
\-- /ws-{name}
\-- /term-{id} |
Why place ReActActor under bot? Because it stays close to bot session memory, completion signals can be forwarded to it naturally, and the UI can bind to its progress cleanly. If the names and positions of messages become confusing, the agent loses its way very quickly.
![DevBegin > How Do You Teach an LLM to Wait? - An Introduction to ReAct Actor Planning [PART 2] > 2026-04-14-react-part2-memory-logging.png](/download/attachments/125731621/2026-04-14-react-part2-memory-logging.png?version=1&modificationDate=1776167883263&api=v2)
The ReAct state machine alone did not solve everything. On-device LLMs especially tended to forget where the work currently stood. That is why two supporting devices were added.
Current state: Waiting
Completed work: meeting_create, term_send("npm test")
Pending item: test result from term-0
Expected next step: read the result and report it to the user |
This memory is generated from actor state and prepended before each LLM call. In effect, the agent opens its own notebook every turn before deciding what to do next.
Log tag | Meaning |
|---|---|
| What judgment was made |
| Which tool was executed |
| When waiting started and what was received |
| Which state transition occurred |
The pairing matters because memory keeps the agent from forgetting its own work, while logs let the developer trace the agent's mistakes. One is memory for the agent. The other is memory for the human.
There were many frameworks in 2025 and 2026: LangGraph, OpenAI Agents SDK, AutoGen, CrewAI, Semantic Kernel, and more. So why did AgentZero still hold on to the actor model?
Viewpoint | Typical agent framework | Akka actors |
|---|---|---|
State | Often hidden in dicts, history objects, or runner internals | State is explicit through |
Failure recovery | Usually centered on manual retry logic | Supervision is built in |
Concurrency | Depends heavily on how well async code was designed | Actor boundaries isolate concurrent work naturally |
Distributed scaling | Often needs extra design | Connects naturally to the Akka family |
Timeouts and waiting | Must be implemented per use case | Core tools like |
Put simply, a general framework may help you start an agent quickly, but Akka gives you a stronger shape for an agent that has to keep living. When many terminals move at once, external completion signals arrive unpredictably, and both recovery and state observation matter, that difference becomes large.
![DevBegin > How Do You Teach an LLM to Wait? - An Introduction to ReAct Actor Planning [PART 2] > 2026-04-14-react-part2-safety-harness.png](/download/attachments/125731621/2026-04-14-react-part2-safety-harness.png?version=1&modificationDate=1776167883738&api=v2)
Saying an agent can move on its own also means it can run out of control on its own. That is why the runtime was built with safety devices from the start.
Device | Meaning | Value |
|---|---|---|
| Prevents infinite loops | 10 |
| Forces a re-evaluation after waiting too long | 30 seconds |
Repeated-call cap | Blocks the same tool from being spammed | 3 times |
| Prevents a single round from exploding | 30 |
| Immediate user stop | Immediate |
The goal is not to remove autonomy. It is to retain control so that when autonomy leaves the track, the system can bring it back. As PART 3 will show, these safety devices later grow into more concrete forms such as the DONE handshake, _pendingDone, ESC control, and UI cards.
This part was not about flashy implementation. It was about why that implementation had to exist in the first place.
Become() holds that flow as an explicit state machineThe next question follows naturally.
Once that design entered real code, where did it break and what else had to be added?
That is where PART 3 begins. It examines how ReActActor was implemented in code, which protocol reconnected it to terminal AIs, why the DONE(...) handshake became necessary, and why devices such as the card UI, ESC control, and queueing had to be added.
NEXT - PART 3 If PART 2 was the design episode that teaches the moderator how to wait, PART 3 is the implementation episode that shows what happened when that moderator finally stepped onto the real stage. -> How Does ReAct Come Alive in Real Code? - AgentZero Implementation Deep Dive [PART 3] |