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.
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 |
|---|---|---|
|
empty |
Path to the EPDDL domain. With |
|
empty |
Path to the EPDDL problem. |
|
empty |
Action-type libraries the domain declares. Empty supplies the
|
|
empty |
Path to the plank binary. Empty takes |
|
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. |
|
empty |
Pins a heuristic: |
|
empty |
Pins a search strategy: |
|
empty |
Path to a JSON selection policy replacing the built-in rule table. |
|
empty |
Path to a JSON map from grounded epistemic action names to PlanSys2 action expressions. Empty falls back to the naming convention. |
|
|
What to do with a branching solution: |
|
|
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. |
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 |
|---|---|
|
One continuation per possible outcome, by item index. |
|
The observation selecting each, in the same order. |
|
Whether the outcome has to be observed at all. |
|
The epistemic conditions the action needs. |
|
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.
CheckKnowledgeA condition node, the counterpart of
CheckOverAllReqfor 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, andaction, the action identifier used in the error message.ApplyEpistemicUpdateThe 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, andobserved, the outcome the performer observed, which the packaged action template binds toExecuteAction’s outcome port and which is left empty to let the state decide. Output port:outcome, the outcome that occurred, which the followingEpistemicSwitchreads. The two are separate blackboard entries:observedholds the performer’s report andoutcomeholds the state’s reading of it.EpistemicSwitchA 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, andoutcomes, 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.CheckBeliefUnchangedA 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:
actionfor the message, andenabled, false for a deployment that would rather not be interrupted.CheckEpistemicGoalA condition node that runs once, after the policy, with a single
goalport, 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_domainThe 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_detailsOne 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 |
|---|---|
|
Be the model of this task. The task is supplied inline as |
|
Does this formula hold now? The response distinguishes a failure, no task loaded or a formula that does not parse, from the answer itself. |
|
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. |
|
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. |
|
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. |
|
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 |
|---|---|
|
every cell in it has been observed and settled as free |
|
any one cell in it is occupied |
|
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 |
|---|---|---|
|
The EPDDL front end: runs plank, caches the result |
No |
|
The three service definitions |
No |
|
The solver plugin over Aletheia’s planning core, policy and belief state serialisation |
No |
|
Policy, tree rendering, the four nodes, the state node |
No |
|
Regions of a map, and their translation into calls on the state |
No |
|
The |
Yes |
|
The subprocess plan solver plugin |
No |
|
The |
No |
|
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.