feat(executor): stop_plan_at — cancel an RTDL plan at a given op#110
Conversation
New builtin MCP capability robonix/system/executor/builtin/stop_plan_at lets
the planner/LLM arm a breakpoint on an in-flight RTDL plan: when execution
reaches the node with a given op_id, the whole plan is cancelled (same teardown
as cancel_plan). 'when' selects the phase — on_enter cancels before the op's
call runs (op never executes); on_complete (default) cancels right after it
finishes. Stop points are stored per-plan and checked at each do node in the
execution loop; firing reuses begin_cancel + async-cancel drain so the existing
is_cancelled checks halt the rest of the plan. Unknown plan = success no-op;
unreferenced op_id never fires.
Adds StopWhen, PlanRun.stop_points, PlanRuntime::{stop_plan_at_builtin,
should_stop_at,trigger_stop}, the builtin dispatch + spec, the do-node
breakpoint checks, README docs, and unit tests.
…el stop in pilot prompt
Companion read-side builtins so the LLM can inspect a running plan before
stopping it (inspect first, then act):
- get_all_plans: list active plan_ids with op_count / cancelled / stop_point
count (no args).
- get_plan_status(plan_id): per-op {op_id, kind, description, live state,
armed stop_point}. Executor snapshots the op list at register and records
each op's state at every node_state emit site (operator terminals, do-node
result, on_enter cancel, async poll tick) so status reflects live progress.
Pilot's in-flight-trees prompt block now tells the LLM it can get_plan_status
to read op_ids/state and stop_plan_at a chosen op, not just cancel the whole
plan. Adds unit tests for both builtins.
cargo fmt --all / clippy --workspace -D warnings / test --workspace all green.
get_all_plans listed only plan_id + counts, leaving the LLM unable to tell which running plan is which. Snapshot the root node's description (which carries the model's rtdl_description, via pick_description's root fallback) into PlanRun at register time and surface it as each plan's `description`. No proto change — the executor Plan has no plan-level description field, so the root node's description is the in-executor equivalent.
End-to-end test on the Webots deploymentThe LLM agent autonomously drives the new Test environment
The model matters: the whole point is that the LLM itself chooses to call Local CI (pre-test)
Test flow
Result✅
A. Stack boot (
|
The dispatch log line printed only call_id/provider/contract, and the result preview was capped at 120 chars — so plan-control calls (stop_plan_at/cancel_plan/get_plan_status) were unreadable in the log (no args, truncated result). Log a bounded args preview (256 chars) on dispatch and widen the success-result preview to 512, so the executor log is self-contained for debugging these calls.
get_plan_status is a query; a stale or already-finished plan_id should be an error (not found), not a success with running:false. The cancel/stop commands stay idempotent no-ops, but a read must fail clearly so the LLM knows the id is wrong — get_all_plans is the way to check liveness.
|
@robonix-ci test |
✅ robonix CI — 14/14 passed (100%)
Report: open HTML report. |
KouweiLee
left a comment
There was a problem hiding this comment.
Thanks for the PR. I found two issues that look worth fixing before merge:
stop_plan_atnever fires for operator-nodeop_ids.
get_plan_status exposes every plan node, including sequence and parallel nodes, but the executor only checks stop points in the RTDL_DO branch. If the planner chooses the root sequence or a parallel group from the returned status, stop_plan_at succeeds and stores the stop point, but execution never consults it for that node kind. Either check stop points for all node kinds at node entry/completion, or only expose/accept DO-node op_ids.
Relevant code: system/executor/src/service.rs in the RTDL_DO branch, and system/executor/src/plan_runtime.rs where get_plan_status returns all run.ops.
- The new plan-control builtins are not treated as control-only trees.
is_control_only currently hides trees that consist only of cancel_plan / cancel_all_plans, so the LLM does not try to cancel its own control action. With this PR, a tree containing only get_plan_status or stop_plan_at can still be advertised as in-flight task work, which reopens the self-control-loop risk this guard was added to prevent. Please include the new control builtins in that classification.
Relevant code: system/pilot/src/planner.rs in is_control_only.
Validation: I attempted cargo test -p robonix-executor --all-targets and cargo test -p robonix-pilot --all-targets, but both fail before reaching tests because build.rs cannot resolve common_interfaces/sensor_msgs/msg/Image.msg from the capability lib roots.
OK for the |
|
Correction on validation: my earlier note about tests being blocked by missing
The two review findings above still stand; this only corrects the validation note. |
|
I fixed these 2 problems. When CI is ok, we can test it through CI.
|
|
@robonix-ci test |
1 similar comment
|
@robonix-ci test |
❌ robonix CI — no scenario summaryNo machine-readable scenario summary was produced. Treat this as an incomplete infrastructure run, not as a verified Webots pass. Report: no testing report artifact was available for this run. |
|
@robonix-ci test |
❌ robonix CI — 14/15 passed (93%)
Failure details
LLM analysis Change summary: Added three new executor builtins: get_all_plans, get_plan_status, stop_plan_at; Added plan_control_builtins test scenario in testing/scenarios/builtin/plan_control.yaml; Modified plan_runtime.rs to support stop points, op state recording, and plan inspection. Report: open HTML report. |
|
@robonix-ci test |
✅ robonix CI — 15/15 passed (100%)
LLM analysis Change summary: Added Report: open HTML report. |
Summary
Lets the planner/LLM stop a running RTDL plan at a specific op ("after this op finishes" or "the moment it is reached"), plus the read-side tools to inspect what is running first. "Stop" = cancel the whole plan (same teardown as
cancel_plan).Adds three executor builtin MCP capabilities (auto-registered via the
BUILTINStable, discovered by pilot likecancel_plan), an executor-logging fix, and a pilot prompt update so the LLM knows the inspect-then-act workflow. No proto/contract change.LLM-facing tool names:
executor.builtin_get_all_plans,executor.builtin_get_plan_status,executor.builtin_stop_plan_at.New interfaces (inputs / outputs)
1.
get_all_plans— list in-flight plansrobonix/system/executor/builtin/get_all_plansInput: none.
{}Output (JSON string):
{ "count": 2, "plans": [ { "plan_id": "1", "description": "run the three shell commands in strict sequence", "op_count": 4, "cancelled": false, "stop_points": 0 } ] }countexcludes them). Use this to act on what is currently running.descriptionis the plan's root-node description (carries the model'srtdl_description), so the LLM can tell which plan is which.op_countincludes operator nodes (sequence/parallel) + leafdonodes.stop_points= number of armed stop points.2.
get_plan_status— inspect one plan's ops + live staterobonix/system/executor/builtin/get_plan_statusInput:
plan_id(string, required).{ "plan_id": "1" }Output (running plan):
{ "plan_id": "1", "running": true, "cancelled": false, "ops": [ { "op_id": "4", "kind": "sequence", "description": "run the three shell commands…", "state": "running", "stop_point": null }, { "op_id": "7", "kind": "do", "description": "echo STEP_THREE_DONE", "state": "pending", "stop_point": "on_enter" } ] }kind∈sequence | parallel | do.state∈pending | running | succeeded | failed | canceled | timeout | paused(pending= op not started yet).stop_point∈on_enter | on_complete | null.get_plan_status: no active RTDL plan '<id>' (it never existed or already finished/cancelled); call get_all_plans to list running plans. Useget_all_plansto check whether a plan is still running;get_plan_statusis only for inspecting a plan you know is live.3.
stop_plan_at— arm a stop point on a planrobonix/system/executor/builtin/stop_plan_atInput:
plan_id(string, required),op_id(string, required),when(string, optional —on_enter | on_complete, defaulton_complete).{ "plan_id": "1", "op_id": "7", "when": "on_enter" }Output (text):
Stop point set: RTDL plan '1' will be cancelled on_enter op_id=7.RTDL plan '1' is not running (already finished or cancelled); no stop point needed.(success no-op — avoids planner retry loops)stop_plan_at: invalid when 'halt' (expected on_enter or on_complete)/stop_plan_at: op_id must not be emptySemantics: when execution reaches the node with
op_id, the whole plan is cancelled (marked cancelled + running async calls get<contract>/cancel, likecancel_plan; the loop'sis_cancelledchecks halt the rest).on_enter— cancel before that op's call runs (the op never executes).on_complete— cancel right after that op's call finishes.op_idthat never executes simply never fires; setting on an already-finished plan is a success no-op.Other changes
planner.rs build_forest_block): the in-flight-trees block now tells the LLM it canget_plan_statusto read op_ids/state andstop_plan_ata chosen op — not onlycancel_planthe whole plan.service.rs): the dispatch log line now includes a boundedargs=preview (256 chars) and the success-result preview is widened 120→512 chars, so plan-control calls are readable inrbnx logs -t executor.Implementation
plan_runtime.rs:StopWhenenum;PlanRun.{stop_points, ops, op_state, description};PlanRuntime::{stop_plan_at_builtin, should_stop_at, trigger_stop, get_plan_status_builtin, get_all_plans_builtin, record_plan_ops, record_op_state}.service.rs: op-id breakpoint checks in thedo-node arm ofexecute_node; op-state recorded at every node_state emit site; op list + description snapshotted at register.dispatch/builtin.rs: dispatch +BuiltinSpec(description + JSON schema) for the three ops.dispatch/async_poll.rs: record live op state per poll tick.Validation
cargo fmt --all -- --checkclean ·cargo clippy --workspace --all-targets -- -D warningsclean ·cargo test -p robonix-executor→ 31 passed (8 new).gpt-5.5): the LLM autonomously ranget_all_plans→get_plan_status→stop_plan_at(on_enter)on a live plan and cancelled it before the target op — full boot/exec/verify logs in the PR comment below.