Refactoring an Agent Runtime from PCA to ReAct

Agent ArchitectureReActPlanningDAGSoftware Architecture

I recently refactored the execution architecture of a general-purpose agent system from a mostly static Planning -> Compiling -> Action model to a ReAct-oriented action model. On the surface, it looks like removing one Planner and one Compiler layer. In practice, it changes how the system handles uncertainty.

For a specialized workflow with a well-defined process, Planning-Compiling-Action feels natural: decompose the user goal into steps, compile those steps into a DAG, then let the runtime execute it. It behaves like a traditional workflow engine: structured, auditable, and easy to verify.

General-purpose agents have a different problem. User goals are often incomplete, tool outputs are unstable, and pages, files, databases, and model responses can all change the next best action. The earlier a static plan is frozen, the more likely it is to become stale during execution.

This article documents the design tradeoffs behind the refactor. The code examples come from a real system’s core architecture, but they are generalized and do not include business-specific implementation details.

How the Two Modes Split Work

I refer to the old architecture as Planning-Compiling-Action, or PCA.

flowchart LR
  U[User Goal] --> P[Planning]
  P --> C[Compiling]
  C --> D[DAG]
  D --> A[Action Runtime]
  A --> R[Final Delivery]

PCA assumes that the system can produce a reasonably complete, stable, and compilable plan before execution.

ReAct makes the opposite assumption: the plan should not be fixed all at once. The agent moves through thought, action, and observation, then revises the next action based on what it observes.

flowchart LR
  U[User Goal] --> T[Thought]
  T --> A[Action]
  A --> O[Observation]
  O --> T
  O --> F[Final Answer]

In production, both approaches may still produce DAG nodes or queue tasks. The difference is not the executor. The difference is when the DAG is generated, and what information it is based on.

DimensionPlanning-Compiling-ActionReAct
Core abstractionPlan full steps first, then compile an execution graphSelect the next batch of actions from current context
Best fitStable, templateable, well-defined workflowsOpen-ended, exploratory, multi-turn, uncertain tasks
Failure modeEarly planning errors propagate downstreamEach round can correct itself from observations
AuditabilityStrong, with explicit steps and DAG boundariesRequires explicit thought/action/observation traces
Engineering complexityPlanner, Compiler, Validator layering can grow heavyOne-round decisions are simpler, but state management becomes critical
User experienceFeels like submitting a workflow and waitingFeels like an assistant progressing with intermediate evidence

My current split is this: PCA is closer to intelligent workflow orchestration, while ReAct is closer to a scheduling loop inside an agent runtime. PCA fits predictable work. ReAct fits general-purpose tasks.

Old Architecture: Plan, Then Compile

The first layer in the old architecture was the Planner. It did not execute tools. Its job was to decompose the user goal into semantic steps.

A simplified Planner output looked like this:

export type PlannerStep = {
  title: string;
  action?: string;
  contentSpec?: {
    totalWords: number;
    sections?: Array<{ name: string; words: number }>;
  };
};

export async function planSteps(input: string, contextText: string) {
  const response = await chatComplete({
    temperature: 0.2,
    messages: [
      { role: "system", content: buildPlannerSystemPrompt() },
      { role: "user", content: `Context:\n${contextText}\n\nUser request:\n${input}` },
    ],
  });

  const parsed = plannerSchema.safeParse(extractJsonObject(response.text));
  return parsed.success ? parsed.data.steps : fallbackSteps(input);
}

The benefit is clear semantic separation. The Planner focuses on what should be done, without worrying about which tool each step maps to, which arguments are required, or how dependencies should be wired.

The second layer was the Compiler. It mapped PlannerStep objects into executable nodes.

export type DagNode = {
  stepId: string;
  title: string;
  cellId: string;
  args: Record<string, unknown>;
  dependsOn: string[];
};

export async function compileStepsToDag(opts: {
  input: string;
  steps: PlannerStep[];
  catalog: CellCatalog;
  providedInputs?: Record<string, unknown>;
}) {
  const allowedCellIds = new Set(opts.catalog.cells.map((cell) => cell.cellId));

  const response = await chatComplete({
    temperature: 0.2,
    messages: [
      { role: "system", content: buildCompilerSystemPrompt(opts.catalog) },
      {
        role: "user",
        content:
          `User request:\n${opts.input}\n\n` +
          `planner steps:\n${formatSteps(opts.steps)}\n\n` +
          "Choose a cellId, args, and dependsOn list for each step.",
      },
    ],
  });

  const parsed = dagSchema.safeParse(extractJsonObject(response.text));
  if (!parsed.success) return needInputOrFallback();

  return parsed.data.nodes
    .filter((node) => allowedCellIds.has(node.cellId))
    .map((node) => ({
      ...node,
      args: validateAndFixArgs(node.cellId, node.args, opts.catalog),
    }));
}

