Skip to main content

System architecture

Longshot’s architecture is built for massive parallelism and stateless execution. This page explains each component and how they interact.

High-level architecture

┌─────────────────────────────────────────────────────────────────┐
│                         User Request                             │
└────────────────────────────┬────────────────────────────────────┘

                    ┌────────▼────────┐
                    │  Root Planner   │ (Continuous LLM session)
                    │   (Pi Agent)    │
                    └────────┬────────┘
                             │ Creates tasks
                ┌────────────┼────────────┐
                │            │            │
         ┌──────▼─────┐ ┌───▼────┐ ┌────▼─────┐
         │ Subplanner │ │ Worker │ │ Worker   │ (50+ parallel)
         │ (complex)  │ │        │ │          │
         └──────┬─────┘ └───┬────┘ └────┬─────┘
                │           │            │
         ┌──────▼─────┐     │            │
         │  Workers   │     │            │
         │ (parallel) │     │            │
         └──────┬─────┘     │            │
                │           │            │
       ┌────────┴───────────┴────────────┴─────────┐
       │            Merge Queue                      │ (Serial, priority)
       │  ┌──────────────────────────────────────┐  │
       │  │ Priority 1: fix-001                  │  │
       │  │ Priority 1: conflict-fix-002         │  │
       │  │ Priority 5: worker/task-003          │  │
       │  │ Priority 5: worker/task-007          │  │
       │  └──────────────────────────────────────┘  │
       └────────────────────┬───────────────────────┘

                   ┌────────▼────────┐
                   │   Main Branch   │
                   │   (Target Repo) │
                   └────────┬────────┘

                   ┌────────▼────────┐
                   │   Reconciler    │ (Background, every 5min)
                   │ (Build + Test)  │
                   └────────┬────────┘
                            │ Creates fix tasks
                   ┌────────▼────────┐
                   │  Root Planner   │ (Injects high-priority tasks)
                   └─────────────────┘

Core components

Root planner

Location: packages/orchestrator/src/planner.ts Purpose: Iterative LLM-driven task generation with continuous conversation and delta optimization. Key responsibilities:
  • Analyzes user request + repository state (SPEC.md, FEATURES.json, file tree)
  • Creates granular tasks with descriptions, scope, acceptance criteria
  • Maintains a continuous Pi agent session across planning iterations
  • Adapts based on worker handoffs (minimum 3 handoffs trigger replan)
  • Injects external tasks (conflict fixes, reconciler fixes)
Planning loop:
while (iteration < maxIterations) {
  // Collect completed handoffs from workers
  collectCompletedHandoffs();
  
  // Determine if we need a new plan
  const needsPlan = 
    hasCapacity && 
    (iteration === 0 || handoffsSinceLastPlan >= 3 || noActiveWork);
  
  if (needsPlan && !planningDone) {
    const repoState = await readRepoState(targetRepoPath);
    const tasks = await plan(request, repoState, newHandoffs);
    
    if (tasks.length === 0 && noActiveWork) {
      planningDone = true;
    } else {
      dispatchTasks(tasks); // Fire-and-forget parallel dispatch
    }
  }
  
  await sleep(500);
}
Delta optimization: To avoid sending unchanged context repeatedly (saves ~40K chars per iteration), the planner tracks:
  • previousFileTree: File tree sent in last prompt
  • previousFeaturesHash: Hash of FEATURES.json content
  • previousDecisionsHash: Hash of DECISIONS.md content
Follow-up prompts only include changed files, features, or decisions. Task routing:
if (task.scope.length >= 4) {
  // Large scope → route to subplanner for decomposition
  handoff = await subplanner.decomposeAndExecute(task);
} else {
  // Small scope → direct to worker
  handoff = await workerPool.assignTask(task);
}

Subplanner

