You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 → ReviewGateProcessor → SynthesizeEnrichmentProcessor → PlanExecutionProcessor → PostExecutionReviewGateProcessor) that duplicates, one for one, capabilities the workflow graph
already owns:
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).
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 { }.
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.
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.
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:
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.
Hand-rolled expression evaluator.LoopNodeExecutor.evaluateExpression() parses
conditions by naive String.split on < / > / == — no <=, no compound expressions,
silent false on anything unrecognized.
State pollution. Puts the raw NodeResult object into context
(loop_last_result), which is not designed for JSON snapshot round-tripping.
Outside the sealed hierarchy.BreakRule and LoopCondition bypass the sealed TransitionRule interface entirely — state.setLoopBreakTarget(...) is a side-channel into TransitionPostProcessor.
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.
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 budget — maxReplans / maxIterations → BoundedTransition bound; confirm the
bound and the current iteration count are visible to prompts (feedback template) and
transition rules.
Time/token budget — maxDuration / 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)
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
(
BoundedTransitionwith feedback injection, shipped in #87). Planning, research, codegeneration, 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 entireio.hensu.core.planexecution pipeline, and theLoopNodeconstruct(
LoopNode/LoopNodeExecutor/BreakRule/LoopCondition) are to be removed.Problem
The plan subsystem is a second orchestrator hiding inside a single node.
AgenticNodeExecutorforks on
node.hasPlanningEnabled(): planning-disabled nodes take the standard path, whileplanning-enabled nodes run a five-processor pipeline (
PlanCreationProcessor→ReviewGateProcessor→SynthesizeEnrichmentProcessor→PlanExecutionProcessor→PostExecutionReviewGateProcessor) that duplicates, one for one, capabilities the workflow graphalready owns:
maxReplans)reviewflag)ReviewConfigon nodesplanFailureTargetonFailure gotomaxSteps/maxDurationconstraintsPlanEvent/PlanObserverExecutionListenerplan { }static step sequenceConcrete costs of the duplication:
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).The planner's value-add is a prompt template.
LlmPlannerconsists ofPLANNING_PROMPT_TEMPLATE("You are a planning agent…"), a revision template, and JSONparsing. A prompt is agent configuration, not engine code. It belongs in
agents { }.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 toolnames, string-templated arguments, parse failures — and sits outside the agent abstraction.
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.
STATICmode is the clearestsymptom: a predefined sequence of steps is definitionally what the graph expresses, so
plan { }is a shadow DSL for sequences.Liskov erosion.
StandardNodecarries three planning-only fields (planningConfig,staticPlan,planFailureTarget) that are dead weight for every non-planning node, and theexecutor dispatches on configuration rather than behavior.
LoopNode: the same anti-pattern, second copy
LoopNode+LoopNodeExecutor+BreakRule+LoopConditionis a third control-flowmechanism (after graph transitions and the plan pipeline), and it repeats the designation
mistake in a worse form:
whileloop inside asingle node execution — no per-iteration transitions, review, checkpointing, or recovery.
A crash mid-loop loses all iterations.
LoopNodeExecutor.evaluateExpression()parsesconditions by naive
String.spliton</>/==— no<=, no compound expressions,silent
falseon anything unrecognized.NodeResultobject into context(
loop_last_result), which is not designed for JSON snapshot round-tripping.BreakRuleandLoopConditionbypass the sealedTransitionRuleinterface entirely —state.setLoopBreakTarget(...)is a side-channel intoTransitionPostProcessor.hensu-dslconstructs aLoopNode; it is reachable onlythrough serialization — dead weight that still costs executor, serializer, and visualizer
branches.
Ironically,
LoopCondition.Expressionis the only place in the codebase with anarbitrary-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
nodewhose agent is defined inagents { }with a planning role and (typically) low temperature. The plan lands in contextvia
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.ToolRequestalready exists as the protocol seam. The engine never interpretstool-call lists itself. Note: no
ToolCapableimplementation exists yet inhensu-core—scoping the agent-native tool loop is a prerequisite work item of this ticket, and
ToolRegistry/ToolDefinitionsurvive as its vocabulary.Static sequences are graphs. Any
plan { }usage is rewritten as explicit nodes withexplicit 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), exceptToolRegistry/ToolDefinitionrelocation (see below).hensu-coreexecutors/processors:AgenticNodeExecutor(fold back intoStandardNodeExecutordispatch),PlanCreationProcessor,PlanExecutionProcessor,SynthesizeEnrichmentProcessor,ReviewGateProcessor,PostExecutionReviewGateProcessor(plan-gate variants only — node-level review stays).
StandardNode:planningConfig,staticPlan,planFailureTargetfields and builder methods.HensuState.get/setActivePlan, active-plan fields inHensuSnapshot,plan columns/JSON in
JdbcWorkflowStateRepository, plan handling inTransitionPostProcessor(_plan_failure_target).PlanningConfigBuilder,PlanBuilder,planning { }/plan { }wiring inStandardNodeBuilderandKotlinScriptParser.PlanStepActionSerializer/Deserializer, plan branches inNodeSerializer/NodeDeserializer,JacksonPlanResponseParser,registrations in
HensuJacksonModule.ExecutionEvent,ExecutionEventBroadcaster,LoggingExecutionListener,CompositeExecutionListener,ExecutionQueryService,onPlannerStart/onPlannerCompleteonExecutionListener, plan entries inCoreModelNativeConfigandHensuEnvironmentProducer.TextVisualizationFormat,MermaidVisualizationFormat.hensu-core:LoopNode,LoopNodeExecutor,BreakRule,LoopCondition,registration in
DefaultNodeExecutorRegistry, loop branches inNodeTargetsandNode,loopBreakTargethandling inHensuState/TransitionPostProcessor.hensu-serialization: loop branches inNodeSerializer/NodeDeserializer.hensu-cli: loop rendering inTextVisualizationFormat,MermaidVisualizationFormat.Keep / relocate
ToolRegistry,ToolDefinition— move usage behind the agent-side tool capability.ReviewConfigreview 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
LoopNodeprovided, for allstandard nodes. Exit-condition expressiveness has been verified against the current
TransitionRulehierarchy:output through engine variables extracted by
OutputExtractionPostProcessor:ScoreTransition(numericscorethresholds),ApprovalTransition(approvedboolean),RubricFailTransition(rubric outcome),Success/FailureTransition(result status).BoundedTransition(ScoreTransition)with feedback is therefore a complete ralph looptoday for score/rubric-driven convergence. The gap: no general output predicate — a
rule like "exit when
status == "complete"" over an arbitrary declared variable does notexist; the vocabulary is closed (status, score, approved, rubric). Resolution: add a
ConditionTransitionto the sealed hierarchy — a predicate on a declaredwrites/enginevariable, participating in
requiredEngineVars()so prompt injection and output extractionwire automatically. This absorbs the only useful part of
LoopCondition.Expression, with aproperly specified predicate model instead of string-sniffing.
maxReplans/maxIterations→BoundedTransitionbound; confirm thebound and the current iteration count are visible to prompts (feedback template) and
transition rules.
maxDuration/maxTokenBudgethave no generic equivalent; decidewhether a transition-level budget is warranted or explicitly out of scope (YAGNI).
StepStarted/StepCompleted; genericloop iterations are full node executions, so standard listener events must be sufficient for
SSE streaming and CLI progress.
checkpoint/resume through the standard recovery path.
Prerequisite / follow-up
ToolCapable-style interface,AgentResponse.ToolRequesthandlingin the standard executor) — separate ticket, blocks removal of
ToolCallStepHandlerif anyworkflow relies on engine-executed tools.
.ktworkflows usingplanning { }orplan { }(rewrite as planner-role node + ralph loop, or explicit node sequence).
Counterpoints considered
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.
same observability falls out of nodes-as-steps through the standard listener, with better
checkpointing.
Acceptance criteria
io.hensu.core.planpackage no longer exists;ToolRegistry/ToolDefinitionlive under theagent/tool capability seam.
StandardNodehas no planning fields; a single executor path serves all standard nodes.planning { }/plan { }with a clear parse error pointing to the migrationnotes.
active plan fail loud on resume with an actionable message (or are migrated, if any exist).
LoopNode,LoopNodeExecutor,BreakRule,LoopCondition, andloopBreakTargetstate aregone; serializers reject the loop node type with an actionable error.
ConditionTransition(general output predicate) exists in the sealedTransitionRulehierarchy, wired through
requiredEngineVars(), covered by DSL syntax and a loop-exit test.transition machinery or implemented as a transition-level extension — no node-level flags,
no planning-specific constructs.
loop test, e.g. rubric-driven convergence).
the docs.
References
hensu-core/src/main/java/io/hensu/core/plan/— subsystem under removalhensu-core/src/main/java/io/hensu/core/execution/executor/AgenticNodeExecutor.java— fork pointhensu-core/src/main/java/io/hensu/core/execution/executor/LoopNodeExecutor.java— LoopNodeanti-pattern under removal (hand-rolled expression evaluator at lines 99–125)
.claude/rules/10-java-standards.md— module boundaries, narrow capability interfacesdocs/tickets/dsl-parser-frontend-hardening.md— DSL-as-source-of-truth direction