Architecture

ePlanSys attaches to PlanSys2 through its extension points. The planner is a plan solver plugin, the tree builder is a behavior tree builder plugin, the epistemic behavior tree nodes are loaded as a BehaviorTree.CPP plugin library, the model of what the agents know is a separate lifecycle node, and perception is a second one that watches a map and reports to the first. Around them sit a front end that grounds EPDDL into the task they all read, a command line that asks the model questions, and a metapackage that installs the set. Nothing in plansys2_executor links against any of it. Every attachment is made through pluginlib or a parameter, so a classical deployment loads none of it.

Three of these extension points were widened in the upstream packages to carry a policy through the system. PlanItem gained the fields that let a plan branch, ActionExecution gained the field that carries what a performer observed, and the executor gained the ability to load behavior tree nodes named by parameter. All three are additions that a classical run leaves at their defaults, so the classical execution path is unchanged. NOTICE documents these and the remaining differences from upstream.

digraph architecture { rankdir=LR; node [shape=box, fontname="sans-serif", fontsize=10]; edge [fontname="sans-serif", fontsize=9]; subgraph cluster_plansys2 { label="PlanSys2"; fontname="sans-serif"; fontsize=10; planner [label="planner"]; executor [label="executor"]; problem [label="problem expert"]; domain [label="domain expert"]; } subgraph cluster_epistemic { label="ePlanSys"; fontname="sans-serif"; fontsize=10; solver [label="EpistemicPlanSolver\n(plan solver plugin)"]; builder [label="EpistemicBTBuilder\n(BT builder plugin)"]; nodes [label="epistemic BT nodes\n(BT.CPP plugin library)"]; state [label="epistemic_state\n(lifecycle node)"]; perceive [label="epistemic_perception\n(lifecycle node)"]; } map [label="occupancy grid\n(SLAM)", shape=ellipse]; planner -> solver [label="loads"]; executor -> builder [label="loads"]; executor -> nodes [label="loads"]; executor -> problem [label="facts"]; executor -> domain [label="action trees"]; nodes -> state [label="services"]; map -> perceive [label="grid"]; perceive -> state [label="outcomes,\nannouncements"]; solver -> executor [label="Plan with\nepistemic fields", style=dashed]; }

Fig. 1 Where the epistemic packages attach to PlanSys2

Plan solver plugin

plansys2/EpistemicPlanSolver implements the plansys2::PlanSolverBase interface and is exported to plansys2_core by the plugin description in plansys2_epistemic_planner. It is selected by listing it in plan_solver_plugins and naming its class under that entry:

planner:
  ros__parameters:
    plan_solver_plugins: ["EPISTEMIC"]
    EPISTEMIC:
      plugin: "plansys2/EpistemicPlanSolver"

Its parameters are declared under the plugin’s own name at configure time, and are strings except where noted:

Parameter

Default

Meaning

epddl_domain

empty

Path to the EPDDL domain. With epddl_problem, the ordinary way to state the problem, and the epistemic counterpart of model_file.

epddl_problem

empty

Path to the EPDDL problem.

epddl_libraries

empty

Action-type libraries the domain declares. Empty supplies the intermediate library packaged with plansys2_epddl_grounder. This one is a string array, consistent_beliefs is a boolean, and the rest are strings.

plank_command

empty

Path to the plank binary. Empty takes $PLANK, then PATH.

task_file

empty

Absolute path to an already grounded task JSON. Ignored when the EPDDL sources are set; with neither, the task must arrive in the planning request instead.

heuristic

empty

Pins a heuristic: ug, ed, ks, wc, rpg, radd, kadd or kff. Empty leaves the choice to the selection policy.

strategy

empty

Pins a search strategy: gbfs, ehc, aostar, replan or portfolio. Empty leaves the choice to the selection policy, and AO* chosen that way on a task with sensing actions gets a short pass before replan takes the rest of the timeout, as in the Aletheia binary.

policy_file

empty

Path to a JSON selection policy replacing the built-in rule table.

action_mapping

empty

Path to a JSON map from grounded epistemic action names to PlanSys2 action expressions. Empty falls back to the naming convention.

