Original Korean article

ReAct implementation source page (Korean)

This English page mirrors the original series under the English PART 1 page.

If PART 2 explained ReAct actor planning, PART 3 shows how that design closes into real code, a real UI, and a real protocol for collaborating with terminal AIs. Date: 2026-04-14. Audience: developers who already read PART 1 and PART 2.

0. Introduction

The conclusion of PART 2 was clear: a general LLM cannot wait on its own. That is why AgentZero needed a ReActActor that wraps Thinking -> Acting -> Waiting -> Complete inside Akka Become().

PART 3 is the next scene. It does not stop at the design document. It shows how that design was wired into the real AgentZero codebase.

AgentBotWindow -> AgentBotActor -> ReActActor -> StageActor -> TerminalActor
                                             -> AI CLI -> bot-chat DONE -> MainWindow
                                             -> StageActor -> AgentBotActor -> ReActActor

What this part covers

  • ReAct was implemented not as an abstract pattern, but as a concrete state machine
  • The broken link to terminal AIs was bridged through the /agent-zero + DONE(...) handshake
  • Real-world issues such as runaway tool use, dropped signals, and endless waiting became controllable through guards and UI feedback

1. A Bird's-Eye View of the Implementation

The full implementation closes across five axes inside Project/AgentZeroWpf.

Axis

Concrete files

Responsibility

State machine

Actors/ReActActor.cs

Transitions between Thinking / Acting / Waiting / Complete

Message contract

Actors/Messages.cs

Defines StartReAct, ReActProgress, TerminalDoneSignal, and more

Broker layer

Actors/AgentBotActor.cs, Actors/StageActor.cs

Forwards traffic between the UI, ReActActor, and terminal messages

UI layer

UI/APP/AgentBotWindow.xaml.cs

Owns RunReActAsync, ESC control, and card rendering

External return channel

UI/APP/MainWindow.xaml.cs, bot-chat.ps1

Acts as the door through which terminal AIs come back via DONE(...)

Once you see that structure, the PART 1 question, "How does Akka talk to AI-agent CLIs?" stops being abstract and becomes a concrete code path.

2. Demon Slayer's Form Shift - ReActActor Turns the Loop into a State Machine

The biggest change was separating the old synchronous RunFunctionCallLoopAsync into its own ReActActor.

Idle
  -> StartReAct
  -> Thinking
  -> Acting
  -> Waiting
  -> Thinking ...
  -> Complete
  -> Idle

The concrete message contract is explicit too.

  • StartReAct
  • ReActProgress
  • CompletionSignal
  • SkipWaiting
  • CancelReAct
  • TerminalDoneSignal
  • ReActResult

The reason this matters is simple. A loop dies when it returns. An actor does not. Once it enters Waiting, an external signal can wake it back into Thinking. The idea from PART 2, teaching an LLM to wait, becomes working code here.

Two supporting foundations that were added earlier remain important as well.

  • AiMode-DiagnosticLogging.md - the [AI-REQ], [AI-FnCall], [AI-TOOL], and [AI-RESP] logging system
  • OnDevice-SessionMemory.md - actor-side memory that stores recent work and injects it into the first-round System message

ReAct therefore entered the runtime not as a standalone trick, but as an execution engine built on top of logs and memory.

3. Phase 2, Phase 3 - When the Design Lands on Real Buttons

Seen in implementation order, the first turning point came from two completion reports.

3.1 ReAct-Phase2-Complete.md

What was actually finished there?

  • Created Actors/ReActActor.cs
  • Extended Actors/Messages.cs
  • Spawned ReActActor as a child inside Actors/AgentBotActor.cs
  • Added state-transition tests in Project/AgentTest/Actors/ReActActorTests.cs

That means the first real milestone was not "a design." It was a testable actor.

3.2 ReAct-Phase3-Complete.md

