{"version":3,"file":"plan-schema.mjs","names":[],"sources":["../../../../../../../ai/src/planner/plan-schema.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { PlannerPlan, PlannerStep } from \"../contracts/planner/planner-plan.type\";\n\n/**\n * Build the Standard Schema the planning agent emits — an ordered\n * `{ steps: [...], summary? }` plan whose every step references one of\n * `capabilityNames` via the `capability` field.\n *\n * Mirrors the router's hand-built schema approach\n * (`supervisor/router-factory.ts`): the JSON Schema extension carries\n * the capability names as an `enum` so capable providers enforce the\n * choice natively, while `validate()` still accepts the shape softly so\n * providers without native structured output can pass a parsed object\n * through. Validation is intentionally lenient on `capability` — an\n * unknown name is surfaced later by the planner as a typed\n * `PlannerPlanInvalidError`, with the full forensic context, rather\n * than as an opaque schema issue here.\n *\n * `maxSteps` cannot be expressed on the wire (strict mode rejects\n * `maxItems`), so `validate()` enforces a hard parse-time ceiling\n * derived from it — see {@link parsedStepCeiling}.\n */\nexport type PlanSchema = StandardSchemaV1<PlannerPlan> & {\n  \"~standard\": {\n    /**\n     * JSON Schema extension read by the native structured-output path.\n     * Part of the declared type so callers don't have to re-assert it.\n     */\n    jsonSchema: { input: () => Record<string, unknown> };\n  };\n};\n\n/**\n * Slack allowed over `maxSteps` before a returned plan is rejected\n * outright. A model that overshoots the prompt's \"at most N steps\" by a\n * little is normal and the runtime truncates the tail to `skipped`;\n * one that returns several times the budget is malfunctioning (or the\n * provider/proxy is not the one we think it is), and parsing it is\n * unbounded work on attacker-adjacent input.\n */\nconst STEP_CEILING_FACTOR = 4;\n\n/**\n * Ceiling used when `planSchema` is built without a `maxSteps` — direct\n * callers outside `PlannerRun`, which has no runtime truncation of its\n * own to fall back on.\n */\nconst DEFAULT_STEP_CEILING = 100;\n\n/**\n * Hard upper bound on the number of steps `validate()` will parse.\n *\n * Strict-mode JSON Schema can't carry `maxItems`, so nothing on the wire\n * stops a provider from returning an arbitrarily long `steps[]`; before\n * 4.15.0 the whole array was parsed, normalized and stored, and only the\n * execution loop truncated it. This is the parse-time backstop that\n * makes the bound hold regardless of what the provider honors.\n */\nexport function parsedStepCeiling(maxSteps?: number): number {\n  if (maxSteps === undefined) {\n    return DEFAULT_STEP_CEILING;\n  }\n\n  return Math.max(1, Math.ceil(maxSteps)) * STEP_CEILING_FACTOR;\n}\n\nexport function planSchema(capabilityNames: string[], maxSteps?: number): PlanSchema {\n  // OpenAI strict `json_schema` mode (and other native structured-output\n  // providers) require EVERY property to appear in `required` — with truly\n  // optional fields expressed as nullable — and reject array `minItems` /\n  // `maxItems`. So the schema is strict-shaped: all keys required, the\n  // optional ones nullable, no item-count bounds on the wire. Both bounds\n  // live in `validate()` instead: non-empty below, and the over-long\n  // ceiling that `maxItems` would have expressed.\n  const stepCeiling = parsedStepCeiling(maxSteps);\n\n  const jsonSchema = {\n    type: \"object\",\n    properties: {\n      summary: {\n        type: [\"string\", \"null\"],\n        description: \"One-line summary of the overall strategy.\",\n      },\n      steps: {\n        type: \"array\",\n        description: \"Ordered steps to execute, one capability dispatch each.\",\n        items: stepItemsSchema(capabilityNames),\n      },\n    },\n    required: [\"summary\", \"steps\"],\n    additionalProperties: false,\n  };\n\n  return {\n    \"~standard\": {\n      version: 1,\n      vendor: \"warlock-planner\",\n      jsonSchema: {\n        input: () => jsonSchema,\n      },\n      validate(value: unknown): StandardSchemaV1.Result<PlannerPlan> {\n        if (!value || typeof value !== \"object\") {\n          return { issues: [{ message: \"plan must be an object\" }] };\n        }\n\n        const record = value as { steps?: unknown; summary?: unknown };\n\n        if (!Array.isArray(record.steps) || record.steps.length === 0) {\n          return { issues: [{ message: \"plan `steps` must be a non-empty array\" }] };\n        }\n\n        // Reject an over-long plan HERE, before a single step is\n        // normalized — the runtime's tail truncation runs after the whole\n        // array has been parsed and stored, so it bounds execution but\n        // not the parsing cost of a pathological response. Rejecting\n        // rather than truncating is deliberate: a plan several times its\n        // budget is a malfunction worth surfacing as\n        // `PlannerPlanInvalidError`, not something to silently trim into\n        // a plausible-looking prefix.\n        if (record.steps.length > stepCeiling) {\n          return {\n            issues: [\n              {\n                message: `plan \\`steps\\` must not exceed ${stepCeiling} entries (received ${record.steps.length})`,\n              },\n            ],\n          };\n        }\n\n        const steps: PlannerStep[] = [];\n\n        for (const raw of record.steps) {\n          const normalized = normalizeStep(raw);\n\n          if (!normalized) {\n            return {\n              issues: [{ message: \"each plan step must carry a string `capability` and `input`\" }],\n            };\n          }\n\n          steps.push(normalized);\n        }\n\n        const summary = typeof record.summary === \"string\" ? record.summary : undefined;\n\n        return { value: summary !== undefined ? { steps, summary } : { steps } };\n      },\n    } as StandardSchemaV1<PlannerPlan>[\"~standard\"] & {\n      jsonSchema: { input: () => Record<string, unknown> };\n    },\n  };\n}\n\n/** Per-step JSON Schema object — one capability dispatch. */\nfunction stepItemsSchema(capabilityNames: string[]): Record<string, unknown> {\n  return {\n    type: \"object\",\n    properties: {\n      id: {\n        type: [\"string\", \"null\"],\n        description: \"Stable step id, referenced by dependsOn.\",\n      },\n      capability: {\n        type: \"string\",\n        enum: capabilityNames,\n        description: \"Name of the capability to dispatch for this step.\",\n      },\n      input: {\n        type: \"string\",\n        description: \"Concrete input passed to the capability's execute().\",\n      },\n      reason: { type: [\"string\", \"null\"], description: \"Why this step exists.\" },\n      dependsOn: {\n        type: [\"array\", \"null\"],\n        items: { type: \"string\" },\n        description: \"Ids of steps this one conceptually follows.\",\n      },\n    },\n    // Strict mode: every property required; the genuinely-optional ones\n    // (id / reason / dependsOn) are nullable. `validate()` treats null and\n    // missing identically, so a model emitting `null` round-trips fine.\n    required: [\"id\", \"capability\", \"input\", \"reason\", \"dependsOn\"],\n    additionalProperties: false,\n  };\n}\n\n/**\n * Coerce one raw step object into a {@link PlannerStep}, returning\n * `undefined` when the mandatory `capability` / `input` strings are\n * missing. Optional fields are copied only when well-typed.\n */\nfunction normalizeStep(raw: unknown): PlannerStep | undefined {\n  if (!raw || typeof raw !== \"object\") {\n    return undefined;\n  }\n\n  const record = raw as {\n    id?: unknown;\n    capability?: unknown;\n    input?: unknown;\n    reason?: unknown;\n    dependsOn?: unknown;\n  };\n\n  if (typeof record.capability !== \"string\" || record.capability.length === 0) {\n    return undefined;\n  }\n\n  if (typeof record.input !== \"string\") {\n    return undefined;\n  }\n\n  const step: PlannerStep = {\n    capability: record.capability,\n    input: record.input,\n  };\n\n  if (typeof record.id === \"string\") {\n    step.id = record.id;\n  }\n\n  if (typeof record.reason === \"string\") {\n    step.reason = record.reason;\n  }\n\n  if (Array.isArray(record.dependsOn) && record.dependsOn.every((entry) => typeof entry === \"string\")) {\n    step.dependsOn = record.dependsOn as string[];\n  }\n\n  return step;\n}\n"],"mappings":";;;;;;;;;AAwCA,MAAM,sBAAsB;;;;;;AAO5B,MAAM,uBAAuB;;;;;;;;;;AAW7B,SAAgB,kBAAkB,UAA2B;CAC3D,IAAI,aAAa,QACf,OAAO;CAGT,OAAO,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,CAAC,IAAI;AAC5C;AAEA,SAAgB,WAAW,iBAA2B,UAA+B;CAQnF,MAAM,cAAc,kBAAkB,QAAQ;CAE9C,MAAM,aAAa;EACjB,MAAM;EACN,YAAY;GACV,SAAS;IACP,MAAM,CAAC,UAAU,MAAM;IACvB,aAAa;GACf;GACA,OAAO;IACL,MAAM;IACN,aAAa;IACb,OAAO,gBAAgB,eAAe;GACxC;EACF;EACA,UAAU,CAAC,WAAW,OAAO;EAC7B,sBAAsB;CACxB;CAEA,OAAO,EACL,aAAa;EACX,SAAS;EACT,QAAQ;EACR,YAAY,EACV,aAAa,WACf;EACA,SAAS,OAAsD;GAC7D,IAAI,CAAC,SAAS,OAAO,UAAU,UAC7B,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,yBAAyB,CAAC,EAAE;GAG3D,MAAM,SAAS;GAEf,IAAI,CAAC,MAAM,QAAQ,OAAO,KAAK,KAAK,OAAO,MAAM,WAAW,GAC1D,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,yCAAyC,CAAC,EAAE;GAW3E,IAAI,OAAO,MAAM,SAAS,aACxB,OAAO,EACL,QAAQ,CACN,EACE,SAAS,kCAAkC,YAAY,qBAAqB,OAAO,MAAM,OAAO,GAClG,CACF,EACF;GAGF,MAAM,QAAuB,CAAC;GAE9B,KAAK,MAAM,OAAO,OAAO,OAAO;IAC9B,MAAM,aAAa,cAAc,GAAG;IAEpC,IAAI,CAAC,YACH,OAAO,EACL,QAAQ,CAAC,EAAE,SAAS,8DAA8D,CAAC,EACrF;IAGF,MAAM,KAAK,UAAU;GACvB;GAEA,MAAM,UAAU,OAAO,OAAO,YAAY,WAAW,OAAO,UAAU;GAEtE,OAAO,EAAE,OAAO,YAAY,SAAY;IAAE;IAAO;GAAQ,IAAI,EAAE,MAAM,EAAE;EACzE;CACF,EAGF;AACF;;AAGA,SAAS,gBAAgB,iBAAoD;CAC3E,OAAO;EACL,MAAM;EACN,YAAY;GACV,IAAI;IACF,MAAM,CAAC,UAAU,MAAM;IACvB,aAAa;GACf;GACA,YAAY;IACV,MAAM;IACN,MAAM;IACN,aAAa;GACf;GACA,OAAO;IACL,MAAM;IACN,aAAa;GACf;GACA,QAAQ;IAAE,MAAM,CAAC,UAAU,MAAM;IAAG,aAAa;GAAwB;GACzE,WAAW;IACT,MAAM,CAAC,SAAS,MAAM;IACtB,OAAO,EAAE,MAAM,SAAS;IACxB,aAAa;GACf;EACF;EAIA,UAAU;GAAC;GAAM;GAAc;GAAS;GAAU;EAAW;EAC7D,sBAAsB;CACxB;AACF;;;;;;AAOA,SAAS,cAAc,KAAuC;CAC5D,IAAI,CAAC,OAAO,OAAO,QAAQ,UACzB;CAGF,MAAM,SAAS;CAQf,IAAI,OAAO,OAAO,eAAe,YAAY,OAAO,WAAW,WAAW,GACxE;CAGF,IAAI,OAAO,OAAO,UAAU,UAC1B;CAGF,MAAM,OAAoB;EACxB,YAAY,OAAO;EACnB,OAAO,OAAO;CAChB;CAEA,IAAI,OAAO,OAAO,OAAO,UACvB,KAAK,KAAK,OAAO;CAGnB,IAAI,OAAO,OAAO,WAAW,UAC3B,KAAK,SAAS,OAAO;CAGvB,IAAI,MAAM,QAAQ,OAAO,SAAS,KAAK,OAAO,UAAU,OAAO,UAAU,OAAO,UAAU,QAAQ,GAChG,KAAK,YAAY,OAAO;CAG1B,OAAO;AACT"}