Location: packages/orchestrator/src/subplanner.ts Purpose: Recursive task decomposer that breaks large tasks into worker-sized units. Configuration:
const DEFAULT_SUBPLANNER_CONFIG = {
  maxDepth: 3,              // Max recursion depth
  scopeThreshold: 4,        // Files threshold for decomposition
  maxSubtasks: 10          // Max subtasks per decomposition
};
Decomposition logic:
function shouldDecompose(task: Task, config: SubplannerConfig, depth: number): boolean {
  if (depth >= config.maxDepth) return false;         // Max depth reached
  if (task.scope.length < config.scopeThreshold) return false; // Small enough
  return true;
}
Recursive execution:
async decomposeAndExecute(parentTask: Task, depth: number): Promise<Handoff> {
  // Create Pi session for this decomposition
  const piSession = await createPlannerPiSession(...);
  
  // Initial prompt with parent task context
  const prompt = buildInitialMessage(parentTask, repoState, depth);
  await piSession.session.prompt(prompt);
  
  const subtasks = parseTasks(response);
  
  // Dispatch subtasks in parallel
  for (const subtask of subtasks) {
    if (shouldDecompose(subtask, config, depth + 1)) {
      // Recurse: subtask is still complex
      handoff = await this.decomposeAndExecute(subtask, depth + 1);
    } else {
      // Atomic: dispatch to worker
      handoff = await workerPool.assignTask(subtask);
    }
  }
  
  // Aggregate all subtask handoffs into parent handoff
  return aggregateHandoffs(parentTask, subtasks, allHandoffs);
}
Example decomposition tree:
task-004: "Create todo CRUD endpoints" (scope: 5 files)
├─ task-004-sub-1: "Create todo model and schema" (scope: 2 files)
│  └─ Worker → worker/task-004-sub-1
├─ task-004-sub-2: "Create GET /todos endpoint" (scope: 1 file)
│  └─ Worker → worker/task-004-sub-2
├─ task-004-sub-3: "Create POST /todos endpoint" (scope: 1 file)
│  └─ Worker → worker/task-004-sub-3
└─ task-004-sub-4: "Create DELETE /todos endpoint" (scope: 1 file)
   └─ Worker → worker/task-004-sub-4

Worker pool

Location: packages/orchestrator/src/worker-pool.ts Purpose: Ephemeral sandbox spawner that creates Modal sandboxes for task execution. Architecture: No persistent pool — each task spawns a fresh sandbox:
class WorkerPool {
  async assignTask(task: Task): Promise<Handoff> {
    const worker = {
      id: `ephemeral-${task.id}`,
      currentTask: task,
      startedAt: Date.now()
    };
    
    // Spawn Python subprocess that creates Modal sandbox
    const handoff = await this.runSandboxStreaming(
      task.id,
      task.branch,
      JSON.stringify({
        task,
        systemPrompt: this.workerPrompt,
        repoUrl: this.config.git.repoUrl,
        llmConfig: this.config.llm
      })
    );
    
    return handoff;
  }
}
Sandbox lifecycle (Python layer):
# infra/spawn_sandbox.py
import modal

def spawn_sandbox(task: Task, repo_url: str, llm_config: LLMConfig):
    # 1. Create sandbox
    sandbox = modal.Sandbox.create(
        "ubuntu-22.04",
        cpu=4,
        memory=8192,
        timeout=1800
    )
    
    # 2. Clone repository
    sandbox.exec(f"git clone {repo_url} /workspace")
    sandbox.exec(f"cd /workspace && git checkout -b {task.branch}")
    
    # 3. Start Pi agent
    result = sandbox.exec(
        "node",
        args=["worker-runner.js", json.dumps(task)],
        env={"LLM_ENDPOINT": llm_config.endpoint, ...}
    )
    
    # 4. Push branch
    sandbox.exec(f"cd /workspace && git push origin {task.branch}")
    
    # 5. Extract handoff
    handoff = json.loads(result.stdout)
    
    # 6. Terminate sandbox (automatic)
    return handoff
