Skip to main content

Overview

The Planner implements the root-level planning loop that drives the entire orchestrator. It uses an LLM to continuously generate tasks, monitors their execution, and adapts based on worker handoffs. Location: packages/orchestrator/src/planner.ts

Class: Planner

Constructor

new Planner(
  config: OrchestratorConfig,
  plannerConfig: PlannerConfig,
  taskQueue: TaskQueue,
  workerPool: WorkerPool,
  mergeQueue: MergeQueue,
  monitor: Monitor,
  systemPrompt: string,
  subplanner?: Subplanner,
  deps?: Partial<PlannerDeps>
)
config
OrchestratorConfig
required
Orchestrator configuration including LLM, git, and sandbox settings
plannerConfig
PlannerConfig
required
Planner-specific config with maxIterations
systemPrompt
string
required
System prompt loaded from prompts/root-planner.md
subplanner
Subplanner
Optional subplanner for recursive task decomposition

Core Methods

runLoop()

The main planning loop that runs until all work is complete or max iterations reached.
async runLoop(request: string): Promise<void>
request
string
required
User’s high-level request (e.g., “Build Minecraft according to SPEC.md”)
Behavior:
  1. Initializes Pi agent session with system prompt
  2. Enters iteration loop with configurable sleep interval (500ms)
  3. Collects completed worker handoffs
  4. Triggers planning when:
    • First iteration (iteration === 0)
    • Enough handoffs received (>= 3 since last plan)
    • No active work remaining
  5. Dispatches tasks to workers or subplanner based on scope complexity
  6. Implements exponential backoff on consecutive errors (max 10)
  7. Waits for all active tasks to complete before exit
Planning Triggers:
  • MIN_HANDOFFS_FOR_REPLAN = 3 - Minimum handoffs before replanning
  • Adaptive planning allows the LLM to control batch sizing

plan()

Single planning iteration that prompts the LLM and parses task responses.
async plan(
  request: string,
  repoState: RepoState,
  newHandoffs: Handoff[]
): Promise<Task[]>
request
string
required
Original user request
repoState
RepoState
required
Current repository state (file tree, commits, SPEC.md, etc.)
newHandoffs
Handoff[]
required
Worker handoffs since last planning iteration
Returns: Array of parsed Task objects Delta Optimization: The planner tracks previous state to avoid re-sending unchanged context:
  • previousFileTree - Only sends new/removed files
  • previousFeaturesHash - Only includes FEATURES.json if changed
  • previousDecisionsHash - Only includes DECISIONS.md if changed
This reduces prompt size by ~40K chars per iteration on average.

injectTask()

Injects tasks directly into the dispatch pipeline without LLM planning.
injectTask(task: Task): void
task
Task
required
Task to inject (used for conflict fixes and reconciler-generated tasks)
Use Cases:
  • Merge conflict resolution tasks from mergeQueue.onConflict
  • Build/test fix tasks from reconciler.onSweepComplete

Task Dispatch

Scope-Based Routing

Tasks are routed based on scope complexity:
if (shouldDecompose(task, DEFAULT_SUBPLANNER_CONFIG, 0)) {
  // Route to subplanner for recursive decomposition
  handoff = await this.subplanner.decomposeAndExecute(task, 0, span)
} else {
  // Route directly to ephemeral worker
  handoff = await this.workerPool.assignTask(task)
}
Decomposition Threshold: Tasks with scope.length >= 4 files

Scope Tracking

Prevents conflicting concurrent edits:
const overlaps = this.scopeTracker.getOverlaps(task.id, task.scope)
if (overlaps.length > 0) {
  logger.warn('Scope overlap detected', { overlaps })
}
this.scopeTracker.register(task.id, task.scope)

Task Retry

Failed tasks are automatically retried:
const MAX_TASK_RETRIES = 1

if ((task.retryCount ?? 0) < MAX_TASK_RETRIES) {
  const retried = this.taskQueue.retryTask(task.id)
  if (retried) {
    this.dispatchSingleTask(task)
  }
}

Message Builders

buildInitialMessage()

Constructs the first LLM prompt with full repository context.
private buildInitialMessage(
  request: string,
  repoState: RepoState
): string
Includes:
  • User request
  • SPEC.md (product specification)
  • FEATURES.json (feature dependency graph)
  • AGENTS.md (coding conventions)
  • DECISIONS.md (architecture decisions)
  • Complete file tree
  • Recent commits (last 40)

buildFollowUpMessage()

Constructs incremental updates for subsequent iterations.
private buildFollowUpMessage(
  repoState: RepoState,
  newHandoffs: Handoff[]
): string
Includes:
  • File tree delta (new/removed files only)
  • Recent commits
  • Updated FEATURES.json (if changed)
  • Updated DECISIONS.md (if changed)
  • New worker handoffs with:
    • Task status (complete/failed/blocked)
    • Summary (max 2000 chars)
    • Files changed (max 10 files)
    • Concerns and suggestions
  • Currently active tasks
  • Merge queue health (success rate, conflicts)
  • Locked file scopes
  • Build/test health from reconciler

Callbacks

onTaskCreated()

onTaskCreated(callback: (task: Task) => void): void
Called when a new task is created by the LLM.

onTaskCompleted()

onTaskCompleted(callback: (task: Task, handoff: Handoff) => void): void
Called when a worker completes a task.

onIterationComplete()

onIterationComplete(
  callback: (iteration: number, tasks: Task[], handoffs: Handoff[]) => void
): void
Called after each planning iteration.

onError()

onError(callback: (error: Error) => void): void
Called on planning failures.

Configuration

PlannerConfig

interface PlannerConfig {
  maxIterations: number  // Default: 100
}

Constants

const LOOP_SLEEP_MS = 500                    // Loop tick interval
const MIN_HANDOFFS_FOR_REPLAN = 3            // Trigger threshold
const BACKOFF_BASE_MS = 2_000                // Error backoff base
const BACKOFF_MAX_MS = 30_000                // Max backoff delay
const MAX_CONSECUTIVE_ERRORS = 10            // Abort threshold
const MAX_TASK_RETRIES = 1                   // Per-task retry limit
const MAX_HANDOFF_SUMMARY_CHARS = 2000       // Summary truncation
const MAX_FILES_PER_HANDOFF = 10             // File list truncation

Usage Example

import { Planner } from '@longshot/orchestrator'

const planner = new Planner(
  config,
  { maxIterations: 100 },
  taskQueue,
  workerPool,
  mergeQueue,
  monitor,
  rootPlannerPrompt,
  subplanner
)

planner.onTaskCreated((task) => {
  console.log(`Task created: ${task.id}`)
})

planner.onTaskCompleted((task, handoff) => {
  console.log(`Task ${task.id} ${handoff.status}`)
})

await planner.runLoop("Build the product according to SPEC.md")

Build docs developers (and LLMs) love