The next step was UI integration.

  • Added a ReAct checkbox to AgentBotWindow.xaml
  • Added RunReActAsync() to AgentBotWindow.xaml.cs
  • Branched from the old FnCall loop into the ReAct path inside StreamAiResponseAsync()
  • Connected ReActProgress and ReActResult directly to the UI through SetReActCallbacks

From this point onward, ReAct stopped being a back-end experiment and became a mode that users could actually click and use.

4. Your Name + Howl's Door - How the Terminal AI Link Was Rebuilt

This is where PART 3 becomes concrete. Even if ReActActor is well designed, collaboration does not close if the AI running inside a terminal, such as Claude Code, cannot send a result back.

4.1 Routing only works if the other side knows its name

ReAct-YourName-Identity-Protocol.md exposed a simple but fatal problem.

  • AgentZero only said, "I am AgentZero."
  • The peer terminal did not know whether its own identity was Claude1 or Claude2.
  • As a result, it replied with something like DONE(Claude, ...), using the model family name instead of the routing key.

That forced the handshake text to change.

/agent-zero Hello, I am AgentZero.
Your terminal tab name is 'Claude1'.
After you respond, you must run:
bot-chat.ps1 "DONE(Claude1, summary of your response)" 

That one line makes the first argument of DONE the routing key instead of the model label.

4.2 /agent-zero becomes a skill loader

ReAct-SkillActivation-Prompt.md explains the next step. Terminal AIs do not know what bot-chat.ps1 is by default. So every conversation is prefixed with /agent-zero to force-load the skill.

/agent-zero Please review this code

That way the terminal AI reads .claude/skills/agent-zero/SKILL.md, learns how bot-chat.ps1 works, and sends a DONE(...) message at the end. In more traditional systems, SKILL.md is the document-shaped IDL and /agent-zero is the switch that activates the contract.

4.3 Where does DONE return?

Terminal AI
  -> bot-chat.ps1 "DONE(Claude1, reviewed 3 items)"
  -> CliHandler
  -> MainWindow.HandleBotChat()
  -> TerminalDoneSignal
  -> StageActor
  -> AgentBotActor
  -> ReActActor

The actor tree introduced in PART 1 now gains an actual return corridor that reconnects work happening outside the terminal process back into the runtime.

5. Kekkaishi's Barrier - How Tool Runaway Was Contained

Another class of problem emerged immediately in practice. The smarter the model looked, the more tools it tried to call in one burst.

ReAct-ToolCall-Constraint-Prompting.md summarizes the failures clearly.

  • Index explosions like term_read(tab_index=0..78)
  • Repeated term_read calls
  • Unrequested over-expansion such as meeting_create

The implementation uses three defensive layers to stop that behavior.

  1. Prompt constraints such as "maximum 5 tool calls per response," "only one stage_send per response," and "only one term_read per response"
  2. Code-side blocking through response.ToolCalls.Take(5)
  3. Repeated-call protection that returns an error after the same (function + arguments) combination appears more than three times

This matters because the quality of an agent system is not determined by model intelligence alone. It depends just as much on whether the runtime draws a strong operational boundary around the tools it exposes.

6. Jujutsu Kaisen's Domain Release - Stopping Well Is Harder Than Running Well

ReAct-SequenceControl-ESC.md contains one of the deepest lessons from the late implementation phase: it is harder to stop a system well than to make it run.

The concrete problem looked like this.

  • DONE may arrive five seconds late after Waiting has already ended
  • That late signal may be dropped if the actor is currently in Thinking

Two devices solved it.

6.1 _pendingDone queueing

If a TerminalDoneSignal arrives while the actor is in Thinking or Acting, it is stored in _pendingDone instead of being discarded. When the actor enters Waiting again, the queued signal is consumed immediately and drives the actor back into Thinking. Late DONE messages no longer evaporate into empty air.

6.2 ESC control

The user also needs a way to control the loop directly.

  • Press ESC once: SkipWaiting
  • Press ESC twice: CancelReAct

That makes ReAct more than "an engine that usually runs by itself." It becomes an engine whose control can be reclaimed by the user.

