How Embabel Dynamic Replanning Works: Architecture and Implementation

Embabel's dynamic replanning system allows agents to discard invalidated plans during execution by emitting a ReplanRequestedEvent, which triggers the core planner to clear state and generate a fresh plan while recording telemetry via OpenTelemetry spans and metrics.

Embabel dynamic replanning enables AI agents to adapt when tools determine that the current plan is no longer viable. In the embabel/embabel-agent repository, this mechanism is implemented through a sophisticated event-driven architecture that bridges tool execution with planning telemetry. The system provides developers with multiple integration points—from high-level wrappers to low-level exceptions—while maintaining full observability through structured metadata.

The Plan-Execute-Observe Loop

Embabel agents operate on a continuous plan-execute-observe cycle. During the execution phase, each tool evaluates whether the current plan remains valid given the context and results. When a tool detects that the plan has become obsolete—whether due to changed conditions, policy violations, or unexpected outputs—it signals the need for regeneration.

This signal propagates through the agent core as a ReplanRequestedEvent defined in ReplanRequestedEvent.java (or via a thrown ReplanRequestedException from ReplanRequestedException.java), carrying a human-readable reason string that explains why the current plan must be abandoned. The core planner receives this event, immediately clears the existing Plan object, resets the iteration counter, and initiates a new planning cycle with the updated context.

Triggering a Replan

Developers can initiate dynamic replanning through three distinct mechanisms, each suited to different architectural needs.

Using Tool Wrappers

The Tool utility class in Tool.java provides three static helper methods that transform standard tools into replan-capable components. These wrappers return a ReplanDecision object—defined in ReplanDecision.java—that carries an optional reason string back to the agent core:

  • Tool.replanAlways(delegate) – Forces a replan after every tool execution, regardless of outcome.
  • Tool.replanWhen(delegate, predicate) – Conditionally triggers replanning only when the supplied predicate evaluates to true against the tool's output.
  • Tool.replanAndAdd(delegate, valueComputer) – Forces a replan while attaching a computed value to the decision metadata.
// Force replanning after every execution
Tool<String> myTool = Tool.replanAlways(delegateTool);

// Conditional replanning based on output analysis
Tool<String> conditional = Tool.replanWhen(delegateTool,
    result -> result.contains("retry"));

// Replan with custom diagnostic information
Tool<String> withReason = Tool.replanAndAdd(delegateTool,
    output -> new ReplanDecision("Stuck on \"%s\"".formatted(output)));

Throwing ReplanRequestedException

For imperative control flow, code within skills or tool implementations can throw ReplanRequestedException with a descriptive reason. The agent's process-event listener intercepts this exception and converts it into a ReplanRequestedEvent for consistent handling.

public void someSkill() {
    if (needsNewPlan()) {
        throw new ReplanRequestedException("policy violation – re‑plan");
    }
}

Automatic Detection

Certain internal Embabel components invoke Tool.replanWhen automatically. For example, tool loops that exhaust their configured retry limits trigger replanning without requiring explicit user code, ensuring the agent can recover from transient failures autonomously.

The Replanning Flow

When a replan is triggered, the system executes a precise six-step sequence:

  1. Tool Execution – The tool runs within the embabel.tool_loop span context.
  2. Decision Generation – The tool returns a ReplanDecision or throws ReplanRequestedException.
  3. Event Emission – The agent core creates a ReplanRequestedEvent containing the reason string.
  4. Observer Handling – EmbabelSpanEventListener receives the event and starts a new embabel.replan span with gen_ai.operation.name = "replan", adding the embabel.replan.reason attribute when present.
  5. Metric Update – EmbabelMetricsEventListener increments the embabel.planning.replanning.total counter, tagged with the agent name.
  6. Planner Restart – The planning component discards the old plan, resets the iteration counter to 1, and invokes the planner to generate a fresh plan. The attribute embabel.plan.is_replanning resets to "false" until another request occurs.

Observability and Telemetry

Embabel dynamic replanning exposes rich telemetry through OpenTelemetry-compatible spans and metrics, enabling debugging of planning behavior in production.

The EmbabelSpanEventListener in EmbabelSpanEventListener.java tracks two critical metadata attributes:

Attribute Type Description
embabel.plan.is_replanning Boolean string Set to "true" when the current planning iteration was triggered by a replan request
embabel.replan.reason String Free-form text supplied by the tool that caused the replan

Additionally, the EmbabelMetricsEventListener in EmbabelMetricsEventListener.java maintains a counter metric embabel.planning.replanning.total that increments for every replan request, enabling aggregation of replanning frequency by agent name.

// Reading replanning telemetry in a custom span listener
public void onProcessEvent(AgentProcessEvent event) {
    if (event instanceof ReplanRequestedEvent rep) {
        Observation observation = Observation.create(SpanAttributes.EMBABEL_REPLAN, "replan")
            .lowCardinalityKeyValue(SpanAttributes.GEN_AI_OPERATION_NAME, "replan")
            .lowCardinalityKeyValue(SpanAttributes.EMBABEL_REPLAN_REASON, rep.getReason())
            .start();
        // ... observation handling ...
    }
}

Summary

  • Embabel dynamic replanning operates through a plan-execute-observe loop where tools signal plan invalidation via ReplanRequestedEvent or ReplanRequestedException.
  • Developers trigger replans using tool wrappers (replanAlways, replanWhen, replanAndAdd), manual exceptions, or rely on automatic detection for retry exhaustion.
  • The EmbabelSpanEventListener records replanning context through attributes embabel.plan.is_replanning and embabel.replan.reason.
  • The counter metric embabel.planning.replanning.total provides quantitative insights into replanning frequency per agent.
  • All replanning events carry human-readable reasons, enabling transparent debugging of why agents abandon plans during execution.

Frequently Asked Questions

What is the difference between ReplanRequestedEvent and ReplanRequestedException?

ReplanRequestedException is a throwable exception designed for imperative code paths within skills or tools, while ReplanRequestedEvent is the internal event object that propagates through the agent's event bus. When code throws ReplanRequestedException, the agent's exception handler catches it and converts it into a ReplanRequestedEvent for uniform processing by listeners like EmbabelSpanEventListener.

How can I track how many times an agent replans?

The embabel.planning.replanning.total counter metric increments for every replan request and is tagged with the agent name. You can query this metric in your observability backend to analyze replanning frequency. Additionally, the embabel.plan.is_replanning span attribute marks individual planning iterations that were triggered by replan requests.

Can I provide a custom reason when requesting a replan?

Yes. When using Tool.replanAndAdd(), the ReplanDecision constructor accepts a reason string that appears in the embabel.replan.reason span attribute. Similarly, ReplanRequestedException accepts a reason parameter in its constructor. This text helps operators understand why specific tools determined the plan was no longer viable.

Which Embabel component actually generates the new plan?

While the ReplanRequestedEvent signals the need for regeneration, the core planner component (integrated with the agent's planning cycle) generates the new plan. When the event is received, the planner discards the existing Plan object, resets the iteration counter to 1, and invokes the LLM or planning algorithm to create a fresh strategy based on current context.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →