/** * Agent-driven goal decomposition for the planner. * * This module is the model-decomposition counterpart to the deterministic * heuristic path in `plan-proposal.ts`. Given a goal, it drives a bounded, * READ-ONLY planning agent (through the injected `DecompositionRunner` seam, * this module never imports the agent machinery, so it stays in `core` and is * fully unit-testable with a stubbed runner), parses and STRICTLY validates * the agent's structured output, and produces a `PlanProposal` tagged with * honest provenance. * * Failure honesty is the whole point: a spawn error, a timeout/cancellation, * or output that is still malformed after ONE repair attempt all fall back to * the existing heuristic `singleItemProposal` path, tagged * `decomposedBy: 'heuristic'` with a `fallbackReason`. The heuristic path is * reused verbatim, `singleItemProposal`/`assemblePlanProposal` are never * modified, so their existing byte-for-byte test expectations are preserved; * provenance is layered on afterward. * * The module NEVER performs surgery on agent output to force it to validate: * a proposal is accepted only when `assemblePlanProposal` reports ZERO issues * (no dangling dependency, no dependency cycle, no unresolved phase). Anything * short of that is one repair attempt and then an honest fallback, never a * silent edit that drops or rewrites the agent's work items. */ import { AdaptivePlanner } from './adaptive-planner.js'; import type { PlannerInputs, DecompositionGate } from './adaptive-planner.js'; import { type PlanProposal, type PlanProposalIssue, type RawDecomposition, type DecompositionAgentUsage } from './plan-proposal.js'; /** A phase in the agent's decomposition. Optional across the contract, items * may share an implicit single phase. */ export interface DecompositionAgentPhase { title: string; description?: string; } /** * A single work item in the agent's decomposition. * * `ordinal` fixes a stable execution order independent of array position. * `dependsOn` entries may be either other items' titles or their ordinals * (as a number or numeric string), both are resolved to titles before the * proposal is assembled. */ export interface DecompositionAgentItem { title: string; brief: string; ordinal: number; phase?: string; dependsOn?: Array; suggestedArchetype?: string; likelyFiles?: string[]; verification?: string[]; canRunConcurrently?: boolean; needsReview?: boolean; } /** The full JSON object a planning agent emits. */ export interface DecompositionAgentOutput { phases?: DecompositionAgentPhase[]; items: DecompositionAgentItem[]; /** Extra explicit edges; `{from, to}` reads "from depends on to". */ dependencies?: Array<{ from: string | number; to: string | number; }>; notes?: string[]; risks?: string[]; } /** Hard bounds on a planning-agent run. */ export interface DecompositionBounds { /** Maximum agent turns before the run is stopped. */ maxTurns: number; /** Total token budget; exceeding it stops the run. */ tokenCeiling: number; /** Wall-clock timeout in ms; exceeding it cancels the run. */ wallTimeoutMs: number; } export interface DecompositionRunnerRequest { goal: string; workingDir: string; systemPrompt: string; userPrompt: string; bounds: DecompositionBounds; /** Which attempt this is; `'repair'` prompts include prior validation errors. */ attempt: 'initial' | 'repair'; } /** * Terminal status of a planning-agent run. * - `completed`, the agent finished and produced final output text. * - `cancelled`, the run was stopped: an external kill, the wall-clock * timeout firing, or the token ceiling being crossed all collapse to this. * - `failed` , the agent could not be spawned or errored mid-run. */ export type DecompositionRunStatus = 'completed' | 'cancelled' | 'failed'; export interface DecompositionRunResult { status: DecompositionRunStatus; /** The agent's final output text (empty when it never produced any). */ output: string; usage?: DecompositionAgentUsage | undefined; elapsedMs: number; /** Error detail for `failed`, or a stop detail for `cancelled` (e.g. 'wall-timeout'). */ detail?: string | undefined; agentId?: string | undefined; } export interface DecompositionRunner { run(request: DecompositionRunnerRequest): Promise; } export interface DecomposeGoalConstraints { /** Concurrency capacity the plan should respect, if the caller set one. */ capacity?: number | undefined; /** Dollar budget for the workstream, if the caller set one. */ budgetUsd?: number | undefined; } export interface DecomposeGoalRequest { goal: string; workingDir: string; constraints?: DecomposeGoalConstraints | undefined; /** Optional free-form user context handed to the planning agent. */ userContext?: string | undefined; } export interface DecompositionServiceConfig { mode: 'agent' | 'heuristic'; bounds: DecompositionBounds; } /** An honest, machine-readable record of how a decomposition resolved. */ export type DecompositionOutcome = { kind: 'agent'; itemCount: number; repaired: boolean; usage?: DecompositionAgentUsage | undefined; costUsd?: number | undefined; elapsedMs: number; } | { kind: 'heuristic-configured'; } | { kind: 'gate-declined'; reasonCode: DecompositionGate['reasonCode']; } | { kind: 'fallback'; reason: string; usage?: DecompositionAgentUsage | undefined; elapsedMs?: number | undefined; }; export interface DecomposeGoalDeps { /** Optional token→dollars estimator; when absent, `agentCostUsd` stays undefined. */ estimateCostUsd?: ((usage: DecompositionAgentUsage) => number | undefined) | undefined; /** Optional honest-event sink, invoked exactly once per decomposition. */ onOutcome?: ((outcome: DecompositionOutcome) => void) | undefined; } export interface DecomposeGoalResult { proposal: PlanProposal; gate: DecompositionGate; issues: PlanProposalIssue[]; outcome: DecompositionOutcome; } export interface ParsedDecomposition { ok: boolean; raw?: RawDecomposition | undefined; notes?: string[] | undefined; errors: string[]; } /** * Parse + strictly validate a planning agent's output text into a * `RawDecomposition` the assembler can consume. Never throws; every problem is * accumulated into `errors` so a single repair prompt can address them all at * once. Structural rejects: unparseable JSON, empty/absent items array, empty * item title/brief, non-finite ordinal, empty phase title. */ export declare function parseDecomposition(text: string): ParsedDecomposition; /** * Map a validated `DecompositionAgentOutput` into the `RawDecomposition` shape * `assemblePlanProposal` consumes. Items are ordered by `ordinal` (stable on * ties). Every referenced phase is materialized so the assembler never has to * synthesize an "Unphased" bucket, an unresolved phase from here would be a * real bug, not agent sloppiness. Ordinal-based dependency references are * resolved to titles so the assembler's title resolver handles them uniformly. */ export declare function toRawDecomposition(output: DecompositionAgentOutput): RawDecomposition; /** * Build the system prompt for the read-only planning agent. It is explicit * about the exact JSON contract and about the read-only posture (the agent's * tool set already excludes write/edit/exec; this reinforces intent). */ export declare function buildPlannerSystemPrompt(): string; /** * Decompose a goal into a `PlanProposal`, honestly tagged with provenance. * * Control flow: * 1. `config.mode === 'heuristic'` → the configured heuristic path (no agent). * 2. The planner's decompose gate declines → honest single-item (no agent). * 3. No runner available → heuristic fallback (reason: no runtime). * 4. Agent path: run → parse → strict-validate → (assemble; issues===0 ? * accept : ONE repair attempt) → accept-or-fallback. Spawn error, * cancellation (kill / wall-timeout / token ceiling), or still-invalid * output after repair all fall back to the heuristic path with a reason. */ export declare function decomposeGoal(request: DecomposeGoalRequest, planner: AdaptivePlanner, inputs: PlannerInputs, config: DecompositionServiceConfig, runner: DecompositionRunner | null, deps?: DecomposeGoalDeps): Promise; //# sourceMappingURL=plan-decomposition.d.ts.map