Streaming output: The worker pool streams stdout line-by-line, forwarding agent progress:
rl.on('line', (line: string) => {
  if (line.startsWith('[spawn]')) {
    logger.info('Worker progress', { phase: 'sandbox', detail: line });
  } else if (line.startsWith('[worker:')) {
    logger.info('Worker progress', { phase: 'execution', detail: line });
  }
});
This enables real-time visibility in the dashboard.

Merge queue

Location: packages/orchestrator/src/merge-queue.ts Purpose: Serial merge pipeline with priority ordering, conflict retry, and automatic rebasing. Queue structure:
interface MergeQueueEntry {
  branch: string;
  priority: number;  // 1 (highest) → 10 (lowest)
  enqueuedAt: number;
}
Branches are sorted by priority first, then by enqueue time. Merge strategies:
  1. Fast-forward (default for single-file changes):
    git merge --ff-only origin/worker/task-001
    
  2. Rebase (preserves linear history):
    git rebase origin/worker/task-001
    
  3. Merge commit (fallback):
    git merge --no-ff origin/worker/task-001
    
Conflict handling:
if (result.conflicted) {
  const retries = this.retryCount.get(branch) ?? 0;
  
  if (retries < maxConflictRetries) {
    // Rebase branch onto latest main before retry
    await rebaseBranch(branch, mainBranch);
    this.enqueue(branch, 1); // High priority
    this.retryCount.set(branch, retries + 1);
  } else {
    // Retries exhausted → create conflict-fix task
    const fixTask = {
      id: `conflict-fix-${counter}`,
      description: `Resolve merge conflict from ${branch}. Files: ${conflicts.join(', ')}`,
      scope: conflicts.slice(0, 5),
      branch: branch,  // Reuse original branch
      priority: 1
    };
    
    planner.injectTask(fixTask);
  }
}
Background processing:
startBackground(intervalMs = 5000) {
  this.timer = setInterval(async () => {
    while (this.queue.length > 0) {
      const branch = this.dequeue();
      const result = await this.mergeBranch(branch);
      
      for (const cb of this.callbacks) {
        cb(result);
      }
    }
  }, intervalMs);
}

Reconciler

Location: packages/orchestrator/src/reconciler.ts Purpose: Periodic build and test reconciliation with LLM-generated fix tasks. Sweep interval: Adaptive (1min when errors detected → 5min when green) Sweep logic:
async sweep(): Promise<SweepResult> {
  // 1. Run build check
  const tscResult = await exec('npx', ['tsc', '--noEmit'], targetRepo);
  const buildOk = tscResult.code === 0;
  
  // 2. Run test check
  const testResult = await exec('npm', ['test'], targetRepo);
  const testsOk = testResult.code === 0;
  
  // 3. Check for conflict markers
  const conflictResult = await exec('git', ['grep', '-rl', '<<<<<<<'], targetRepo);
  const conflictFiles = conflictResult.stdout.split('\n').filter(Boolean);
  const hasConflicts = conflictFiles.length > 0;
  
  // 4. If all green, no action needed
  if (buildOk && testsOk && !hasConflicts) {
    return { buildOk: true, testsOk: true, fixTasks: [] };
  }
  
  // 5. Call LLM to analyze failures and generate fix tasks
  const prompt = `
## Build Output
${buildOutput}

## Test Output
${testOutput}

## Conflict Files
${conflictFiles.join(', ')}

Analyze these failures and generate fix tasks.
  `;
  
  const response = await llm.complete([{ role: 'user', content: prompt }]);
  const fixTasks = parseTasks(response.content);
  
  // 6. Return fix tasks for injection
  return { buildOk, testsOk, hasConflicts, fixTasks };
}
Fix task injection: The orchestrator wires reconciler output back to the planner:
reconciler.onSweepComplete((result) => {
  planner.setLastSweepResult(result);
  
  for (const task of result.fixTasks) {
    planner.injectTask(task); // High-priority injection
  }
});
Adaptive interval:
if (consecutiveGreenSweeps >= 3) {
  adjustInterval(maxIntervalMs); // Slow down to 5min
} else {
  adjustInterval(minIntervalMs); // Speed up to 1min
}