conditional_plan

flatten

What to do with a branching solution: policy, flatten or reject. The parameters file shipped by plansys2_bringup sets policy.

consistent_beliefs

true

Refuses any state in which an agent believes a contradiction. An agent that observes an event its beliefs rule out is left with no accessible world, and every goal about what it believes then holds without it believing anything. false accepts those plans, as plank does.

The solver validates a solution before returning it, and a plan that fails validation is not returned at all.

The planner node answers both planning services late: the service callback hands the request to the node’s own planning thread and returns, so the executor is free while the search runs, and one request is searched at a time. This matters in plansys2_bringup’s monolithic node, where one executor serves every PlanSys2 node and also answers the lifecycle manager bringing them up. A search that ran inside its callback held all of that for as long as it took: seconds, for an epistemic domain of any size, which is long enough for bring-up to give up on nodes that were never asked.

Running the planner as a subprocess

plansys2_aletheia_plan_solver provides a second plan solver plugin, plansys2/AletheiaPlanSolver, which reaches the same planner by running its binary instead of linking it. It is to the epistemic planner what plansys2_popf_plan_solver is to POPF: an adapter that writes the input to a file, runs a process under the solver timeout, and reads its output back.

planner:
  ros__parameters:
    plan_solver_plugins: ["ALETHEIA"]
    ALETHEIA:
      plugin: "plansys2/AletheiaPlanSolver"
      command: "/abs/path/to/epistemic_planner"
      conditional_plan: "policy"

It adds three parameters to the seven above: command, the binary, which defaults to epistemic_planner on the path; arguments, appended verbatim; and output_dir, where the task, the plan and the planner’s log are written. The other seven keep their names and meanings, so a parameters file moves between the two plugins by changing plugin alone.

The plan file names actions and event indices only. What a branch is taken on, what an action requires to be known, and what the goal is are properties of the task, not of the plan, so the task is parsed on this side as well and the conversion is performed by plansys2_epistemic_planner’s own policy serialisation. The returned plan is also validated against the task as parsed here, in addition to the planner’s own validation: the two can disagree only if the binary and the workspace were built from different Aletheia checkouts, which is the failure a separately built planner introduces. The aletheia package installs both from one checkout, and a test in this package holds the two plugins to the same policy on the fleet tasks.

The in-process plugin remains the better default, since it costs no process launch, no serialisation, and no separately installed binary. The subprocess plugin is for a planner versioned apart from the workspace, one run under its own resource limits, or one being compared against the in-process build.

Policies

A policy is a tree, and it travels in the epistemic fields of plansys2_msgs/PlanItem:

Field

Contents

children

One continuation per possible outcome, by item index.

outcomes

The observation selecting each, in the same order.

sensing

Whether the outcome has to be observed at all.

knowledge_requirements

The epistemic conditions the action needs.

epistemic_action

The action’s name in the planner’s own vocabulary.

A classical plan sets none of them and is read the way PlanSys2 writes it: item i followed by item i+1. Every plan is therefore a policy here, and the distinction that matters is whether it branches.

Behavior tree

plansys2::EpistemicBTBuilder renders each policy node as the PlanSys2 action subtree, unchanged, wrapped in the three things a sequence cannot express: a guard on what must be known, the knowledge update the action performs, and a branch on what was observed.

Sequence "node_0 (peek A)"
  CheckKnowledge         node="0"
  WaitAtStartReq         action="(peek A):0"           ┐
  ApplyAtStartEffect     action="(peek A):0"           │
  ReactiveSequence                                     │ PlanSys2's own
    CheckOverAllReq      action="(peek A):0"           │ nodes, driving the
    ExecuteAction        action="(peek A):0"           │ same performers
                         outcome="{epistemic_observed_0}"
  CheckAtEndReq          action="(peek A):0"           │
  ApplyAtEndEffect       action="(peek A):0"           ┘
  ApplyEpistemicUpdate   node="0" observed="{epistemic_observed_0}"
                                  outcome="{epistemic_outcome_0}"
  EpistemicSwitch        node="0" outcome="{epistemic_outcome_0}"
                                  outcomes="e_tails;e_heads"
    Sequence "node_1 (shout-tails A)"
    Sequence "node_2 (open A)"
CheckEpistemicGoal goal="(K A tails)"

A node with a single continuation renders without a switch, so a classical plan comes out as the same flat sequence PlanSys2 would have built.

get_graph reports the policy as a temporal graph: one node per policy node, an arc to each continuation. SimpleBTBuilder derives its graph by working out which actions establish each other’s preconditions, because a sequential plan leaves the ordering open. A policy has it already, so this records the structure instead of deriving one, and propagate fills in the bounds from the durations the plan carries.

The lower bound on every arc is the parent’s whole duration, so a continuation cannot begin before the action it follows has finished. That is stricter than a classical plan needs, and it is a requirement of the epistemic layer rather than a scheduling choice: CheckKnowledge on a continuation is evaluated against the state that ApplyEpistemicUpdate produced for its parent, so the two overlapping would check a guard against a model that does not exist yet.

The consequence is that the epistemic builder runs one action at a time, where SimpleBTBuilder can run independent actions concurrently. Recovering that would mean proving, for each pair, both that their PDDL effects do not interfere and that neither one’s knowledge guard depends on the other’s epistemic update. The second condition has no counterpart in classical planning and is not checked anywhere today, so the builder sequences rather than assume it.

Reporting an observation

Which outcome a sensing action produced is a question about the world, and the epistemic state can answer it alone only when its model designates a single world. With several designated the model is undetermined, and the answer must come from the agent that performed the sensing.

The observation travels on the protocol that carries every other execution report. plansys2_msgs/ActionExecution defines an outcome field alongside status, meaningful on FINISH, which a performer sets when it finishes:

// in the performer's do_work, once the sensor has answered
finish(true, 1.0, "Corridor inspected", clear ? "e-inspect-clear"
                                              : "e-inspect-blocked");

The token must name an event of the action’s model, since the policy branches on it. status remains the human-readable message for the log. An ordinary action leaves the field empty, which is the default for every classical performer and the reason none of them required changes.

The value then reaches the tree without further configuration. ExecuteAction provides an outcome output port, the packaged action template binds it to ApplyEpistemicUpdate’s observed input, and each policy node receives its own blackboard entry, so two sensing actions on different branches cannot overwrite each other. plansys2_msgs/ActionExecutionInfo also carries the field, allowing a monitor subscribed to action_execution_info to observe which branch a policy took without subscribing to the actions hub.

An observation originating elsewhere, such as a perception node operating independently of the acting robot, is supplied by binding observed in a deployment-specific action template. Both routes are covered by the integration tests; see Executing a policy end to end.

Epistemic nodes

Four node types are registered by the plugin library libplansys2_epistemic_bt_nodes.so.

CheckKnowledge

A condition node, the counterpart of CheckOverAllReq for conditions the problem expert cannot answer. “The corridor is clear” is a fact and lives there; “r1 knows whether the corridor is clear” is not a fact about the corridor, and no set of predicates records it. Ports: node, the index of the policy node being guarded, and action, the action identifier used in the error message.

ApplyEpistemicUpdate

The counterpart of ApplyAtEndEffect, one level up. An action changes what agents know by more than its own effects: an announcement informs everyone listening, and an observation that finds nothing still rules worlds out. This performs the DEL product update, so the next action’s guard is checked against the state this action produced. Input ports: node, action, and observed, the outcome the performer observed, which the packaged action template binds to ExecuteAction’s outcome port and which is left empty to let the state decide. Output port: outcome, the outcome that occurred, which the following EpistemicSwitch reads. The two are separate blackboard entries: observed holds the performer’s report and outcome holds the state’s reading of it.

EpistemicSwitch

A control node with no PlanSys2 counterpart. It reads the outcome the update reported and runs the continuation planned for it. Ports: node, outcome, the observed outcome, and outcomes, the outcomes in child order, separated by semicolons. An outcome the policy does not list fails the node instead of defaulting to a branch: every branch was built for a different belief, and running one anyway is a robot acting confidently on a belief nothing supports. The failure reaches the executor, whose answer to a failed plan is to replan.

