Directory Handler — LangGOAP Tier 1 Primer#

A minimal end-to-end GOAP loop, adapted from GOApy’s directory_handler example.

What this notebook shows#

  • How to describe a tiny world with boolean state keys.

  • How to wrap real side-effecting functions as ActionSpec objects.

  • How GoapGraph.invoke() runs the full plan → execute → observe loop.

  • How the planner skips unnecessary work when the goal is already partially satisfied.

Scenario#

We want a workspace directory to exist and contain a .token marker file. The planner has two actions at its disposal:

Action

Preconditions

Effects

create_workspace

workspace_exists=False

workspace_exists=True

create_token

workspace_exists=True, token_exists=False

token_exists=True

A* discovers that create_workspace must come before create_token because the latter’s preconditions include workspace_exists=True. The notebook uses a tempfile.TemporaryDirectory so it can run repeatedly without touching your filesystem.

import tempfile
from pathlib import Path

from tutorial_examples.directory_handler import (
    directory_handler_actions,
    initial_world_state,
)

from langgoap import GoalSpec, GoapGraph, successful_action_names

Action definitions#

The executable bodies live in examples/tutorials/tutorial_examples/directory_handler.py — the same module used by the integration test, so this notebook and the test are exercising identical code.

def create_workspace(ws: dict[str, Any]) -> dict[str, Any]:
    workspace = Path(ws["workspace"])
    workspace.mkdir(parents=True, exist_ok=True)
    return {"workspace_exists": True, "token_exists": _token_exists(workspace)}


def create_token(ws: dict[str, Any]) -> dict[str, Any]:
    workspace = Path(ws["workspace"])
    (workspace / ".token").touch(exist_ok=True)
    return {"token_exists": True}

GOAP Execution Graph#

The planner discovers a plan, the executor runs each action, and the observer checks progress — replanning automatically if something fails.

from IPython.display import Image, display

tmp_graph = tempfile.TemporaryDirectory()
graph = GoapGraph(actions=directory_handler_actions(Path(tmp_graph.name) / "ws"))
display(Image(graph.compile().get_graph().draw_mermaid_png()))
../../_images/d203e373178a0dc06fff2c111d0586811c8dcb2466c9a8c1d741df7bb3c2bb7d.png

Running the loop on an empty workspace#

With nothing in the temporary directory yet, the planner must run both actions in order.

tmp = tempfile.TemporaryDirectory()
workspace = Path(tmp.name) / "goap_workspace"

actions = directory_handler_actions(workspace)
result = GoapGraph(actions=actions).invoke(
    goal=GoalSpec(conditions={"workspace_exists": True, "token_exists": True}),
    world_state=initial_world_state(workspace),
)

print(f"Status:         {result['status']}")
print(f"Workspace dir:  {workspace.is_dir()}")
print(f"Token file:     {(workspace / '.token').is_file()}")

successful = successful_action_names(result)
print(f"Plan executed:  {' → '.join(successful)}")

Partially-satisfied goal#

If the workspace directory already exists, the planner should skip create_workspace and run only create_token. This is the key advantage of goal-oriented planning over hard-coded scripts — the loop automatically adapts to the current world state.

tmp2 = tempfile.TemporaryDirectory()
workspace2 = Path(tmp2.name) / "goap_workspace"
workspace2.mkdir()  # pre-create the workspace directory

actions = directory_handler_actions(workspace2)
result = GoapGraph(actions=actions).invoke(
    goal=GoalSpec(conditions={"workspace_exists": True, "token_exists": True}),
    world_state=initial_world_state(workspace2),
)

print(f"Status:         {result['status']}")
successful = successful_action_names(result)
print(f"Plan executed:  {successful}")
print(f"Token file:     {(workspace2 / '.token').is_file()}")

Goal already fully satisfied#

When the world already matches the goal, A* returns an empty plan and the executor runs nothing. Ambient idempotency is a nice side-effect of goal-driven planning.

tmp3 = tempfile.TemporaryDirectory()
workspace3 = Path(tmp3.name) / "goap_workspace"
workspace3.mkdir()
(workspace3 / ".token").touch()

actions = directory_handler_actions(workspace3)
result = GoapGraph(actions=actions).invoke(
    goal=GoalSpec(conditions={"workspace_exists": True, "token_exists": True}),
    world_state=initial_world_state(workspace3),
)

print(f"Status:         {result['status']}")
successful = successful_action_names(result)
print(f"Plan executed:  {successful}")

Visualizing the plan#

Once the planner has produced a plan, we can render it in ASCII, Mermaid, or Graphviz DOT. In Jupyter, plan.visualize(format="auto") returns an IPython.display.Markdown that renders Mermaid inline.

# Plan the empty-workspace scenario directly to inspect the plan object
from langgoap.planner.astar import plan as astar_plan
from langgoap.state import PlanningState

tmp4 = tempfile.TemporaryDirectory()
workspace4 = Path(tmp4.name) / "goap_workspace"
actions = directory_handler_actions(workspace4)

state = PlanningState.from_dict(initial_world_state(workspace4))
plan_obj = astar_plan(
    state,
    GoalSpec(conditions={"workspace_exists": True, "token_exists": True}),
    actions,
)
print(plan_obj.to_ascii())
Plan (2 steps, cost=2)
├── [0] create_workspace
├── [1] create_token  (after: [0])

Summary#

You’ve just seen the simplest possible end-to-end LangGOAP loop:

  1. Describe world state as a plain dict[str, Any].

  2. Wrap your side-effecting functions in ActionSpec objects that declare preconditions and effects.

  3. Call GoapGraph(actions).invoke(goal, world_state) and let the planner discover the execution order.

The planner adapts to partially-satisfied goals, and Plan.to_ascii() lets you inspect what it produced. Everything in this notebook is verified by tests/integration/test_directory_handler.py.