How Claude thinks, acts, and repeats · Files: Agents/ReactAgent.cs, ParallelReactAgent.cs, PlanningAgent.cs
By itself, Claude just turns text into more text (Guide 01). An agent is a loop around that. It calls Claude, reads what Claude says, optionally calls a tool via the MCP server, feeds the result back to Claude, and repeats — until Claude decides it has a final answer.
while loop with an LLM inside. Each iteration: send all context to Claude, get a reply, parse it, invoke any tool it asked for via IMcpServerService.InvokeAsync(), add the result to context, then loop again. The loop exits when Claude's reply contains <final_answer> or when MaxIterations is reached.
This is the ReAct pattern: Reasoning + Acting. The model reasons about what to do, acts by calling a tool, then reasons again based on the result.
McpServerService — a dictionary of IMcpTool objects running in-process. In this MCP Version, ReactAgent calls the same IMcpServerService interface, but the implementation is RealMcpClientService — which spawns NexaFlowDemo.McpServer as a subprocess and communicates via JSON-RPC 2.0 over stdio. The agent loop code is identical; only the tool dispatch layer changed.
| Iteration | Input sent to Claude | Claude's reply | Code's response |
|---|---|---|---|
| 1 | System prompt + history: [user] What is the stock level for NF-SKU-001 and who supplies it? |
<thinking>I need inventory first.</thinking> |
Finds <tool_call> → calls RealMcpClientService.InvokeAsync("inventory_lookup", ...) → MCP server subprocess responds → gets result. Adds to history. continue. |
| 2 | Same system prompt + history now has 3 entries | <thinking>Stock is fine. Now supplier.</thinking> |
Finds <tool_call> → calls MCP server again → gets supplier data. Adds to history. continue. |
| 3 | Everything above + 2 more entries. History now has 5 entries. | <thinking>I have both answers.</thinking> |
Finds <final_answer> → yield return new AgentStep { Type = FinalAnswer, ... } → yield break. Loop ends. |
for (int i = 0; i < _options.MaxIterations; i++)
{
// 1. Build prompt from system instructions + full conversation history
var prompt = BuildPrompt(request, history);
// 2. Call Claude — get back a string
string reply = await _llm.CompleteAsync(prompt, ct);
history.Add(new AgentMessage { Role = "assistant", Content = reply });
// 3. Did Claude say it has a final answer?
var finalAnswer = ExtractTag(reply, "final_answer");
if (!string.IsNullOrWhiteSpace(finalAnswer))
{
yield return new AgentStep { Type = AgentStepType.FinalAnswer, Content = finalAnswer };
yield break;
}
// 4. Did Claude ask to call a tool?
var toolCallXml = ExtractTag(reply, "tool_call");
if (!string.IsNullOrWhiteSpace(toolCallXml))
{
var toolName = ExtractTag(toolCallXml, "name");
// _mcp is IMcpServerService — in this project: RealMcpClientService
var toolResult = await _mcp.InvokeAsync(toolName, ParseToolInput(inputJson));
history.Add(new AgentMessage
{
Role = "tool",
Content = $"<tool_result><name>{toolName}</name><output>{resultText}</output></tool_result>"
});
continue;
}
}
RunAsync returns IAsyncEnumerable<AgentStep>. This lets the SignalR hub push each step to the browser as it arrives — so users see "Thinking...", "Calling inventory_lookup", "Got result: 250 units", "Final answer" appearing live, not all at once after 10 seconds.
IEnumerable<T> with yield return, but async. Each yield return pushes one step to the caller immediately, without waiting for the rest of the loop to finish.
When Claude needs two or more independent pieces of information, ParallelReactAgent lets it request multiple tools in a single <tool_calls> (plural) block, then runs them all concurrently with Task.WhenAll.
var tasks = calls.Select(c => _mcp.InvokeAsync(c.Name, c.Input)).ToList();
var results = await Task.WhenAll(tasks); // all MCP tool calls run in parallel
If each tool call takes 200ms, two sequential calls cost 400ms. With Task.WhenAll, the cost is still ~200ms.
PlanningAgent forces Claude to produce a full plan first, then execute each step in order. It works in three phases:
| Phase | What Happens | LLM calls |
|---|---|---|
| 1 — Plan | Ask Claude to break the task into 2–5 numbered steps in <plan><step>…</step></plan> | 1 |
| 2 — Execute | For each step, ask Claude what tool to call. Run the tool via MCP. Collect results. | 1 per step |
| 3 — Synthesize | Ask Claude to combine all step results into a final coherent answer. | 1 |
From the web layer's perspective, there is only ever one thing to call: IMultiAgentOrchestrator.RunAsync(request). The orchestrator is a router: one entry point, multiple strategies behind it. It picks ReactAgent, ParallelReactAgent, or PlanningAgent based on AgentRunRequest.AgentType.
public class AgentStep
{
public AgentStepType Type { get; set; } // Thinking | ToolCall | ToolResult | FinalAnswer | Error
public string Content { get; set; }
public string? ToolName { get; set; }
public string? ToolInput { get; set; }
public string? ToolOutput { get; set; }
public bool IsError { get; set; }
}
Task.WhenAll. Faster for independent lookups.IAsyncEnumerable<AgentStep> streams progress to the UI in real time._mcp.InvokeAsync() in this project routes to RealMcpClientService → McpServer subprocess → real MCP tool method.