CheckBeliefUnchanged

A condition node that fails when the belief has changed from outside the plan, which is what makes an announcement interrupt a policy it invalidates. Ports: action for the message, and enabled, false for a deployment that would rather not be interrupted.

CheckEpistemicGoal

A condition node that runs once, after the policy, with a single goal port, empty for none. A tree with no epistemic goal, which is to say a classical plan, succeeds here, since the executor already checks the PDDL goal. Reaching a leaf means one execution finished, not that the goal holds: every leaf was believed to reach it, but that belief was formed against the model at planning time.

The domain side

The problem expert answers what is true and the epistemic state answers what is known. Neither says what the domain declares can be done, and for EPDDL that is a different question from the PDDL one: an action here is an event model with per-agent observability, and no part of that has a PDDL surface. get domain action details on the classical terminal is blind to all of it.

Two services on the same node answer it, named epistemic_domain to keep what is being asked about clear. They live on the node that holds the task because in EPDDL the domain and the problem are one grounded artefact: the event models and the initial model arrive together, and a separate node answering about the first would have to ground the same sources again.

epistemic_domain/get_domain

The agents, the atoms, the grounded actions, and which of those are sensing. Sensing is not a flag a domain sets; it is what having more than one event that can occur amounts to. It also reports the frame, S5 or KD45, and whether any action gives two agents different observability.

epistemic_domain/get_action_details

One action’s event model: every event with its precondition and effects, which of them can actually occur, and a line per agent saying what that agent observes of it.

That last line is the part with no PDDL counterpart. For the corridor mission’s inspect_r1:

r1: sees which event occurred
r2: sees nothing of which event occurred

Two agents equally affected by an action, learning entirely different things from it. The terminal reaches both services through ros2 plansys2 epistemic domain and ros2 plansys2 epistemic action <name>.

Epistemic state

epistemic_state_node runs a lifecycle node named epistemic_state. It is to knowledge what the problem expert is to facts: it holds one pointed Kripke model at a time, for one grounded task, and answers six services. Each of them is reachable from a terminal through ros2 plansys2 epistemic, described in The command line.

Service

Question

epistemic_state/load_task

Be the model of this task. The task is supplied inline as task_json, by path as task_file, or as the EPDDL sources to ground (epddl_domain, epddl_problem, epddl_libraries); loading resets the model to that task’s initial state. The response reports the number of worlds, agents and actions loaded.

epistemic_state/check_formula

Does this formula hold now? The response distinguishes a failure, no task loaded or a formula that does not parse, from the answer itself.

epistemic_state/apply_action

This action ran: perform the DEL product update and report what was observed. The request names the action in the task’s own vocabulary and may carry the observed outcome; the response reports the outcome that occurred and the size of the resulting state.

epistemic_state/get_goal

What is being aimed at, and whether it holds yet. The response says whether the goal is the loaded task’s own or one set since.

epistemic_state/set_goal

Aim at something else, without re-grounding the problem. The formula is parsed against the loaded task, so a goal naming an agent or atom the task does not have is refused here, before it can fail inside a planning request. An empty goal restores the task’s own.

epistemic_state/announce

Everyone just learned that this is true. The model is restricted to the worlds where the formula holds. An announcement that holds nowhere the state considers possible is refused, since applying it would empty the model.

It declares the same four EPDDL parameters as the solver, plus task_file, and publishes on epistemic_state/state with transient-local durability. A task named at configure time is the common case: one mission, one task, loaded before anything asks a question about it. Pointing this node and the planner at the same pair of .epddl files is what keeps the policy and the model it is checked against expressed in one vocabulary. The failure the arrangement is there to prevent is a policy naming an action the state has never heard of.

It is a managed node of the system, not a process started alongside it. Both launch files start it on epistemic_state:=True, and the lifecycle manager configures and activates it with the other four and takes it down with them. It is off by default, since a classical system has no use for it.

