pleach
Build

Plans

PlanManager — multi-step plans with stable step IDs, revision history, and a next-step picker that respects dependencies.

Plans are one half of the cross-session state thematic island — paired with memory. Plans span turns within or across sessions; memory spans sessions. Both outlive a single session arc; neither fits the runtime-lifecycle cluster.

A Plan is the variable surface for work that spans more than one turn: the steps the agent committed to, which session executed each one, what evidence each one produced, and the revision-by-revision history of how the plan changed. The substrate doesn't pick the steps — the application's planner does — but it owns the persistence shape, the dependency-aware step picker, and the revision audit trail. See Lineage for how plans join sessions, and Subagents for how a step's suggestedAgent routes to a spec.

Ships from @pleach/core/plans. PlanManager is the orchestrator; Plan, PlanStep, and PlanRevision are the wire shapes.

import {
  PlanManager,
  type Plan,
  type PlanStep,
  type PlanRevision,
} from "@pleach/core/plans";

Plan shape

FieldTypeNotes
planIdstring10-char id, assigned at create
titlestringHuman-readable
objectivestringHigh-level goal
statusenumactive / completed / paused / abandoned
stepsPlanStep[]Ordered
createdAtISO-8601UTC
createdInSessionstringSession that created the plan
createdInChatstringChat that created the plan
updatedAtISO-8601Bumped on every revision
revisionnumberMonotonic; bumped on every mutation
tagsstring[]Optional categorization

status flips to completed automatically when every step is completed or skippedPlanManager.updateStep moves the record from the active namespace to the completed namespace at that boundary.

PlanStep shape

FieldTypeNotes
stepIdstringStable across revisions — step_ + 8-char id
titlestringShort label
descriptionstringWhat this step should accomplish
statusenumpending / active / completed / skipped / failed
executedInSessionstring | undefinedSession that ran this step
executedInChatstring | undefinedChat that ran this step
suggestedAgentstring | undefinedRecommended spec name
suggestedToolsstring[] | undefinedExpected tool calls
dependsOnstring[] | undefinedOther stepIds that must complete first
completionEvidenceobject | undefinedcanvasCards, jobIds, summary
updatedAtISO-8601Bumped on every step mutation

stepId stability is the load-bearing invariant — revisePlan preserves completionEvidence, executedInSession, and executedInChat for steps whose stepId survives the revision, even if their position or surrounding steps change. Steps without a passed-in stepId get a new one and start clean.

PlanManager methods

MethodReturnsEffect
createPlan(input)PlanAssigns planId, stamps revision 1, writes the initial PlanRevision snapshot
updateStep(planId, stepId, update, sessionId)PlanMutates one step; auto-flips plan to completed when all steps are completed or skipped
revisePlan(planId, revision, sessionId)PlanReplace title, objective, or the full step list; rejects revisions to completed plans
getActivePlans()Plan[]Up to 50
getCompletedPlans()Plan[]Up to 50
getPlan(planId)Plan | nullChecks active first, then completed
getRevisions(planId)PlanRevision[]Sorted ascending by revision
getNextStep(plan)PlanStep | nullFirst pending step whose dependsOn are all completed or skipped
buildActivePlanSection(plans)stringRenders an active-plan summary block for system-prompt injection

getNextStep is pure; the rest read or write through the BaseStore the manager was constructed with.

Revision history

Every mutation writes a PlanRevision to a per-plan namespace alongside the plan itself.

FieldTypeNotes
revisionnumberMatches Plan.revision at the time of the snapshot
snapshotPlanstructuredClone of the plan at this revision
changeDescriptionstringFree-text; updateStep writes Step "<title>" → <status>
sessionIdstringWhich session made the change
timestampISO-8601UTC

Revisions are append-only. getRevisions(planId) returns them sorted ascending so a UI can render the plan's evolution as a timeline without re-sorting.

Example

import { PlanManager } from "@pleach/core/plans";
import { MyStore } from "./my-store";

const plans = new PlanManager(new MyStore(), orgId, userId);

const plan = await plans.createPlan({
  title:     "Q3 vendor evaluation",
  objective: "Pick a vendor for the queue rewrite.",
  steps: [
    { title: "List candidates",  description: "Pull from prior research." },
    { title: "Run benchmarks",   description: "p99 + cost.",
      dependsOn: [] /* filled in after createPlan assigns stepIds */ },
    { title: "Write recommendation", description: "1-pager." },
  ],
  sessionId: session.id,
  chatId:    chat.id,
});

// Pick what to do next — dependency-aware.
const next = plans.getNextStep(plan);
if (next) {
  await plans.updateStep(
    plan.planId,
    next.stepId,
    { status: "active", executedInSession: session.id, executedInChat: chat.id },
    session.id,
  );
}

Revising mid-flight

The reader can see how revisePlan carries stepIds forward so completed work survives a re-plan.

const current = await plans.getPlan(plan.planId);
if (!current) throw new Error("plan vanished");

const revised = await plans.revisePlan(
  current.planId,
  {
    steps: [
      // Pass existing stepIds back in to preserve their evidence + executedIn* fields.
      ...current.steps.filter((s) => s.status === "completed"),
      { title: "Cross-check sources", description: "Verify two corroborating refs." },
      { title: "Write recommendation", description: "1-pager." },
    ],
    changeDescription: "Inserted cross-check after benchmark results came in.",
  },
  session.id,
);

Steps without a stepId get a fresh one and start pending. Steps whose stepId survives the revision keep completionEvidence, executedInSession, and executedInChat even if their position shifts.

Walking revision history

The snippet renders a plan's evolution by reading revisions ascending.

const history = await plans.getRevisions(plan.planId);

for (const rev of history) {
  console.log(
    `r${rev.revision}`,
    rev.changeDescription,
    `(${rev.snapshot.steps.length} steps)`,
  );
}

changeDescription is the free-text label updateStep and revisePlan write; the snapshot is a full structuredClone of the plan at that revision, so a UI can diff one snapshot against the next without re-querying.

Where to go next

On this page