This architecture works well for specialized agents. Fixed data processing, report generation, approval assistance, document pipelines, and similar workflows can all be planned, compiled, and executed. Each layer has a clear responsibility:

LayerResponsibility
PlannerDecompose the goal into human-readable steps
CompilerMap steps to tools, arguments, and dependencies
ValidatorCheck arguments, dependencies, and data flow
RuntimeExecute the DAG and collect results
FinalizerSummarize results and deliver the final answer

The downside also comes from this layering. Once a user adds a requirement mid-run, or a tool returns an unexpected result, the system needs to re-plan, re-compile, cancel old tasks, migrate context, and handle DAG versions. The architecture gets heavy.

Where PCA Gets Complicated

In PCA, the Compiler is usually where complexity accumulates.

At first, the Compiler only maps steps to tools. Over time, it tends to absorb more and more fallback behavior:

NeedCompiler patch
Planner misses word count or sectionsAuto-fill contentSpec
Model chooses the wrong toolFix obvious cellId mismatches
Argument names differMap query/prompt/task/description to each other
Attachment tasksInsert attachment extraction nodes
Long document generationSplit into section-level nodes
Chart and report order is wrongPost-process DAG dependencies
Data flow is incompleteValidate dependencies before execution

Each fix is reasonable on its own. Taken together, the Compiler stops being just a compiler. It becomes part Planner, part policy engine, and part error recovery system.

The old startup chain was also long:

const outcome = await specifyOutcome({ input, contextText });

const planned = await planSteps({
  input,
  contextText,
  traceTag: "agent.planner",
});

const compiled = await compileStepsToDag({
  input,
  contextText,
  steps: planned.steps,
  catalog,
  providedInputs,
  targetOutcome: outcome,
  traceTag: "agent.compiler",
});

await rebuildTasksForDag({
  runId,
  dagVersion: 1,
  nodes: compiled.nodes,
});

The issue is not that this cannot work. The issue is that it moves too many decisions to the beginning. Before observing any real tool output, the system tries to lock down target outcome, execution steps, tool choices, arguments, dependencies, and delivery structure.

For predictable work, this is efficient. For a general-purpose agent, it is too early.

ReAct Generates Action DAGs

The change is that planning and compiling are no longer two large stages. The model emits a set of executable actions under the current context.

The simplified schema looks like this:

const actionSchema = z.object({
  thoughtSummary: z.string().min(1),
  actions: z.array(
    z.object({
      title: z.string().min(1),
      tool: z.string().min(1),
      args: z.record(z.any()).default({}),
      dependsOn: z.array(z.string()).default([]),
    }),
  ).min(1).max(8),
  finalInstruction: z.string().optional(),
});

The builder no longer returns PlannerStep objects. It returns executable nodes and ReAct trace events.

export async function buildReactDag(opts: {
  input: string;
  contextText?: string;
  catalog: CellCatalog;
  incremental?: boolean;
}) {
  const allowed = new Set(opts.catalog.cells.map((cell) => cell.cellId));

  let actions = buildHeuristicActions(opts.input, opts.catalog);
  let thoughtSummary = "Choose the necessary tools to gather evidence, then deliver the answer.";

  const response = await chatComplete({
    temperature: 0.1,
    messages: [
      { role: "system", content: buildReactPrompt(opts.catalog, opts.incremental) },
      {
        role: "user",
        content:
          `${opts.contextText ? `Context:\n${opts.contextText}\n\n` : ""}` +
          `User request:\n${opts.input}`,
      },
    ],
  });

  const parsed = actionSchema.safeParse(extractJsonObject(response.text));
  if (parsed.success) {
    const validActions = parsed.data.actions.filter((action) =>
      allowed.has(action.tool),
    );
    if (validActions.length > 0) {
      thoughtSummary = parsed.data.thoughtSummary;
      actions = validActions;
    }
  }

  actions = ensureFinalSynthesisAction(actions, opts.input, opts.catalog);
  actions = normalizeActionDependencies(actions);

  return {
    nodes: actions.map(toDagNode),
    traceEvents: buildReactTraceEvents(thoughtSummary, actions),
  };
}