It runs as its own process even under the monolithic launch, where the other four share one. That is not an oversight: plansys2_node manages it through LifecycleServiceClient, which needs the node’s name and not its class, so plansys2_bringup links against nothing epistemic and the package every other distribution’s workflow builds is unchanged. The same reasoning as bt_node_plugins on the executor, one level up.

The switch is a launch argument, not a parameter, because it decides two things that must agree: whether the process is started, and whether the manager waits for it. A parameters file can set the second but not the first, so the argument takes precedence over the parameters file.

startup_function in plansys2_lifecycle_manager brings up whatever map it is given, skipping a name it does not find, so one function serve a four-node classical bringup, a five-node epistemic one, and the six-node system that adds perception. The standalone lifecycle_manager_node the distributed launch runs takes the set as its managed_nodes parameter for the same reason; the distributed launch assembles that set in Python, once the launch arguments have values, because naming a node that was not started makes startup block and then fail.

Announcing is the counterpart of the problem expert’s set predicate, and it is deliberately not the same operation. Setting a predicate changes what is true; announcing changes what is known. Every agent that could not previously tell the surviving worlds apart from the ruled-out ones now can, and knows that the others can too, so announcing muddy_c1 in the muddy-children model makes (K c1 muddy_c1) hold when it did not before. Knowledge that reaches only one agent is not a public announcement and cannot be expressed this way; that needs an event model, which an action supplies.

The goal it holds is what the plan solver plans for. The solver subscribes to epistemic_state/state, where the goal travels latched, and replaces the task’s goal with it when the two differ. It subscribes instead of calling a service because planning runs inside the planner’s own service callback, and a service call from there can deadlock a single-threaded executor. Setting goal_from_state to false plans for the problem exactly as written.

When information arrives from outside the plan

A policy is built against a belief. While it runs, its own actions move that belief in ways it has branches for. An announcement is the other case: perception resolves a region, an operator says something, two robots reconcile their maps when a link comes back, and the model moves for a reason the policy never accounted for.

Nothing in a behavior tree can see a service call, so a running policy would carry on against a belief that no longer holds until some later action failed for what looks like an unrelated reason. The state therefore counts the announcements it has taken, publishing the count as belief_version, and CheckBeliefUnchanged reads it.

The packaged action template puts that node inside the reactive sequence, so it is re-checked while the action runs and the interruption arrives when the information does, rather than after the action it invalidated has finished. The executor’s answer to a failed plan is to replan, and the model it replans from is the one the announcement produced.

Only announcements count. A policy that abandoned itself every time one of its own actions worked would never finish anything, so apply_action does not touch the version. A deployment that would rather finish what it started sets the node’s enabled port to false.

When replanning stops getting anywhere

A plan that fails, is replanned, and fails the same way is a loop. The planner is being asked the same question and answering it the same way, and the executor would keep driving the robot into the same failure until someone stopped it. Epistemic missions reach this more easily than classical ones, because EpistemicSwitch fails on any outcome the policy did not plan for, and a domain that cannot express the outcome will not plan for it next time either.

The executor counts replans that arrive with no action having completed since the last one. Counting replans alone would be wrong: a mission that replans often while getting further is healthy, and what marks the loop is replanning with nothing having happened in between. Past max_replans_without_progress, which defaults to three, the plan is aborted through the ordinary failure path rather than replanned again. Setting it to zero asks the executor to keep trying however long the loop runs.

Recovery and this guard pull in opposite directions on purpose. Repairing the model is what lets a contradicted belief be corrected and the next plan be a different one; the guard is what stops the case where it is not.

When the world contradicts the model

The model is a belief about the world, and the world can contradict it. A robot drives out carrying the map’s word that a corridor is clear, looks, and sees that it is blocked. apply_action is then handed an outcome the model holds impossible.

Refusing the update keeps the model honest about never having represented what happened, but it freezes it, and a frozen model is worse than a corrected one: the replan that follows plans from the belief the sensor just contradicted, and routes the robot down the corridor it has looked at.

