npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

htn-plan

v1.2.1

Published

A pure TypeScript Hierarchical Task Network (HTN) planner

Readme

htn-plan

Mission Planning: The Hierarchical Task Network (HTN) Engine
A pure TypeScript library that decomposes vague, high-level goals into a flat, ordered list of executable micro-tasks.

License: MIT TypeScript Tests


Why an HTN instead of a Finite State Machine?

When you give a system a massive, vague goal ("Clean the house", "Write a German learning app"), a Finite State Machine breaks — it's too rigid to handle open-ended decomposition.

Autonomous systems instead use an HTN:

| Concept | Description | |---|---| | Compound Task | An abstract goal that cannot be executed directly (e.g. "FetchCoffee"). | | Method | A recipe that decomposes one Compound Task into an ordered list of Sub-Tasks. Multiple methods can exist for the same task; the first one whose precondition passes is used. | | Operator (Primitive Task) | A directly executable action with a precondition and an effect on the world state. |

The engine takes a high-level goal, recursively breaks it down through Methods, and returns a flat, chronologically ordered array of Operators ready for execution.


Example Output

Given a single high-level goal, the engine produces a fully decomposed, flat execution plan:

Goal: "FetchCoffee"

FetchCoffee
 └─ StandardFetch
     ├─ MoveToKitchen   ← Operator
     ├─ PourCoffee      ← Operator
     └─ ReturnToStart   ← Operator

Flat execution plan → [MoveToKitchen, PourCoffee, ReturnToStart]

Use Cases

🤖 Robotics

Goal: "FetchCoffee"

FetchCoffee → [GoToKitchen, PourCoffee, ReturnToStart]
GoToKitchen → [TurnLeft, MoveForward5m, AvoidTable]

🧠 AI Agent

The exact same engine, different domain:

Goal: "WriteCode"

WriteCode  → [ResearchAPI, DraftCode, WriteTests]
ResearchAPI → [SearchGoogle, ReadDocs, Summarize]

One engine. Any domain. Zero coupling to the real world.

🧠 LLM Orchestration (Neuro-Symbolic AI)

LLMs are great at reasoning but terrible at long-term rigid planning. Use the LLM to translate natural language into your HTN state, and use htn-plan to strictly orchestrate the LLM's tool calls without hallucinations.

User: "Research quantum computing and write a summary"

LLM translates → HTN state: { topic: "quantum computing", hasSummary: false }
HTN plan       → [SearchWeb, ReadSources, ExtractKeyPoints, WriteSummary]

Every tool call is gated by a precondition — no hallucinated steps, no skipped actions.

Installation

npm install htn-plan

Quick Start

import { createPlanner } from 'htn-plan';
import type { Domain } from 'htn-plan';

// 1. Define your world state shape
interface RobotState {
  location: string;
  hasItem: boolean;
  batteryLevel: number;
}

// 2. Build your domain (operators + compound tasks)
const domain: Domain<RobotState> = {
  operators: {
    MoveToKitchen: {
      name: 'MoveToKitchen',
      condition: (s) => s.batteryLevel > 0 && s.location !== 'Kitchen',
      effect:    (s) => ({ ...s, location: 'Kitchen' }),
    },
    PourCoffee: {
      name: 'PourCoffee',
      condition: (s) => s.location === 'Kitchen' && !s.hasItem,
      effect:    (s) => ({ ...s, hasItem: true }),
    },
    ReturnToStart: {
      name: 'ReturnToStart',
      condition: (s) => s.hasItem,
      effect:    (s) => ({ ...s, location: 'Start' }),
    },
  },
  compoundTasks: {
    FetchCoffee: {
      name: 'FetchCoffee',
      methods: [
        {
          name: 'StandardFetch',
          condition: (_s) => true,
          subtasks: ['MoveToKitchen', 'PourCoffee', 'ReturnToStart'],
        },
      ],
    },
  },
};