Several constraints matter here.

First, the LLM emits actions, not abstract steps. tool must come from the catalog, args must be executable, and dependsOn must reference known nodes. The Planner and Compiler boundary is collapsed into action selection.

Second, the system keeps heuristic fallbacks. If the model fails, JSON parsing fails, or a tool does not exist in the catalog, the system falls back to conservative actions instead of crashing.

Third, the system always ensures a final synthesis action. General-purpose agents can complete retrieval or intermediate analysis and still fail to deliver a clear answer. That constraint belongs in the architecture, not only in the prompt.

function ensureFinalSynthesisAction(actions: Action[], input: string, catalog: CellCatalog) {
  if (actions.some((action) => isFinalAnswerAction(action))) {
    return actions;
  }

  return [
    ...actions,
    {
      title: "Synthesize observations and deliver the final answer",
      tool: preferredAiTool(catalog),
      args: {
        task: [
          "Synthesize all upstream tool observations and answer the user directly.",
          "Only output the user-facing deliverable. Do not output planning notes or step numbers.",
          `Original user request: ${input}`,
        ].join("\n"),
      },
      dependsOn: actions.map((_, index) => `react_${index + 1}`),
    },
  ];
}

Fourth, ReAct needs explicit thought/action/observation records. Otherwise it becomes harder to audit than PCA.

const traceEvents = [
  {
    type: "react_thought",
    payload: { iteration: 1, summary: thoughtSummary },
  },
  ...nodes.flatMap((node, index) => [
    {
      type: "react_action",
      payload: {
        iteration: index + 1,
        tool: node.cellId,
        title: node.title,
        argsPreview: compactArgs(node.args),
      },
    },
    {
      type: "react_observation",
      payload: {
        iteration: index + 1,
        summary: `Wait for ${node.cellId}, then decide the next step.`,
      },
    },
  ]),
];

This is not about exposing raw chain-of-thought to users. It is about debugging the system: why a tool was selected, which arguments were passed, and why a later round added or canceled an action.

The Real Change Is the State Machine

Many discussions of ReAct focus on prompts. From an architect’s perspective, the bigger change is state management.

In the old architecture, a task moved from planned to compiled to queued to completed, like a batch workflow.

In the new architecture, a run is a multi-round state machine:

const contextText = [
  buildConversationContext(messages),
  buildObservationContext(completedTasks),
  trigger === "auto"
    ? "Use completed observations to decide the next step."
    : "Use the latest user message to decide whether to revise the plan.",
].join("\n");

const react = await buildReactDag({
  input: latestUserText,
  contextText,
  catalog,
  incremental: true,
});

const nextDagVersion = currentDagVersion + 1;

await cancelPendingTasks({ runId, dagVersion: currentDagVersion });
await rebuildTasksForDag({
  runId,
  dagVersion: nextDagVersion,
  nodes: selectReactRoundNodes(react.nodes),
});

The point is not simply calling the LLM again. Each round carries observation context: which tasks completed, which failed, what the user added, which results remain reusable, and where evidence is still missing.

PCA re-planning tends to mean rebuilding the plan. ReAct re-planning means appending the next step based on observations.

This changes the user experience. The user no longer needs to wait for a large plan to finish. The system can continue after intermediate results arrive, and it can reorder future work when the user interrupts.

Re-reading This Through Mainstream Agent Designs

After reviewing OpenAI Deep Research, OpenAI Computer-Using Agent, Anthropic’s workflow/agent distinction, LangGraph, Kimi K2/K2.5, and Manus, I would narrow the claim: the real distinction is not whether the system plans. It is whether planning is front-loaded once, or continuously revised inside an observation loop.

I use the references in two ways. OpenAI, Anthropic, and LangGraph documents help show how mainstream products and engineering frameworks organize agents. The ReAct, AutoGen, Kimi, and Manus-related papers ground concepts such as reasoning/action loops, multi-agent collaboration, agentic capability, and risks in autonomous execution. These sources do not prove that one architecture is always correct. They help separate stable design choices from product-level exploration.

OpenAI Deep Research is a good example. It targets complex research tasks and emphasizes multi-step research, browsing, analysis, synthesis, and pivoting as new information appears [1]. This system is not plan-free. It puts planning inside a long-running research loop: search, read sources, identify gaps, search again, then synthesize. Its shape is closer to ReAct plus a research finalizer than to a one-shot static DAG.