Monitor

Location: packages/orchestrator/src/monitor.ts Purpose: Real-time metrics collection and snapshot generation. Metrics tracked:
interface MetricsSnapshot {
  timestamp: number;
  activeWorkers: number;
  pendingTasks: number;
  runningTasks: number;
  completedTasks: number;
  failedTasks: number;
  suspiciousTaskCount: number;     // 0 tokens + 0 tool calls
  commitsPerHour: number;
  mergeSuccessRate: number;
  totalTokensUsed: number;
  totalCostUsd: number;
  activeToolCalls: number;
  estimatedInFlightTokens: number;
}
Suspicious task detection:
if (handoff.metrics.tokensUsed === 0 && handoff.metrics.toolCallCount === 0) {
  monitor.recordSuspiciousTask(task.id, '0 tokens and 0 tool calls');
}
Indicates agent may have failed silently.

Package structure

Longshot is organized as a TypeScript monorepo with Python CLI wrapper:
longshot/
├── packages/
│   ├── core/                     # Shared types, logging, git utilities
│   │   ├── src/
│   │   │   ├── types.ts          # Task, Handoff, HarnessConfig
│   │   │   ├── logger.ts         # Structured logging
│   │   │   ├── tracer.ts         # Distributed tracing
│   │   │   └── git.ts            # Git operations
│   │   └── package.json
│   │
│   ├── orchestrator/             # Planning, worker pool, merge queue
│   │   ├── src/
│   │   │   ├── orchestrator.ts   # Factory (wires all components)
│   │   │   ├── planner.ts        # Root planning loop
│   │   │   ├── subplanner.ts     # Recursive decomposition
│   │   │   ├── worker-pool.ts    # Ephemeral sandbox spawner
│   │   │   ├── merge-queue.ts    # Serial merge pipeline
│   │   │   ├── reconciler.ts     # Build/test health checker
│   │   │   ├── monitor.ts        # Metrics collection
│   │   │   ├── task-queue.ts     # Task state management
│   │   │   └── config.ts         # Environment config
│   │   └── package.json
│   │
│   └── sandbox/                  # Modal sandbox definition
│       ├── src/
│       │   ├── index.ts          # Modal image definition
│       │   ├── worker-runner.ts  # Pi agent harness
│       │   └── handoff.ts        # Handoff builder
│       └── package.json

├── infra/
│   ├── spawn_sandbox.py          # Python → Modal bridge
│   └── sandbox_image.py          # Modal image builder

├── main.py                       # CLI entrypoint (Python)
├── dashboard.py                  # Rich TUI dashboard (Python)
└── prompts/
    ├── root-planner.md           # Root planner system prompt
    ├── subplanner.md             # Subplanner system prompt
    ├── worker.md                 # Worker system prompt
    └── reconciler.md             # Reconciler system prompt

Stateless design

Longshot’s architecture is radically stateless:

Workers

Ephemeral sandboxes — create, execute, terminate. No persistent pool, no session management.

State storage

Git is the only source of truth. Repository structure + branches = current state.

Orchestrator

Runs locally. No database, no Redis, no persistent queue. All state in memory.

Execution

Cloud sandboxes (Modal). Workers run remotely, orchestrator coordinates locally.
Benefits:
  • No operational complexity: No databases, queues, or persistent services to manage
  • Fault tolerance: Workers can fail independently without affecting others
  • Cost efficiency: Sandboxes only run during task execution
  • Audit trail: Git history provides complete execution audit

Next steps

How it works

Follow a request through the execution flow step-by-step

Configuration

Configure workers, LLM endpoints, and sandbox resources

Build docs developers (and LLMs) love