Skip to content

Cpp Fluent API

nick_tsaizer edited this page Apr 25, 2026 · 5 revisions

The C++ wrapper provides a typed, readable layer over the C ABI.

Main header:

#include "tensor_planner.hpp"

Namespace:

tp

Why use the C++ wrapper?

Use it when you want:

  • real C++ object types instead of integer type IDs,
  • fluent action definitions,
  • exceptions instead of manual status checks,
  • plan-step arguments mapped back to typed object pointers.

Minimal C++ planning task

#include "tensor_planner.hpp"

#include <string>

struct Character { std::string name; };
struct Location { std::string name; };

int main() {
  Character player{"player"};
  Location home{"home"};
  Location forest{"forest"};

  tp::Planner planner;

  auto at = planner.predicate<Character, Location>("at");
  auto connected = planner.predicate<Location, Location>("connected");
  auto energy = planner.function<Character>("energy");

  auto move = planner.action("move")
    .param<Character>("who")
    .param<Location>("from")
    .param<Location>("to")
    .require(at("who", "from"))
    .require(connected("from", "to"))
    .require(energy("who") >= 1.0f)
    .removes(at("who", "from"))
    .adds(at("who", "to"))
    .decreases(energy("who"), 1.0f)
    .commit();

  auto state = planner.state()
    .object(player)
    .object(home)
    .object(forest)
    .fact(at(player, home))
    .edge(connected, home, forest)
    .value(energy(player), 2.0f)
    .goal(at(player, forest));

  tp::SolveResult result = planner.solve(state);
  for (const tp::PlanStep &step : result.steps()) {
    if (step.is(move)) {
      Character *who = step.arg<Character>("who");
      Location *from = step.arg<Location>("from");
      Location *to = step.arg<Location>("to");
    }
  }
}

Planner limits

Defaults are small and useful for small tasks:

tp::Planner planner(tp::Limits{
  .max_objects = 32,
  .max_facts = 256,
  .max_goals = 8,
  .max_candidates = 512,
  .max_expansions = 20000,
  .max_plan_length = 64,
});

Increase max_candidates, max_expansions, and max_plan_length for wider domains.

Predicates

Create typed predicates with template arguments:

auto at = planner.predicate<Character, Location>("at");
auto has = planner.predicate<Item>("has");
auto recipe = planner.predicate<Item, Item>("recipe2_a");

Use parameter names when defining action schemas:

at("agent", "from")

Use real objects when defining state facts and goals:

at(player, home)

The wrapper validates arity and object types.

Actions

Action definitions are fluent:

auto gather = planner.action("gather_free")
  .param<Agent>("agent")
  .param<Item>("item")
  .param<Location>("where")
  .require(at("agent", "where"))
  .require(gather_at("item", "where"))
  .require(gather_bare_hands("item"))
  .adds(has("item"))
  .commit();

Use .removes(...) for delete effects and .adds(...) for add effects.

Numeric functions use typed terms too:

auto energy = planner.function<Character>("energy");

planner.action("act")
  .param<Character>("who")
  .require(energy("who") >= 1.0f)
  .decreases(energy("who"), 1.0f)
  .commit();

Supported numeric comparisons are <, <=, ==, >=, and >. Numeric effects use .sets(...), .increases(...), and .decreases(...).

State builder

Register objects, then add facts and goals:

auto state = planner.state()
  .object(agent)
  .object(home)
  .object(forest)
  .object(flint)
  .fact(at(agent, home))
  .edge(connected, home, forest)
  .value(energy(agent), 2.0f)
  .fact(gather_at(flint, forest))
  .goal(has(flint));

Use .edge(predicate, a, b) for undirected binary relations. It adds both facts:

state.edge(connected, home, forest);
// equivalent to:
state.fact(connected(home, forest));
state.fact(connected(forest, home));

Use .fact(...) directly for one-way relations.

Objects are borrowed. Keep them alive while using the solve result.

Reading results

tp::SolveResult result = planner.solve(state);

if (result.solved()) {
  for (const tp::PlanStep &step : result.steps()) {
    if (step.is(gather)) {
      Agent *agent = step.arg<Agent>("agent");
      Item *item = step.arg<Item>("item");
      Location *where = step.arg<Location>("where");
    }
  }
}

Useful result data:

  • result.solved()
  • result.status()
  • result.expansions()
  • result.generated()
  • result.steps()

Scaling up

The same API shape works when a domain has many actions and many possible valid sequences. Add reusable action schemas, put current facts into the state, and solve for the goal instead of branching through all combinations manually.

Clone this wiki locally