OpenAI Computer-Using Agent shows the same point in a different domain. Its loop is perception, reasoning, and action: observe the screen, decide the next move, operate the mouse or keyboard, then observe the new screen state [2]. A full PCA plan would be fragile here because popups, login states, DOM changes, captchas, and form errors can invalidate early assumptions. GUI agents are naturally observation-driven.

Anthropic’s “Building Effective Agents” offers a useful distinction: fixed subtasks fit workflow patterns such as prompt chaining, routing, and parallelization, while unpredictable subtasks fit orchestrator-workers or agents [3]. This supports a hybrid architecture: predictable parts should become workflows, while unpredictable parts should remain dynamically orchestrated.

LangGraph approaches the same problem from the runtime side: long-running execution, statefulness, persistence, human-in-the-loop, debugging, and observability [4]. ReAct is not just a prompting trick. It is a runtime problem involving persistence, recovery, user intervention, and traceability. That is why DAG versions, observation context, and trace events are part of the core path in this refactor.

The ReAct paper is the direct conceptual source for the thought/action/observation loop in this article [5]. AutoGen shows how multi-agent conversation can act as a higher-level orchestration mechanism [6]. Together, they separate single-agent ReAct from multi-agent collaboration: the former explains how one agent continues acting from tool observations, while the latter explains how multiple agents divide work, converse, and aggregate results.

The Kimi K2 and K2.5 technical reports show another route: base models are increasingly optimized for agentic capability, and K2.5 introduces Agent Swarm, which dynamically decomposes complex tasks into heterogeneous subtasks and executes them concurrently [7][8]. This does not invalidate ReAct. It adds multi-agent concurrency on top of it. Architecturally, one action round may no longer be a single linear chain. It may be a set of parallel workers plus an aggregator.

Manus-related research is useful as a product-level case. Manus is described as an autonomous agent that can plan, browse, write code, process files, and produce structured outputs, with a transparent execution window and a strong end-to-end task feel [9]. At the same time, user research on AI agent software highlights recurring issues such as goal misunderstanding, uncontrolled loops, difficult result verification, and a gap between product aspirations and user trust [10]. The more autonomy a system has, the more it needs observation logs, user interruption, permission boundaries, result verification, and failure classification.

I split agent architecture into four layers:

LayerWhere it appears in mainstream systemsArchitectural implication
Model capability layerKimi K2/K2.5, OpenAI o-series, Claude [7][8]The model needs tool use, long-horizon reasoning, and environment interaction
Action-loop layerReAct, CUA, Deep Research [1][2][5]Complex tasks need thought/action/observation loops
Orchestration runtime layerLangGraph, Manus-like execution environments [4][9][10]State, persistence, approvals, traces, and recovery matter more than prompts alone
Specialized workflow layerAnthropic prompt chaining/routing/parallelization [3]Stable processes should remain workflow-like instead of being left to free-form agents

So the conclusion is narrower than “ReAct replaces PCA.” PCA should not be the only top-level control flow for a general-purpose agent, but it should still exist inside a ReAct system as a local workflow for stable subtasks. A mature agent system should not choose between ReAct and PCA globally. It should let ReAct handle dynamic decisions and let PCA handle stable execution fragments.

Why General-Purpose Agents Fit ReAct Better

General-purpose agents face open-ended inputs. A user might say:

User input patternArchitectural challenge
”Help me look into this”The system does not know whether to search, calculate, read files, or answer directly
”Build on the previous result”The system must identify historical results and new constraints
”Do not use that source”The system must down-rank or replace a source
”Give me a short version first”The system must shrink the delivery dynamically
”This is wrong; try another angle”The system must reuse evidence while changing the analysis frame

All of these cases share one property: the correct action depends on context and observations, not just on the initial input.

ReAct puts uncertainty back into the loop:

  1. If information is insufficient, gather evidence first.
  2. If a tool fails, switch tools or lower the evidence weight.
  3. If the user adds context, decide whether it revises the goal, adds constraints, or changes the delivery.
  4. If evidence is sufficient, synthesize the final answer.
  5. If evidence remains insufficient and cannot be improved, state the limitations and uncertainty.

That is closer to how a general-purpose agent actually works than compiling a 10-step DAG at the start.

Specialized Agents Still Need PCA

Moving to ReAct does not make PCA obsolete.

For highly stable specialized workflows, PCA remains the better fit:

Specialized task traitWhy PCA is better
Fixed input fieldsThe system can validate a schema directly
Fixed tool chainThe DAG can be predefined or semi-compiled
Strong compliance requirementsEvery step needs explicit auditability
Cost must be controlledSteps and resources can be estimated upfront
Failure recovery is clearNodes can be retried without re-reasoning

Specialized agents should not be encouraged to improvise. They need determinism, reviewability, and replayability. PCA’s static structure is an advantage there.

This is the layering I prefer:

flowchart TD
  U[User Goal] --> R[General ReAct Agent]
  R -->|Open-ended question| A[Dynamic Actions]
  R -->|Recognized stable task| P[Specialized PCA Workflow]
  P --> C[Compiled DAG]
  A --> E[Runtime]
  C --> E
  E --> F[Final Delivery]

The general-purpose agent understands the user goal, chooses strategy, and handles multi-turn context. Specialized agents execute stable workflows. The general layer uses ReAct. Specialized layers can still use PCA.

Design Principles From This Refactor

First, actions are closer to execution truth than plans.

In the old architecture, Planner steps often looked clean, but only the Compiler knew whether tools existed, whether arguments were valid, and whether dependencies were sound. After the refactor, the model selects actions directly under catalog constraints, reducing information loss between semantic steps and executable nodes.

Second, hard constraints belong in code, not only in prompts.

Tools must come from the catalog. A final delivery node must exist. Dependencies can only reference known nodes. Arguments must be normalized. These should not rely entirely on model discipline.

Third, ReAct must not mean infinite looping.

The system needs iteration limits, per-round action limits, dynamic mode switches, failed-action deduplication, and DAG versioning. Without those controls, ReAct becomes uncontrolled task accumulation.

Fourth, observation context is the most valuable asset in the new architecture.

Without structured observations, ReAct is just repeated model calls. Task state, tool output summaries, failure reasons, user additions, and reusable evidence must be organized into context that the next round can consume.

Fifth, final delivery should be separated from intermediate work.

An agent can internally evaluate, retrieve, clean, validate, and plan charts. The user still needs an answer, document, table, code artifact, or operation result. The architecture should distinguish internal from deliverable_content, so intermediate plans do not leak as final output.

Tradeoff Summary

From an architect’s point of view, the two patterns split like this:

PCA fits agents with determined processes, clear boundaries, and stable deliverables. Its advantages are control, auditability, and optimizability. Its downside is that it is not lightweight when conditions change dynamically.

ReAct fits agents with open-ended goals, uncertain evidence, and multi-round observation. Its advantages are flexibility, robustness to change, and a more natural assistant-like behavior. Its downside is that state, iteration, logging, and convergence need careful design.

This refactor was not a move from a wrong architecture to a right one. It was a natural evolution of the system. Early on, PCA was useful because it made the agent understandable, executable, and debuggable. Later, ReAct became necessary because the central problem shifted to dynamic context, uncertain tool outputs, and multi-turn revision.

The architecture I now prefer is hybrid:

LayerRecommended pattern
General entry pointReAct
Multi-turn revisionReAct
Exploratory researchReAct
Fixed business workflowPCA
High-compliance execution chainPCA
Final delivery synthesisIndependent Finalizer

The key is not choosing one pattern for everything. It is locating uncertainty. Compile the deterministic parts into workflows. Keep the uncertain parts inside observation loops. That keeps the system from getting stuck in a static DAG, while also preventing free-form action from becoming unbounded.

References

  1. OpenAI, Introducing deep research, 2025.
  2. OpenAI, Computer-Using Agent, 2025.
  3. Anthropic, Building Effective AI Agents, 2024.
  4. LangChain, LangGraph overview, LangGraph Docs.
  5. Shunyu Yao et al., ReAct: Synergizing Reasoning and Acting in Language Models, 2022.
  6. Qingyun Wu et al., AutoGen: Enabling Next-Gen LLM Applications via Multi-Agent Conversation, 2023.
  7. Kimi Team, Kimi K2: Open Agentic Intelligence, 2025.
  8. Kimi Team, Kimi K2.5: Visual Agentic Intelligence, 2026.
  9. Minjie Shen and Qikai Yang, From Mind to Machine: The Rise of Manus AI as a Fully Autonomous Digital Agent, 2025.
  10. Pradyumna Shome, Sashreek Krishnan, and Sauvik Das, Why Johnny Can’t Use Agents: Industry Aspirations vs. User Realities with AI Agent Software, 2025.

Comments

Comments are powered by GitHub Issues. A GitHub account is required.