Skip to content

[Feature] Remove plan subsystem, remove LoopNode, add general condition transition #88

Description

@alxsuv

Direction

Planning must not be a node-level designation that forks execution into a separate machinery.
It is an agent role: a regular node whose agent carries a planning prompt, producing a plan as
ordinary node output.

Iteration is likewise not planning-specific. The "ralph loop" — a node re-executing under a
bounded transition with feedback injected from the previous iteration — is a general capability
of every standard node
, built on the transition machinery that already exists
(BoundedTransition with feedback injection, shipped in #87). Planning, research, code
generation, quality convergence against a rubric: all are the same loop with a different agent
role. This ticket therefore has three thrusts: (a) remove the plan subsystem, (b) remove
LoopNode — a second embodiment of the same anti-pattern (iteration as a node designation), and
(c) close the one real gap in the generic loop capability with a general condition transition,
so the loop fully covers what both removed constructs claimed to provide, for all standard nodes.

The planning { } / plan { } DSL blocks, PlanningConfig, PlanningMode, the entire
io.hensu.core.plan execution pipeline, and the LoopNode construct
(LoopNode/LoopNodeExecutor/BreakRule/LoopCondition) are to be removed.

Problem

The plan subsystem is a second orchestrator hiding inside a single node. AgenticNodeExecutor
forks on node.hasPlanningEnabled(): planning-disabled nodes take the standard path, while
planning-enabled nodes run a five-processor pipeline (PlanCreationProcessor
ReviewGateProcessorSynthesizeEnrichmentProcessorPlanExecutionProcessor
PostExecutionReviewGateProcessor) that duplicates, one for one, capabilities the workflow graph
already owns:

Plan subsystem Graph equivalent
Replan loop (maxReplans) Bounded revise transitions (#87)
Review gates 1 and 2 (review flag) ReviewConfig on nodes
planFailureTarget onFailure goto
maxSteps / maxDuration constraints Transition bounds
PlanEvent / PlanObserver ExecutionListener
Active-plan checkpoint persistence Standard snapshot persistence
plan { } static step sequence The graph itself (a sequence of nodes)

Concrete costs of the duplication:

  1. Two control-flow models. Every cross-cutting feature (recovery, review, visualization,
    streaming, feedback injection) must be built and debugged twice. The recovery work already
    tracks four compounding bugs in the standard path; the plan path adds a parallel
    active-plan persistence and resume story on top (HensuState.getActivePlan,
    HensuSnapshot, JdbcWorkflowStateRepository).

  2. The planner's value-add is a prompt template. LlmPlanner consists of
    PLANNING_PROMPT_TEMPLATE ("You are a planning agent…"), a revision template, and JSON
    parsing. A prompt is agent configuration, not engine code. It belongs in agents { }.

  3. Plan-then-execute is a dead-end interaction pattern. The upfront plan is fixed unless a
    step fails; a step that succeeds with surprising output cannot redirect the plan. A ralph
    loop re-decides on every iteration with full context. Modern agents also perform tool use
    natively in an agentic loop; the engine-side JSON-plan interpreter
    (StepHandlerRegistry / ToolCallStepHandler) is brittle by comparison — hallucinated tool
    names, string-templated arguments, parse failures — and sits outside the agent abstraction.

  4. Plan steps are invisible to the graph. Steps are not nodes: no rubrics, no per-step
    review, no transition rules, no graph-structured visualization, and the whole plan collapses
    into a single node execution for checkpointing purposes. STATIC mode is the clearest
    symptom: a predefined sequence of steps is definitionally what the graph expresses, so
    plan { } is a shadow DSL for sequences.

  5. Liskov erosion. StandardNode carries three planning-only fields (planningConfig,
    staticPlan, planFailureTarget) that are dead weight for every non-planning node, and the
    executor dispatches on configuration rather than behavior.

LoopNode: the same anti-pattern, second copy

LoopNode + LoopNodeExecutor + BreakRule + LoopCondition is a third control-flow
mechanism (after graph transitions and the plan pipeline), and it repeats the designation
mistake in a worse form:

  1. Iterations invisible to the graph. The body node executes in a while loop inside a
    single node execution — no per-iteration transitions, review, checkpointing, or recovery.
    A crash mid-loop loses all iterations.
  2. Hand-rolled expression evaluator. LoopNodeExecutor.evaluateExpression() parses
    conditions by naive String.split on < / > / == — no <=, no compound expressions,
    silent false on anything unrecognized.
  3. State pollution. Puts the raw NodeResult object into context
    (loop_last_result), which is not designed for JSON snapshot round-tripping.
  4. Outside the sealed hierarchy. BreakRule and LoopCondition bypass the sealed
    TransitionRule interface entirely — state.setLoopBreakTarget(...) is a side-channel into
    TransitionPostProcessor.
  5. No DSL surface. No builder in hensu-dsl constructs a LoopNode; it is reachable only
    through serialization — dead weight that still costs executor, serializer, and visualizer
    branches.

Ironically, LoopCondition.Expression is the only place in the codebase with an
arbitrary-predicate exit condition — locked inside the wrong construct. That capability is the
one genuine gap in the generic loop (see part c below).

Target design

  • Planner as role. A planning step is a regular node whose agent is defined in
    agents { } with a planning role and (typically) low temperature. The plan lands in context
    via writes = ["plan"] as ordinary output. No engine involvement.

  • Ralph loop as a universal node capability. Any standard node can iterate: multi-node
    cycles (plan → act → check) and single-node self-loops (node revising itself with its previous
    output as feedback) both run on bounded revise transitions with feedback injection (feat(transition): bounded revise transitions with auto feedback injection #87). The
    capability is agent-role-agnostic — the same loop serves planning, research, generation, and
    rubric-driven convergence. No new node types and no node-level flags; if a gap appears
    (exit conditions, iteration counters exposed to prompts and transition rules, per-iteration
    checkpointing), extend the transition machinery, not the node model.

  • Tool use inside the agent. Tool execution moves behind the agent abstraction via a narrow
    capability interface (per 10-java-standards.md: ToolCapable-style, not a God interface).
    AgentResponse.ToolRequest already exists as the protocol seam. The engine never interprets
    tool-call lists itself. Note: no ToolCapable implementation exists yet in hensu-core
    scoping the agent-native tool loop is a prerequisite work item of this ticket, and
    ToolRegistry / ToolDefinition survive as its vocabulary.

  • Static sequences are graphs. Any plan { } usage is rewritten as explicit nodes with
    explicit transitions, gaining per-step review, rubrics, and checkpointing for free.

Scope

Remove

  • io.hensu.core.plan.* — 25 classes, ~2,460 lines (Plan, Planner, LlmPlanner,
    StaticPlanner, PlanExecutor, PlanPipeline, PlanProcessor, PlanContext,
    PlanningConfig, PlanningMode, PlanConstraints, PlannedStep, PlanStepAction,
    StepHandler*, StepResult, PlanEvent, PlanObserver, exceptions), except
    ToolRegistry / ToolDefinition relocation (see below).
  • hensu-core executors/processors: AgenticNodeExecutor (fold back into
    StandardNodeExecutor dispatch), PlanCreationProcessor, PlanExecutionProcessor,
    SynthesizeEnrichmentProcessor, ReviewGateProcessor, PostExecutionReviewGateProcessor
    (plan-gate variants only — node-level review stays).
  • StandardNode: planningConfig, staticPlan, planFailureTarget fields and builder methods.
  • State/persistence: HensuState.get/setActivePlan, active-plan fields in HensuSnapshot,
    plan columns/JSON in JdbcWorkflowStateRepository, plan handling in
    TransitionPostProcessor (_plan_failure_target).
  • DSL: PlanningConfigBuilder, PlanBuilder, planning { } / plan { } wiring in
    StandardNodeBuilder and KotlinScriptParser.
  • Serialization: PlanStepActionSerializer/Deserializer, plan branches in
    NodeSerializer/NodeDeserializer, JacksonPlanResponseParser,
    registrations in HensuJacksonModule.
  • Server: plan events in ExecutionEvent, ExecutionEventBroadcaster,
    LoggingExecutionListener, CompositeExecutionListener, ExecutionQueryService,
    onPlannerStart/onPlannerComplete on ExecutionListener, plan entries in
    CoreModelNativeConfig and HensuEnvironmentProducer.
  • CLI: planning branches in TextVisualizationFormat, MermaidVisualizationFormat.
  • Docs: planning sections in module READMEs and the DSL reference.
  • LoopNode surface (all modules):
    • hensu-core: LoopNode, LoopNodeExecutor, BreakRule, LoopCondition,
      registration in DefaultNodeExecutorRegistry, loop branches in NodeTargets and Node,
      loopBreakTarget handling in HensuState / TransitionPostProcessor.
    • hensu-serialization: loop branches in NodeSerializer / NodeDeserializer.
    • hensu-cli: loop rendering in TextVisualizationFormat, MermaidVisualizationFormat.

Keep / relocate

  • ToolRegistry, ToolDefinition — move usage behind the agent-side tool capability.
  • Node-level ReviewConfig review gates — untouched.
  • BoundedTransition / feedback injection — becomes the sole loop mechanism.

Loop capability gap analysis (part c)

Audit of the generic loop against what the plan subsystem and LoopNode provided, for all
standard nodes. Exit-condition expressiveness has been verified against the current
TransitionRule hierarchy:

  • Exit condition — partially covered, one gap. Transition rules already route on node
    output through engine variables extracted by OutputExtractionPostProcessor:
    ScoreTransition (numeric score thresholds), ApprovalTransition (approved boolean),
    RubricFailTransition (rubric outcome), Success/FailureTransition (result status).
    BoundedTransition(ScoreTransition) with feedback is therefore a complete ralph loop
    today for score/rubric-driven convergence. The gap: no general output predicate — a
    rule like "exit when status == "complete"" over an arbitrary declared variable does not
    exist; the vocabulary is closed (status, score, approved, rubric). Resolution: add a
    ConditionTransition to the sealed hierarchy — a predicate on a declared writes/engine
    variable, participating in requiredEngineVars() so prompt injection and output extraction
    wire automatically. This absorbs the only useful part of LoopCondition.Expression, with a
    properly specified predicate model instead of string-sniffing.
  • Iteration budgetmaxReplans / maxIterationsBoundedTransition bound; confirm the
    bound and the current iteration count are visible to prompts (feedback template) and
    transition rules.
  • Time/token budgetmaxDuration / maxTokenBudget have no generic equivalent; decide
    whether a transition-level budget is warranted or explicitly out of scope (YAGNI).
  • Per-iteration observability — plan steps emitted StepStarted/StepCompleted; generic
    loop iterations are full node executions, so standard listener events must be sufficient for
    SSE streaming and CLI progress.
  • Per-iteration checkpointing — each loop iteration is a normal node execution and must
    checkpoint/resume through the standard recovery path.

Prerequisite / follow-up

  • Agent-native tool loop (ToolCapable-style interface, AgentResponse.ToolRequest handling
    in the standard executor) — separate ticket, blocks removal of ToolCallStepHandler if any
    workflow relies on engine-executed tools.
  • Migration notes for any existing .kt workflows using planning { } or plan { }
    (rewrite as planner-role node + ralph loop, or explicit node sequence).

Counterpoints considered

  • One planning call for N steps is cheaper than N loop iterations. True but marginal: the
    loop's per-iteration call is the price of adaptivity, and purely deterministic tool chains
    belong in the graph as static nodes, which cost zero planning calls.
  • Engine-executed steps give deterministic tool invocation and fine-grained SSE events. The
    same observability falls out of nodes-as-steps through the standard listener, with better
    checkpointing.

Acceptance criteria

  • io.hensu.core.plan package no longer exists; ToolRegistry/ToolDefinition live under the
    agent/tool capability seam.
  • StandardNode has no planning fields; a single executor path serves all standard nodes.
  • DSL rejects planning { } / plan { } with a clear parse error pointing to the migration
    notes.
  • Snapshot schema and JDBC repository carry no plan state; existing persisted snapshots with an
    active plan fail loud on resume with an actionable message (or are migrated, if any exist).
  • LoopNode, LoopNodeExecutor, BreakRule, LoopCondition, and loopBreakTarget state are
    gone; serializers reject the loop node type with an actionable error.
  • ConditionTransition (general output predicate) exists in the sealed TransitionRule
    hierarchy, wired through requiredEngineVars(), covered by DSL syntax and a loop-exit test.
  • Loop gap analysis (part c) resolved: each remaining item either confirmed covered by existing
    transition machinery or implemented as a transition-level extension — no node-level flags,
    no planning-specific constructs.
  • Ralph loop works for an arbitrary standard node (demonstrated by at least one non-planning
    loop test, e.g. rubric-driven convergence).
  • A sample workflow demonstrating planner-role + ralph loop replaces the planning examples in
    the docs.
  • Native build green; no reflect-config entries remain for deleted plan classes.

References

  • hensu-core/src/main/java/io/hensu/core/plan/ — subsystem under removal
  • hensu-core/src/main/java/io/hensu/core/execution/executor/AgenticNodeExecutor.java — fork point
  • hensu-core/src/main/java/io/hensu/core/execution/executor/LoopNodeExecutor.java — LoopNode
    anti-pattern under removal (hand-rolled expression evaluator at lines 99–125)
  • feat(transition): bounded revise transitions with auto feedback injection #87 — bounded revise transitions with auto feedback injection (loop mechanism)
  • .claude/rules/10-java-standards.md — module boundaries, narrow capability interfaces
  • docs/tickets/dsl-parser-frontend-hardening.md — DSL-as-source-of-truth direction

Metadata

Metadata

Assignees

Labels

area: coreWorkflow execution runtimetype: featureNew feature or request

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions