import * as fs from "node:fs"; import { createRequire } from "node:module"; import * as os from "node:os"; import * as path from "node:path"; import { pathToFileURL } from "node:url"; import { PI_CODING_AGENT_PACKAGE_ROOT_ENV } from "../../shared/utils.ts"; import type { JsonSchemaObject } from "../../shared/types.ts"; export const STRUCTURED_OUTPUT_SCHEMA_ENV = "PI_SUBAGENT_STRUCTURED_OUTPUT_SCHEMA"; export const STRUCTURED_OUTPUT_CAPTURE_ENV = "PI_SUBAGENT_STRUCTURED_OUTPUT_CAPTURE"; export const MISSING_STRUCTURED_OUTPUT_CALL_ERROR = "Missing structured_output call; this step has outputSchema and must finish by calling structured_output."; export interface StructuredOutputRuntime { schema: JsonSchemaObject; schemaPath: string; outputPath: string; } const SCHEMA_MAP_KEYWORDS = ["properties", "patternProperties", "$defs", "definitions", "dependentSchemas"] as const; const SCHEMA_SINGLE_KEYWORDS = ["additionalItems", "additionalProperties", "contains", "not", "propertyNames", "if", "then", "else", "unevaluatedItems", "unevaluatedProperties", "contentSchema"] as const; const SCHEMA_ARRAY_KEYWORDS = ["allOf", "anyOf", "oneOf", "prefixItems"] as const; function rewriteLocalJsonPointerRefs(schema: unknown, pointerPrefix: string, inheritsWrapperResource = true): unknown { if (typeof schema === "boolean" || !schema || typeof schema !== "object" || Array.isArray(schema)) return schema; const source = schema as Record; const rewritten: Record = { ...source }; const sharesWrapperResource = inheritsWrapperResource && typeof source.$id !== "string"; if (sharesWrapperResource) { for (const keyword of ["$ref", "$dynamicRef", "$recursiveRef"] as const) { const ref = source[keyword]; if (ref === "#") rewritten[keyword] = pointerPrefix; else if (typeof ref === "string" && ref.startsWith("#/")) rewritten[keyword] = `${pointerPrefix}${ref.slice(1)}`; } } for (const keyword of SCHEMA_MAP_KEYWORDS) { const entries = source[keyword]; if (!entries || typeof entries !== "object" || Array.isArray(entries)) continue; rewritten[keyword] = Object.fromEntries(Object.entries(entries).map(([name, nested]) => [ name, rewriteLocalJsonPointerRefs(nested, pointerPrefix, sharesWrapperResource), ])); } const items = source.items; if (Array.isArray(items)) rewritten.items = items.map((nested) => rewriteLocalJsonPointerRefs(nested, pointerPrefix, sharesWrapperResource)); else if (items !== undefined) rewritten.items = rewriteLocalJsonPointerRefs(items, pointerPrefix, sharesWrapperResource); for (const keyword of SCHEMA_SINGLE_KEYWORDS) { if (source[keyword] !== undefined) rewritten[keyword] = rewriteLocalJsonPointerRefs(source[keyword], pointerPrefix, sharesWrapperResource); } for (const keyword of SCHEMA_ARRAY_KEYWORDS) { if (Array.isArray(source[keyword])) rewritten[keyword] = source[keyword].map((nested) => rewriteLocalJsonPointerRefs(nested, pointerPrefix, sharesWrapperResource)); } const dependencies = source.dependencies; if (dependencies && typeof dependencies === "object" && !Array.isArray(dependencies)) { rewritten.dependencies = Object.fromEntries(Object.entries(dependencies).map(([name, nested]) => [ name, Array.isArray(nested) ? nested : rewriteLocalJsonPointerRefs(nested, pointerPrefix, sharesWrapperResource), ])); } return rewritten; } export function createStructuredOutputToolParameters(schema: JsonSchemaObject): JsonSchemaObject { return { type: "object", properties: { value: rewriteLocalJsonPointerRefs(schema, "#/properties/value") }, required: ["value"], additionalProperties: false, }; } interface CompiledJsonSchema { Check(value: unknown): boolean; Errors(value: unknown): Iterable<{ instancePath?: string; message?: string }>; } type CompileJsonSchema = (schema: unknown) => CompiledJsonSchema; let cachedCompile: Promise | undefined; export async function resolveCompileFromPackageRoot(packageRoot: string): Promise { const requireFromRoot = createRequire(path.join(packageRoot, "package.json")); const resolved = requireFromRoot.resolve("typebox/compile"); const mod = (await import(pathToFileURL(resolved).href)) as { Compile?: unknown }; return typeof mod.Compile === "function" ? (mod.Compile as CompileJsonSchema) : undefined; } async function importCompile(): Promise { const failures: string[] = []; try { const mod = (await import("typebox/compile")) as { Compile?: unknown }; if (typeof mod.Compile === "function") return mod.Compile as CompileJsonSchema; failures.push("typebox/compile did not export a Compile function"); } catch (error) { failures.push(`direct import failed: ${error instanceof Error ? error.message : String(error)}`); } const packageRoot = process.env[PI_CODING_AGENT_PACKAGE_ROOT_ENV]; if (packageRoot) { try { const compile = await resolveCompileFromPackageRoot(packageRoot); if (compile) return compile; failures.push("Pi package root typebox/compile did not export a Compile function"); } catch (error) { failures.push(`Pi package root import failed: ${error instanceof Error ? error.message : String(error)}`); } } else { failures.push(`${PI_CODING_AGENT_PACKAGE_ROOT_ENV} is not set`); } throw new Error(`Cannot load typebox/compile for structured output validation (${failures.join("; ")})`); } function loadCompile(): Promise { if (!cachedCompile) { cachedCompile = importCompile().catch((error) => { cachedCompile = undefined; throw error; }); } return cachedCompile; } export function assertJsonSchemaObject(schema: unknown, label = "outputSchema"): asserts schema is JsonSchemaObject { if (!schema || typeof schema !== "object" || Array.isArray(schema)) { throw new Error(`${label} must be a JSON Schema object.`); } } export function createStructuredOutputRuntime(schema: JsonSchemaObject, baseDir?: string): StructuredOutputRuntime { assertJsonSchemaObject(schema); const rootDir = baseDir ?? os.tmpdir(); fs.mkdirSync(rootDir, { recursive: true }); const dir = fs.mkdtempSync(path.join(rootDir, "pi-subagent-structured-")); const schemaPath = path.join(dir, "schema.json"); const outputPath = path.join(dir, "output.json"); fs.writeFileSync(schemaPath, JSON.stringify(schema), { mode: 0o600 }); return { schema, schemaPath, outputPath }; } export async function validateStructuredOutputValue(schema: JsonSchemaObject, value: unknown): Promise<{ status: "valid" } | { status: "invalid"; message: string }> { const compile = await loadCompile(); let validator: CompiledJsonSchema; try { validator = compile(schema); } catch (error) { return { status: "invalid", message: `invalid outputSchema: ${error instanceof Error ? error.message : String(error)}` }; } if (validator.Check(value)) return { status: "valid" }; const errors = [...validator.Errors(value)] .slice(0, 8) .map((error) => { const pathText = error.instancePath ? error.instancePath.replace(/^\//, "").replace(/\//g, ".") : "root"; return `${pathText}: ${error.message}`; }); return { status: "invalid", message: errors.join("; ") || "schema validation failed" }; } export async function readStructuredOutput(runtime: StructuredOutputRuntime): Promise<{ value?: unknown; error?: string }> { if (!fs.existsSync(runtime.outputPath)) { return { error: MISSING_STRUCTURED_OUTPUT_CALL_ERROR }; } let value: unknown; try { value = JSON.parse(fs.readFileSync(runtime.outputPath, "utf-8")); } catch (error) { return { error: `Failed to read structured output: ${error instanceof Error ? error.message : String(error)}` }; } try { const validation = await validateStructuredOutputValue(runtime.schema, value); if (validation.status === "invalid") return { error: `Structured output validation failed: ${validation.message}` }; } catch (error) { return { error: `Failed to validate structured output: ${error instanceof Error ? error.message : String(error)}` }; } return { value }; } export function cleanupStructuredOutputRuntime(runtime: StructuredOutputRuntime | undefined): void { if (!runtime) return; try { fs.rmSync(path.dirname(runtime.schemaPath), { recursive: true, force: true }); } catch { // Best-effort temp cleanup. } }