7. Yu-Gi-Oh!'s Card Field - Making State Visible to the Eye

An implementation is not complete if only the developer can understand it. Users need to see where the runtime is currently stuck or moving.

ReAct-UI-CardSystem.md is the result of that requirement.

  • Gold card: Thinking
  • Purple card: tool call
  • Blue card: tool result
  • Gold waiting card: Waiting
  • Green card: final Complete

The internal state of ReAct is no longer hidden logic. It is laid out on the field like visible cards. A user can tell immediately whether the runtime is currently reasoning, executing a tool, waiting for completion, or finishing the response.

The combination of RunReActAsync() and HandleReActProgress() is central here. Because the ReActProgress messages emitted by the actor are rendered directly into UI cards, ReAct stops being a system understood only through logs and becomes something you can watch in real time.

8. ActorPlanning Became a Compression of Implementation, Not Just a Design Note

An interesting detail is that ReAct-ActorPlanning.md looks like an early design document, but it was actually written after several implementation problems had already been fought through. That changes its role. It is no longer a pure plan. It is a compressed architecture sheet built from the implementation that already exists.

In short:

  • PART 2 explained why ReActActor was needed
  • The improvement documents from PART 3 recorded where the real implementation broke
  • ReAct-ActorPlanning.md reorganized that journey one more time from the point of view of a state machine

That means this implementation did not descend top-down in one clean stroke. It was refined through failure logs -> patches -> protocol definition -> UI visualization.

9. One Real Turn in Summary - This Is How ReAct Now Runs

User
  -> AgentBotWindow.RunReActAsync
  -> AgentBotActor.StartReAct
  -> ReActActor.Thinking
  -> stage_status / stage_send("/agent-zero ...")
  -> Waiting
  -> Claude1 runs bot-chat.ps1 "DONE(Claude1, reviewed 3 items)"
  -> MainWindow.HandleBotChat
  -> TerminalDoneSignal
  -> ReActActor.Thinking
  -> final summary response + card UI rendering

Yes, a general LLM can be turned into a working agent. But doing so requires more than changing the model. You have to design the actor state machine, session memory, logs, skill activation, the DONE protocol, and the UI visibility together.

10. Closing

PART 1 showed the actor tree and the communication structure. PART 2 explained why the ReAct state machine was necessary. PART 3 shows how that design actually comes alive inside the codebase.

The core idea is not one glamorous algorithm.

  • Make the system capable of waiting through ReActActor
  • Reconnect other AIs through /agent-zero and DONE(...)
  • Keep the whole runtime manageable through constraints, queueing, ESC control, and card-based UI feedback

In the end, AgentZero's ReAct implementation is not a story about "the LLM becoming smart on its own." It is a story about how to design a runtime in which an LLM can actually work. In that sense, the Akka actor model acted not merely as a concurrency framework, but as the skeleton that organized the agent runtime itself.


Reference Documents

  • Tech/DOC/Actor/improvement/ReAct-Phase2-Complete.md
  • Tech/DOC/Actor/improvement/ReAct-Phase3-Complete.md
  • Tech/DOC/Actor/improvement/ReAct-YourName-Identity-Protocol.md
  • Tech/DOC/Actor/improvement/ReAct-ToolCall-Constraint-Prompting.md
  • Tech/DOC/Actor/improvement/ReAct-SequenceControl-ESC.md
  • Tech/DOC/Actor/improvement/ReAct-ActorPlanning.md
  • Tech/DOC/Actor/improvement/ReAct-DONE-Handshake-Protocol.md
  • Tech/DOC/Actor/improvement/ReAct-SkillActivation-Prompt.md
  • Tech/DOC/Actor/improvement/ReAct-UI-CardSystem.md
  • Tech/DOC/Actor/improvement/AiMode-DiagnosticLogging.md
  • Tech/DOC/Actor/improvement/OnDevice-SessionMemory.md
  • No labels