import { getAssistantMcpCatalogPrimitive, type AssistantMcpCatalogEvidenceFloor } from "./assistantMcpCatalogIndex"; import { type AssistantMcpDiscoveryPlannerContract, type AssistantMcpDiscoveryPlannerStatus } from "./assistantMcpDiscoveryPlanner"; import type { AssistantMcpDiscoveryPrimitive } from "./assistantMcpDiscoveryPolicy"; export const ASSISTANT_MCP_DISCOVERY_RUNTIME_DRY_RUN_SCHEMA_VERSION = "assistant_mcp_discovery_runtime_dry_run_v1" as const; export type AssistantMcpDiscoveryRuntimeAdapterStatus = | "dry_run_ready" | "needs_clarification" | "blocked"; export type AssistantMcpDiscoveryRuntimeStepStatus = "ready" | "missing_axes" | "blocked"; export interface AssistantMcpDiscoveryRuntimeStepContract { sequence: number; primitive_id: AssistantMcpDiscoveryPrimitive; step_status: AssistantMcpDiscoveryRuntimeStepStatus; purpose: string; provided_axes: string[]; required_axis_options: string[][]; missing_axis_options: string[][]; optional_axes: string[]; expected_fact_kinds: string[]; evidence_floor: AssistantMcpCatalogEvidenceFloor; runtime_must_execute: true; dry_run_only: true; stop_condition: string; } export interface AssistantMcpDiscoveryEvidenceGateRequirement { required: true; expected_inputs: string[]; answer_may_use_raw_model_claims: false; } export interface AssistantMcpDiscoveryRuntimeDryRunContract { schema_version: typeof ASSISTANT_MCP_DISCOVERY_RUNTIME_DRY_RUN_SCHEMA_VERSION; policy_owner: "assistantMcpDiscoveryRuntimeAdapter"; adapter_status: AssistantMcpDiscoveryRuntimeAdapterStatus; planner_status: AssistantMcpDiscoveryPlannerStatus; mcp_execution_performed: false; execution_steps: AssistantMcpDiscoveryRuntimeStepContract[]; execution_budget: { max_probe_count: number; max_rows_per_probe: number; }; evidence_gate: AssistantMcpDiscoveryEvidenceGateRequirement; user_facing_fallback: string | null; reason_codes: string[]; } function normalizeReasonCode(value: string): string | null { const normalized = value .trim() .replace(/[^\p{L}\p{N}_.:-]+/gu, "_") .replace(/^_+|_+$/g, "") .toLowerCase(); return normalized.length > 0 ? normalized.slice(0, 120) : null; } function pushReason(target: string[], value: string): void { const normalized = normalizeReasonCode(value); if (normalized && !target.includes(normalized)) { target.push(normalized); } } function uniqueStrings(values: string[]): string[] { const result: string[] = []; for (const value of values) { const text = value.trim(); if (text && !result.includes(text)) { result.push(text); } } return result; } function stepStatusFor(input: { adapterStatus: AssistantMcpDiscoveryRuntimeAdapterStatus; missingAxisOptions: string[][]; }): AssistantMcpDiscoveryRuntimeStepStatus { if (input.adapterStatus === "blocked") { return "blocked"; } if (input.missingAxisOptions.length > 0) { return "missing_axes"; } return "ready"; } function stopConditionFor(evidenceFloor: AssistantMcpCatalogEvidenceFloor): string { if (evidenceFloor === "rows_matched") { return "stop_after_allowed_probe_returns_matched_rows_or_reports_no_match"; } if (evidenceFloor === "rows_received") { return "stop_after_allowed_probe_returns_rows_or_reports_empty_source"; } if (evidenceFloor === "source_summary") { return "stop_after_allowed_probe_returns_source_summary_or_limitation"; } return "stop_without_fact_claim"; } function adapterStatusFor(planner: AssistantMcpDiscoveryPlannerContract): AssistantMcpDiscoveryRuntimeAdapterStatus { if (planner.planner_status === "blocked" || planner.catalog_review.review_status === "catalog_blocked") { return "blocked"; } if (planner.planner_status !== "ready_for_execution" || planner.catalog_review.review_status !== "catalog_compatible") { return "needs_clarification"; } return "dry_run_ready"; } function fallbackFor(status: AssistantMcpDiscoveryRuntimeAdapterStatus): string | null { if (status === "dry_run_ready") { return null; } if (status === "blocked") { return "explain_that_runtime_policy_blocked_mcp_discovery"; } return "ask_for_missing_scope_before_mcp_discovery"; } export function buildAssistantMcpDiscoveryRuntimeDryRun( planner: AssistantMcpDiscoveryPlannerContract ): AssistantMcpDiscoveryRuntimeDryRunContract { const adapterStatus = adapterStatusFor(planner); const reasonCodes = uniqueStrings([ ...planner.reason_codes, ...planner.discovery_plan.reason_codes, ...planner.catalog_review.reason_codes ]); const providedAxes = uniqueStrings(planner.required_axes); const executionSteps = planner.discovery_plan.allowed_primitives.map((primitiveId, index) => { const catalog = getAssistantMcpCatalogPrimitive(primitiveId); const missingAxisOptions = planner.catalog_review.missing_axes_by_primitive[primitiveId] ?? []; return { sequence: index + 1, primitive_id: primitiveId, step_status: stepStatusFor({ adapterStatus, missingAxisOptions }), purpose: catalog.purpose, provided_axes: providedAxes, required_axis_options: catalog.required_axes_any_of, missing_axis_options: missingAxisOptions, optional_axes: catalog.optional_axes, expected_fact_kinds: catalog.output_fact_kinds, evidence_floor: catalog.evidence_floor, runtime_must_execute: true, dry_run_only: true, stop_condition: stopConditionFor(catalog.evidence_floor) } satisfies AssistantMcpDiscoveryRuntimeStepContract; }); if (adapterStatus === "dry_run_ready") { pushReason(reasonCodes, "runtime_dry_run_ready_without_mcp_execution"); } else if (adapterStatus === "blocked") { pushReason(reasonCodes, "runtime_dry_run_blocked_before_mcp_execution"); } else { pushReason(reasonCodes, "runtime_dry_run_needs_clarification_before_mcp_execution"); } return { schema_version: ASSISTANT_MCP_DISCOVERY_RUNTIME_DRY_RUN_SCHEMA_VERSION, policy_owner: "assistantMcpDiscoveryRuntimeAdapter", adapter_status: adapterStatus, planner_status: planner.planner_status, mcp_execution_performed: false, execution_steps: executionSteps, execution_budget: planner.discovery_plan.execution_budget, evidence_gate: { required: true, expected_inputs: [ "probe_results", "confirmed_facts", "inferred_facts", "unknown_facts", "source_rows_summary", "query_limitations" ], answer_may_use_raw_model_claims: false }, user_facing_fallback: fallbackFor(adapterStatus), reason_codes: reasonCodes }; }