So allow_recovery on the request repairs the model instead, by trusting the observation over the belief. The model is re-designated to the worlds where the observed event could have fired, and the update proceeds from there. The response reports recovered and what the repair gave up, and the state logs it at warning level, because this is a change of belief rather than a detail of an update.

ApplyEpistemicUpdate asks for recovery by default, on the grounds that a robot which corrects a wrong belief is in better shape than one which stops. Its recover port turns that off for a deployment that would rather fail.

Recovery has a limit worth stating plainly. Re-designation corrects which world is believed actual; it cannot add a world the model never contained. Observing something no world in the model could have produced means the model cannot represent what happened at all, and that is refused whatever allow_recovery says. In the corridor domain, inspecting before driving out is such a case: every world has the robot away from the junction, so there is nothing to re-designate to.

Replanning from the current belief

The same topic carries the model itself, under model, in the shape the task format gives an initial state. The solver reads it and plans from it, which is controlled by initial_from_state and on by default.

This is what makes a replan start from where the mission got to. The executor’s answer to a failed plan is to replan, and an epistemic policy usually fails because EpistemicSwitch saw an outcome the policy did not plan for. Planning again from the model grounding produced would begin at a belief that divergence has already disproved: the robot would be sent to find out what it has just found out, or told to act on a possibility it has ruled out.

The model is resolved against the task before it is used. Every atom, agent and designated world it names must exist there; a name the task lacks is an error and not a new symbol, since it means the state and the planner hold different problems and a policy built from one could not be executed against the other. A model designating no world is refused for the same reason an empty announcement is: every formula holds vacuously in it, so the goal check would report success from a model that says nothing.

Two parameters turn it off, at either end. initial_from_state false on the solver plans from the task’s own initial state. publish_model false on the state node omits the model from the published message, which for a large model is most of it; the solver then falls back to the task’s initial state on its own.

The state advances by executed actions, not by observing the world, which is what distinguishes a belief state from a log. When it disagrees with what the robot observed, the disagreement surfaces at apply_action as an outcome the model cannot account for: a reason to replan, not to overwrite the model quietly.

Which outcome occurred is a question about the world, not about the model. When the state designates a single world it already answers it, and the response reports it. When it designates several the model is undetermined, and the observation has to come from whoever did the sensing.

One agent’s point of view

The state holds one model for the whole system, and that is what makes a group’s knowledge expressible at all: “r1 knows that r2 does not know” is a statement about one structure, not about two. A deployment often needs the other reading, though — what would this robot act on, given only what it has seen — and that is a different model.

epistemic_state/get_agent_perspective returns it: the same worlds and relations, designating what the agent considers possible from the worlds that are actually the case. It comes back in the task format’s initial-state shape, so it can be handed to a planner the same way a replan’s starting model is.

check_formula takes the same view through its agent field. The two readings answer different questions. Asked of the model, blocked is about the corridor; asked of r1, it is about r1, which has no opinion either way until it has looked.

The response also reports whether the agent holds the actual world possible. Under S5 it always does, so an agent can be uninformed but never mistaken. Under KD45 it need not, and that is the difference between an agent that does not know and one that is wrong. The terminal reaches both through ros2 plansys2 epistemic view <agent> and --agent on check.

Perception

The state advances by executed actions, and something still has to tell it what a sensing action saw. plansys2_epistemic_perception is that something: a lifecycle node that watches an nav_msgs/OccupancyGrid, classifies named regions of it, and reports what it found in the vocabulary of the loaded task.

Two things stand between a grid and a formula, and the package exists because neither is mechanical.

The first is a quantifier. A cell is free or occupied; a proposition is about a region, and which quantifier joins them is a modelling decision:

The region is

when

Clear

every cell in it has been observed and settled as free

Blocked

any one cell in it is occupied

Unknown

anything else

Occupied beats unobserved, since no further looking removes an obstacle; unobserved beats free, since a region is clear only when all of it is. A cell that has been seen without being settled, the band between the two thresholds, counts as unobserved, because calling it free would hand an agent knowledge it does not have.