// 3. Create a planner and run it
const result = createPlanner({
  domain,
  initialState: { location: 'Hall', hasItem: false, batteryLevel: 100 },
  goals: ['FetchCoffee'],
}).plan();

// 4. Inspect the result
if (result.success) {
  result.plan.forEach((op) => console.log(op.name));
  // → MoveToKitchen
  // → PourCoffee
  // → ReturnToStart
} else {
  console.error(result.reason, result.failedTask);
}

Core API

createPlanner(config)

Creates a planner instance and returns an object with a single plan() method.

| Parameter | Type | Description | |---|---|---| | config.domain | Domain<TState> | All operators and compound tasks available to the planner. | | config.initialState | TState | The world state before planning begins. Never mutated. | | config.goals | ReadonlyArray<string> | Top-level task names to achieve, resolved left-to-right. |

Returns PlanningResult<TState> — a discriminated union:

// Success
{ success: true;  plan: ReadonlyArray<Operator<TState>> }

// Failure
{ success: false; reason: PlanningFailureReason; failedTask: string }

Failure reasons: "UNKNOWN_TASK" | "OPERATOR_PRECONDITION_FAILED" | "NO_APPLICABLE_METHOD"


Domain<TState> — Fluent Builder

Instead of constructing the plain Domain object literal shown in Quick Start, you can use the Domain class for a chainable, incremental registration API:

import { Domain, createPlanner } from 'htn-plan';

const domain = new Domain<RobotState>()
  .registerOperator({
    name: 'MoveToKitchen',
    condition: (s) => s.batteryLevel > 0 && s.location !== 'Kitchen',
    effect:    (s) => ({ ...s, location: 'Kitchen' }),
  })
  .registerOperator({
    name: 'PourCoffee',
    condition: (s) => s.location === 'Kitchen' && !s.hasItem,
    effect:    (s) => ({ ...s, hasItem: true }),
  })
  .registerOperator({
    name: 'ReturnToStart',
    condition: (s) => s.hasItem,
    effect:    (s) => ({ ...s, location: 'Start' }),
  })
  .registerMethod('FetchCoffee', {
    name: 'StandardFetch',
    condition: () => true,
    subtasks: ['MoveToKitchen', 'PourCoffee', 'ReturnToStart'],
  });

// Pass the Domain instance directly — it implements the Domain<TState> interface
const result = createPlanner({
  domain,
  initialState: { location: 'Hall', hasItem: false, batteryLevel: 100 },
  goals: ['FetchCoffee'],
}).plan();

| Method | Returns | Description | |---|---|---| | .registerOperator(operator) | this | Adds (or overwrites) a primitive task. | | .registerMethod(taskName, method) | this | Appends a decomposition method to a compound task (created on first use). |

Domain.validate()

Eagerly checks that every subtask name referenced in all registered methods resolves to a known operator or compound task. Call this once after building the domain to surface broken references (e.g. typos) before running the planner.

import { Domain, DomainValidationError } from 'htn-plan';

try {
  const domain = new Domain<RobotState>()
    .registerOperator({ name: 'Move', condition: () => true, effect: (s) => s })
    .registerMethod('FetchCoffee', {
      name: 'StandardFetch',
      condition: () => true,
      subtasks: ['Move', 'PourCoffee'], // 'PourCoffee' not yet registered
    })
    .validate(); // throws DomainValidationError: "PourCoffee" is unresolved
} catch (err) {
  if (err instanceof DomainValidationError) {
    console.error(`Unresolved subtask: ${err.unresolvedTask}`);
  }
}

Observability Hooks

Pass a hooks object to createPlanner to trace every planning decision. Useful for debugging complex domains, collecting metrics, or powering a visual plan inspector.

import { createPlanner } from 'htn-plan';
import type { PlannerHooks } from 'htn-plan';