The second is the vocabulary. A region is a place on a map; an atom is a name in a grounded task, and those names are flat: blocked, at-junction_r1. Each region therefore names the atom its occupancy decides and says which way that atom points, so the corridor of the fleet tutorial can be blocked instead of clear_corridor:

epistemic_perception:
  ros__parameters:
    regions: ["corridor"]
    corridor:
      boxes: [2.0, -0.5, 8.0, 0.5, 7.5, 0.5, 8.5, 4.0]
      atom: "blocked"
      atom_true_when_clear: false
      sensing_action: "inspect_r1"
      outcome_when_clear: "e-inspect-clear"
      outcome_when_blocked: "e-inspect-blocked"

Regions are given in metres in the map frame, not in cells, because a map is re-gridded as it grows: the origin moves and the width changes, and a region written in cell indices would come to mean somewhere else without saying so.

How the finding is reported follows from why it is being reported, which is the same distinction announcing draws above. A region bound to a sensing action reports through apply_action: the observation belongs to an action the planner branched on, the state has its event model, and the update is the one the plan accounted for. A region without a binding is announced instead, as information that arrived outside the plan: an operator or two robots reconciling their maps, which is the purpose of a public announcement.

It reports only on change. A map arrives several times a second and says the same thing each time; a repeated announcement is harmless in the model, since restricting to worlds where a formula already holds changes nothing, but a sensing action applied twice is not, and both routes follow the one rule. A region that goes back to undecided produces no call at all: the state has no operation for taking knowledge back.

It is brought up the way the state is. Both launch files start it on epistemic_perception:=True, as a sixth managed node, and the lifecycle manager configures and activates it with the others. It is off by default, and it starts watching nothing until the parameters file names regions under epistemic_perception:.

Asking for it without epistemic_state:=True is refused. Every route perception has for reporting is a call on the state, so the pair is not a degraded system but one that cannot work at all, and both the distributed launch and plansys2_node say so instead of bringing up a node with nowhere to report. As with the state, plansys2_bringup manages it by name over services and links against none of it.

Regions are declared at configure time, not at construction, since there is no list of regions before the parameters are read. Configure is reachable more than once, so the node declares each region’s settings only if they are not already declared, and on_cleanup releases the subscription and the service client that the configuration created. A box is accepted as an integer array as well as a double array, because [2, -1, 8, 1] in a YAML file is a list of integers and a reader writing metres has no reason to expect otherwise.

Executor configuration

Two parameters connect the executor to all of this:

executor:
  ros__parameters:
    bt_builder_plugin: "EpistemicBTBuilder"
    bt_node_plugins: ["libplansys2_epistemic_bt_nodes.so"]

bt_node_plugins is a generic hook: the executor loads BehaviorTree.CPP node libraries before building a tree. That is what keeps plansys2_executor free of any dependency on the epistemic stack. It never links against the epistemic nodes, it loads them, and the nodes find the policy on the blackboard where the executor already publishes the plan it is running.

Package boundaries

Package

Contents

Depends on plansys2_executor

plansys2_epddl_grounder

The EPDDL front end: runs plank, caches the result

No

plansys2_epistemic_msgs

The three service definitions

No

plansys2_epistemic_planner

The solver plugin over Aletheia’s planning core, policy and belief state serialisation

No

plansys2_epistemic_executor

Policy, tree rendering, the four nodes, the state node

No

plansys2_epistemic_perception

Regions of a map, and their translation into calls on the state

No

plansys2_epistemic_bt_builder

The BTBuilder plugin

Yes

plansys2_aletheia_plan_solver

The subprocess plan solver plugin

No

plansys2_tui_cli

The ros2 plansys2 verbs and the Textual dashboard

No

eplansys

Metapackage; dependencies only, no code

No

The builder is a separate package because it is the only piece that needs the executor’s plugin API. Keeping it apart lets everything else build and be tested against a released distribution as well as against this fork.

Within plansys2_epistemic_executor the same boundary is drawn once more. The policy and its rendering as a behavior tree are built as a separate library, plansys2_epistemic_executor_policy, which depends on nothing but the message package: they are the shared reading of a policy, and keeping them free of ROS lets them be tested as the pure transformation they are.