const hooks: PlannerHooks<RobotState> = {
  onTaskExpand:    (name, depth)         => console.log(`[${'  '.repeat(depth)}] expand: ${name}`),
  onMethodTry:     (task, method, depth) => console.log(`[${'  '.repeat(depth)}] try: ${task}/${method}`),
  onBacktrack:     (task, method, depth) => console.log(`[${'  '.repeat(depth)}] backtrack: ${task}/${method}`),
  onOperatorApply: (name, before, after) => console.log(`apply: ${name}`, { before, after }),
};

const result = createPlanner({
  domain,
  initialState: { location: 'Hall', hasItem: false, batteryLevel: 100 },
  goals: ['FetchCoffee'],
  hooks,
}).plan();

| Hook | Signature | Called when | |---|---|---| | onTaskExpand | (taskName, depth) => void | Any task (operator or compound) is dequeued | | onMethodTry | (taskName, methodName, depth) => void | A decomposition method is attempted | | onBacktrack | (taskName, methodName, depth) => void | A method branch fails and the planner backtracks | | onOperatorApply | (operatorName, stateBefore, stateAfter) => void | An operator's effect is applied |


Error Classes

import { PlannerMaxDepthError, DomainValidationError } from 'htn-plan';

| Class | Thrown by | Reason | |---|---|---| | PlannerMaxDepthError | createPlanner().plan() | Recursion depth exceeded (cyclic decomposition) | | DomainValidationError | Domain.validate() | A subtask references an unregistered task |

Both extend Error and have name set to their class name for easy instanceof checks.


// World state — any plain object you define
type State<TState> = TState;

// Directly executable action
interface Operator<TState> {
  readonly name: string;
  condition: (state: TState) => boolean;   // precondition check
  effect:    (state: TState) => TState;    // must return a NEW state (no mutation)
}

// One decomposition recipe for a compound task
interface Method<TState> {
  readonly name: string;
  condition: (state: TState) => boolean;   // when is this decomposition valid?
  subtasks:  ReadonlyArray<string>;        // ordered list of sub-task names
}

// An abstract goal with one or more methods
interface CompoundTask<TState> {
  readonly name: string;
  methods: ReadonlyArray<Method<TState>>;  // tried in order; first valid wins
}

// The complete problem description passed to createPlanner()
interface Domain<TState> {
  operators:     Record<string, Operator<TState>>;
  compoundTasks: Record<string, CompoundTask<TState>>;
}

How the Planner Works

The engine implements a Depth-First Search (DFS) with backtracking:

  1. Take the first task from the queue.
  2. If it is an Operator: check its precondition against the current simulated state. If it passes, apply the effect, add the operator to the plan, and continue with the remaining tasks.
  3. If it is a Compound Task: iterate through its methods in order. For each method whose condition passes, inline its subtasks at the front of the queue and recurse.
  4. If a branch leads to a dead-end (precondition fails deep in the tree), backtrack and try the next method.
  5. Return the first complete plan found, or a failure descriptor when all branches are exhausted.

State is never mutated — each recursive call receives a fresh copy of the world state.


Project Structure

htn-plan/
├── src/
│   ├── types.ts        # All TypeScript type definitions (blueprints)
│   ├── planner.ts      # HTN solver (DFS + backtracking)
│   ├── index.ts        # Public API re-exports
│   └── __tests__/
│       ├── types.test.ts    # Compile-time type checks
│       └── planner.test.ts  # Runtime behaviour & backtracking tests
├── jest.config.js
├── tsconfig.json
└── package.json

Development

Install dependencies

npm install

Run tests (TDD)

npm test

Run tests in watch mode

npm run test:watch

Type-check without emitting

npm run lint

Build

npm run build

Design Principles

  • Pure TypeScript — no runtime dependencies, fully typed generics.
  • Immutable state — operators must return a new state; the planner never mutates the state you pass in.
  • Domain-agnostic — the engine knows nothing about the real world. You wire it up through a Domain object.
  • TDD-first — the type definitions and solver were written test-first; all edge cases (backtracking, empty goals, unknown tasks) are covered by the test suite.

License

MIT © franruedaesq