diff --git a/llm_normalizer/backend/dist/services/assistantAddressLaneResponseRuntimeAdapter.js b/llm_normalizer/backend/dist/services/assistantAddressLaneResponseRuntimeAdapter.js index 2c275c1..189cd39 100644 --- a/llm_normalizer/backend/dist/services/assistantAddressLaneResponseRuntimeAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantAddressLaneResponseRuntimeAdapter.js @@ -2,6 +2,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.runAssistantAddressLaneResponseRuntime = runAssistantAddressLaneResponseRuntime; const assistantAddressTurnFinalizeRuntimeAdapter_1 = require("./assistantAddressTurnFinalizeRuntimeAdapter"); +const assistantRuntimeContractResolver_1 = require("./assistantRuntimeContractResolver"); function toRecordObject(value) { if (!value || typeof value !== "object") { return null; @@ -187,6 +188,10 @@ function runAssistantAddressLaneResponseRuntime(input) { period_to: input.toNonEmptyString(rootFilters?.period_to) }; } + const debugWithRuntimeContracts = (0, assistantRuntimeContractResolver_1.attachAssistantRuntimeContractShadow)(debug, { + userMessage: input.userMessage, + addressRuntimeMeta: input.llmPreDecomposeMeta + }); const finalization = finalizeAddressTurnSafe({ sessionId: input.sessionId, userMessage: input.userMessage, @@ -194,7 +199,7 @@ function runAssistantAddressLaneResponseRuntime(input) { assistantReply: safeAddressReply, replyType: normalizeAddressReplyType(input.addressLane.reply_type), addressLaneDebug: normalizeAddressLaneDebug(input.addressLane.debug), - debug, + debug: debugWithRuntimeContracts, carryoverMeta: normalizeCarryoverMeta(input.carryoverMeta), llmPreDecomposeMeta: normalizeLlmPreDecomposeMeta(input.llmPreDecomposeMeta), appendItem: input.appendItem, @@ -206,6 +211,6 @@ function runAssistantAddressLaneResponseRuntime(input) { }); return { response: finalization.response, - debug + debug: debugWithRuntimeContracts }; } diff --git a/llm_normalizer/backend/dist/services/assistantDebugPayloadAssembler.js b/llm_normalizer/backend/dist/services/assistantDebugPayloadAssembler.js index fba2ce4..a51789b 100644 --- a/llm_normalizer/backend/dist/services/assistantDebugPayloadAssembler.js +++ b/llm_normalizer/backend/dist/services/assistantDebugPayloadAssembler.js @@ -1,6 +1,7 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.buildDeepAnalysisDebugPayload = buildDeepAnalysisDebugPayload; +const assistantRuntimeContractResolver_1 = require("./assistantRuntimeContractResolver"); const assistantStage4AnswerContractAudit_1 = require("./assistantStage4AnswerContractAudit"); function toAnalysisContext(input) { if (!input.active) { @@ -17,7 +18,7 @@ function toAnalysisContext(input) { function buildDeepAnalysisDebugPayload(input) { const analysisContext = toAnalysisContext(input.runtimeAnalysisContext); const answerContractStage4Audit = (0, assistantStage4AnswerContractAudit_1.buildStage4AnswerContractAuditV1)(input.assistantReply); - return { + const debugPayload = { trace_id: input.traceId, prompt_version: input.promptVersion, schema_version: input.schemaVersion, @@ -93,4 +94,8 @@ function buildDeepAnalysisDebugPayload(input) { investigation_state_snapshot: input.investigationStateSnapshot, normalized: input.normalizedPayload }; + return (0, assistantRuntimeContractResolver_1.attachAssistantRuntimeContractShadow)(debugPayload, { + addressRuntimeMeta: input.addressRuntimeMetaForDeep, + groundingStatus: input.groundingCheck.status + }); } diff --git a/llm_normalizer/backend/dist/services/assistantRuntimeContractResolver.js b/llm_normalizer/backend/dist/services/assistantRuntimeContractResolver.js new file mode 100644 index 0000000..1b9cb27 --- /dev/null +++ b/llm_normalizer/backend/dist/services/assistantRuntimeContractResolver.js @@ -0,0 +1,220 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.resolveAssistantRuntimeContractShadow = resolveAssistantRuntimeContractShadow; +exports.buildAssistantRuntimeContractShadowFields = buildAssistantRuntimeContractShadowFields; +exports.attachAssistantRuntimeContractShadow = attachAssistantRuntimeContractShadow; +const assistantRuntimeContracts_1 = require("../types/assistantRuntimeContracts"); +const assistantRuntimeContractRegistry_1 = require("./assistantRuntimeContractRegistry"); +function toRecordObject(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return null; + } + return value; +} +function toNonEmptyString(value) { + if (value === null || value === undefined) { + return null; + } + const text = String(value).trim(); + return text.length > 0 ? text : null; +} +function isAddressIntent(value) { + return Boolean(value) && value !== "unknown"; +} +function asNumber(value) { + if (typeof value === "number" && Number.isFinite(value)) { + return value; + } + if (typeof value === "string" && value.trim().length > 0) { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; + } + return null; +} +function runtimeMetaFrom(input) { + const direct = toRecordObject(input.addressRuntimeMeta); + if (direct) { + return direct; + } + const debug = toRecordObject(input.addressDebug); + if (!debug) { + return null; + } + const dialogContinuationContract = toRecordObject(debug.dialog_continuation_contract_v2); + const orchestrationContract = toRecordObject(debug.orchestration_contract_v1); + if (!dialogContinuationContract && !orchestrationContract) { + return null; + } + return { + dialogContinuationContract, + orchestrationContract, + toolGateDecision: debug.tool_gate_decision, + toolGateReason: debug.tool_gate_reason + }; +} +function resolveCapabilityContractId(debug, meta) { + const explicitCapabilityId = toNonEmptyString(debug.capability_id); + if (explicitCapabilityId && (0, assistantRuntimeContractRegistry_1.getAssistantCapabilityContract)(explicitCapabilityId)) { + return { + capabilityId: explicitCapabilityId, + reasons: ["debug_capability_id_matched_contract"] + }; + } + const orchestrationContract = toRecordObject(meta?.orchestrationContract) ?? toRecordObject(debug.orchestration_contract_v1); + const intent = toNonEmptyString(debug.detected_intent) ?? + toNonEmptyString(orchestrationContract?.address_intent); + if (isAddressIntent(intent)) { + const contract = (0, assistantRuntimeContractRegistry_1.getAssistantCapabilityContractByIntent)(intent); + if (contract) { + return { + capabilityId: contract.capability_id, + reasons: ["intent_matched_capability_contract"] + }; + } + } + return { + capabilityId: null, + reasons: explicitCapabilityId ? ["debug_capability_id_has_no_contract"] : ["capability_contract_not_resolved"] + }; +} +function isSelectedObjectCapability(capabilityId) { + if (!capabilityId) { + return false; + } + const contract = (0, assistantRuntimeContractRegistry_1.getAssistantCapabilityContract)(capabilityId); + return Boolean(contract?.requires_focus_object); +} +function resolveTransitionId(input) { + const dialogContinuationContract = toRecordObject(input.meta?.dialogContinuationContract) ?? + toRecordObject(input.debug.dialog_continuation_contract_v2); + const orchestrationContract = toRecordObject(input.meta?.orchestrationContract) ?? + toRecordObject(input.debug.orchestration_contract_v1); + const finalDecision = toRecordObject(orchestrationContract?.final_decision); + const hardMetaMode = toNonEmptyString(orchestrationContract?.hard_meta_mode); + const toolGateReason = toNonEmptyString(input.meta?.toolGateReason) ?? + toNonEmptyString(input.debug.tool_gate_reason) ?? + toNonEmptyString(finalDecision?.tool_gate_reason); + const continuationDecision = toNonEmptyString(dialogContinuationContract?.decision); + const targetIntent = toNonEmptyString(dialogContinuationContract?.target_intent) ?? + toNonEmptyString(input.debug.detected_intent); + const currentFrameKind = toNonEmptyString(toRecordObject(input.debug.address_root_frame_context)?.current_frame_kind); + const missingFilters = Array.isArray(input.debug.missing_required_filters) ? input.debug.missing_required_filters.length : 0; + if (input.groundingStatus === "route_mismatch_blocked" || + input.debug.route_expectation_status === "mismatch" || + input.debug.limited_reason_category === "execution_error") { + return { transitionId: "T10", reasons: ["blocked_or_route_mismatch_debug_status"] }; + } + if (missingFilters > 0 || input.debug.limited_reason_category === "missing_anchor") { + return { transitionId: "T7", reasons: ["missing_anchor_or_filter_requires_clarification"] }; + } + if (toolGateReason === "memory_recap_followup_detected") { + return { transitionId: "T9", reasons: ["memory_recap_tool_gate_reason"] }; + } + if (hardMetaMode === "capability" || toolGateReason === "assistant_capability_query_detected") { + return { transitionId: "T8", reasons: ["capability_meta_followup_tool_gate_reason"] }; + } + if (isSelectedObjectCapability(input.capabilityId)) { + if (continuationDecision === "continue_previous") { + return { transitionId: "T4", reasons: ["selected_object_capability_continue_previous"] }; + } + if (continuationDecision === "new_topic") { + return { transitionId: "T3", reasons: ["selected_object_capability_explicit_entry"] }; + } + return { transitionId: "T5", reasons: ["selected_object_capability_without_explicit_continuation_decision"] }; + } + if (currentFrameKind === "inventory_drilldown" && targetIntent && targetIntent !== "inventory_on_hand_as_of_date") { + return { transitionId: "T6", reasons: ["root_context_reused_after_drilldown_context"] }; + } + if (continuationDecision === "continue_previous") { + return { transitionId: "T2", reasons: ["root_followup_continue_previous"] }; + } + if (input.capabilityId) { + return { transitionId: "T1", reasons: ["capability_contract_root_or_new_entry"] }; + } + return { transitionId: null, reasons: ["transition_contract_not_resolved"] }; +} +function resolveTruthGateStatus(input) { + if (input.groundingStatus === "route_mismatch_blocked" || input.debug.route_expectation_status === "mismatch") { + return "blocked_route_expectation_failure"; + } + if (input.debug.limited_reason_category === "execution_error") { + return "blocked_execution_error"; + } + if (input.debug.limited_reason_category === "missing_anchor") { + return "blocked_missing_anchor"; + } + if (input.debug.temporal_guard_outcome === "ambiguous_limited" || + input.debug.temporal_alignment_status === "conflicting") { + return "limited_temporal_or_contextual"; + } + if (input.debug.limited_reason_category || input.groundingStatus === "partial") { + return "partial_supported"; + } + const rowsMatched = asNumber(input.debug.rows_matched); + const routeExpectationStatus = toNonEmptyString(input.debug.route_expectation_status); + if ((input.groundingStatus === "grounded" || (rowsMatched !== null && rowsMatched > 0)) && (!routeExpectationStatus || routeExpectationStatus === "matched")) { + return "full_confirmed"; + } + return "unknown"; +} +function carryoverEligibilityFor(transitionId, truthGateStatus) { + if (truthGateStatus.startsWith("blocked")) { + return "none"; + } + if (transitionId === "T3" || transitionId === "T4" || transitionId === "T5") { + return "object_only"; + } + if (transitionId === "T8" || transitionId === "T9") { + return "meta_only"; + } + if (transitionId === "T1" || transitionId === "T2" || transitionId === "T6") { + return "root_only"; + } + if (transitionId === "T7") { + return "full"; + } + return "none"; +} +function resolveAssistantRuntimeContractShadow(input) { + const debug = toRecordObject(input.addressDebug) ?? {}; + const meta = runtimeMetaFrom(input); + const groundingStatus = toNonEmptyString(input.groundingStatus) ?? + toNonEmptyString(toRecordObject(debug.answer_grounding_check)?.status); + const capability = resolveCapabilityContractId(debug, meta); + const transition = resolveTransitionId({ + debug, + meta, + capabilityId: capability.capabilityId, + groundingStatus + }); + const transitionContract = transition.transitionId ? (0, assistantRuntimeContractRegistry_1.getAssistantTransitionContract)(transition.transitionId) : null; + const truthGateStatus = resolveTruthGateStatus({ debug, groundingStatus }); + return { + schema_version: assistantRuntimeContracts_1.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION, + transition_contract_id: transition.transitionId, + transition_contract_title: transitionContract?.title ?? null, + transition_contract_reason: transition.reasons, + capability_contract_id: capability.capabilityId, + capability_contract_reason: capability.reasons, + truth_gate_contract_status: truthGateStatus, + carryover_eligibility: carryoverEligibilityFor(transition.transitionId, truthGateStatus) + }; +} +function buildAssistantRuntimeContractShadowFields(input) { + const decision = resolveAssistantRuntimeContractShadow(input); + return { + assistant_runtime_contract_v1: decision, + transition_contract_id: decision.transition_contract_id, + capability_contract_id: decision.capability_contract_id, + truth_gate_contract_status: decision.truth_gate_contract_status + }; +} +function attachAssistantRuntimeContractShadow(debugPayload, input) { + return { + ...debugPayload, + ...buildAssistantRuntimeContractShadowFields({ + ...input, + addressDebug: debugPayload + }) + }; +} diff --git a/llm_normalizer/backend/src/services/assistantAddressLaneResponseRuntimeAdapter.ts b/llm_normalizer/backend/src/services/assistantAddressLaneResponseRuntimeAdapter.ts index f060f8a..624388c 100644 --- a/llm_normalizer/backend/src/services/assistantAddressLaneResponseRuntimeAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantAddressLaneResponseRuntimeAdapter.ts @@ -7,6 +7,7 @@ import { type AddressLlmPreDecomposeMetaLogInput, type FinalizeAssistantAddressTurnInput } from "./assistantAddressTurnFinalizeRuntimeAdapter"; +import { attachAssistantRuntimeContractShadow } from "./assistantRuntimeContractResolver"; export interface RunAssistantAddressLaneResponseRuntimeInput { sessionId: string; @@ -246,6 +247,10 @@ export function runAssistantAddressLaneResponseRuntime, { + addressRuntimeMeta: input.addressRuntimeMetaForDeep as unknown as Record | null | undefined, + groundingStatus: input.groundingCheck.status + }) as unknown as AssistantDebugPayload; } diff --git a/llm_normalizer/backend/src/services/assistantRuntimeContractResolver.ts b/llm_normalizer/backend/src/services/assistantRuntimeContractResolver.ts new file mode 100644 index 0000000..9c86a34 --- /dev/null +++ b/llm_normalizer/backend/src/services/assistantRuntimeContractResolver.ts @@ -0,0 +1,294 @@ +import { + ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION, + type AssistantCarryoverDepth, + type AssistantRuntimeContractShadowDecision, + type AssistantTransitionClassId +} from "../types/assistantRuntimeContracts"; +import type { AddressIntent } from "../types/addressQuery"; +import { + getAssistantCapabilityContract, + getAssistantCapabilityContractByIntent, + getAssistantTransitionContract +} from "./assistantRuntimeContractRegistry"; + +export interface ResolveAssistantRuntimeContractShadowInput { + userMessage?: unknown; + addressDebug?: Record | null; + addressRuntimeMeta?: Record | null; + groundingStatus?: unknown; +} + +export interface AssistantRuntimeContractShadowFields { + assistant_runtime_contract_v1: AssistantRuntimeContractShadowDecision; + transition_contract_id: AssistantTransitionClassId | null; + capability_contract_id: string | null; + truth_gate_contract_status: AssistantRuntimeContractShadowDecision["truth_gate_contract_status"]; +} + +function toRecordObject(value: unknown): Record | null { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return null; + } + return value as Record; +} + +function toNonEmptyString(value: unknown): string | null { + if (value === null || value === undefined) { + return null; + } + const text = String(value).trim(); + return text.length > 0 ? text : null; +} + +function isAddressIntent(value: string | null): value is AddressIntent { + return Boolean(value) && value !== "unknown"; +} + +function asNumber(value: unknown): number | null { + if (typeof value === "number" && Number.isFinite(value)) { + return value; + } + if (typeof value === "string" && value.trim().length > 0) { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; + } + return null; +} + +function runtimeMetaFrom(input: ResolveAssistantRuntimeContractShadowInput): Record | null { + const direct = toRecordObject(input.addressRuntimeMeta); + if (direct) { + return direct; + } + const debug = toRecordObject(input.addressDebug); + if (!debug) { + return null; + } + const dialogContinuationContract = toRecordObject(debug.dialog_continuation_contract_v2); + const orchestrationContract = toRecordObject(debug.orchestration_contract_v1); + if (!dialogContinuationContract && !orchestrationContract) { + return null; + } + return { + dialogContinuationContract, + orchestrationContract, + toolGateDecision: debug.tool_gate_decision, + toolGateReason: debug.tool_gate_reason + }; +} + +function resolveCapabilityContractId(debug: Record, meta: Record | null): { + capabilityId: string | null; + reasons: string[]; +} { + const explicitCapabilityId = toNonEmptyString(debug.capability_id); + if (explicitCapabilityId && getAssistantCapabilityContract(explicitCapabilityId)) { + return { + capabilityId: explicitCapabilityId, + reasons: ["debug_capability_id_matched_contract"] + }; + } + + const orchestrationContract = toRecordObject(meta?.orchestrationContract) ?? toRecordObject(debug.orchestration_contract_v1); + const intent = + toNonEmptyString(debug.detected_intent) ?? + toNonEmptyString(orchestrationContract?.address_intent); + if (isAddressIntent(intent)) { + const contract = getAssistantCapabilityContractByIntent(intent); + if (contract) { + return { + capabilityId: contract.capability_id, + reasons: ["intent_matched_capability_contract"] + }; + } + } + + return { + capabilityId: null, + reasons: explicitCapabilityId ? ["debug_capability_id_has_no_contract"] : ["capability_contract_not_resolved"] + }; +} + +function isSelectedObjectCapability(capabilityId: string | null): boolean { + if (!capabilityId) { + return false; + } + const contract = getAssistantCapabilityContract(capabilityId); + return Boolean(contract?.requires_focus_object); +} + +function resolveTransitionId(input: { + debug: Record; + meta: Record | null; + capabilityId: string | null; + groundingStatus: string | null; +}): { transitionId: AssistantTransitionClassId | null; reasons: string[] } { + const dialogContinuationContract = + toRecordObject(input.meta?.dialogContinuationContract) ?? + toRecordObject(input.debug.dialog_continuation_contract_v2); + const orchestrationContract = + toRecordObject(input.meta?.orchestrationContract) ?? + toRecordObject(input.debug.orchestration_contract_v1); + const finalDecision = toRecordObject(orchestrationContract?.final_decision); + const hardMetaMode = toNonEmptyString(orchestrationContract?.hard_meta_mode); + const toolGateReason = + toNonEmptyString(input.meta?.toolGateReason) ?? + toNonEmptyString(input.debug.tool_gate_reason) ?? + toNonEmptyString(finalDecision?.tool_gate_reason); + const continuationDecision = toNonEmptyString(dialogContinuationContract?.decision); + const targetIntent = + toNonEmptyString(dialogContinuationContract?.target_intent) ?? + toNonEmptyString(input.debug.detected_intent); + const currentFrameKind = toNonEmptyString(toRecordObject(input.debug.address_root_frame_context)?.current_frame_kind); + const missingFilters = Array.isArray(input.debug.missing_required_filters) ? input.debug.missing_required_filters.length : 0; + + if ( + input.groundingStatus === "route_mismatch_blocked" || + input.debug.route_expectation_status === "mismatch" || + input.debug.limited_reason_category === "execution_error" + ) { + return { transitionId: "T10", reasons: ["blocked_or_route_mismatch_debug_status"] }; + } + + if (missingFilters > 0 || input.debug.limited_reason_category === "missing_anchor") { + return { transitionId: "T7", reasons: ["missing_anchor_or_filter_requires_clarification"] }; + } + + if (toolGateReason === "memory_recap_followup_detected") { + return { transitionId: "T9", reasons: ["memory_recap_tool_gate_reason"] }; + } + + if (hardMetaMode === "capability" || toolGateReason === "assistant_capability_query_detected") { + return { transitionId: "T8", reasons: ["capability_meta_followup_tool_gate_reason"] }; + } + + if (isSelectedObjectCapability(input.capabilityId)) { + if (continuationDecision === "continue_previous") { + return { transitionId: "T4", reasons: ["selected_object_capability_continue_previous"] }; + } + if (continuationDecision === "new_topic") { + return { transitionId: "T3", reasons: ["selected_object_capability_explicit_entry"] }; + } + return { transitionId: "T5", reasons: ["selected_object_capability_without_explicit_continuation_decision"] }; + } + + if (currentFrameKind === "inventory_drilldown" && targetIntent && targetIntent !== "inventory_on_hand_as_of_date") { + return { transitionId: "T6", reasons: ["root_context_reused_after_drilldown_context"] }; + } + + if (continuationDecision === "continue_previous") { + return { transitionId: "T2", reasons: ["root_followup_continue_previous"] }; + } + + if (input.capabilityId) { + return { transitionId: "T1", reasons: ["capability_contract_root_or_new_entry"] }; + } + + return { transitionId: null, reasons: ["transition_contract_not_resolved"] }; +} + +function resolveTruthGateStatus(input: { + debug: Record; + groundingStatus: string | null; +}): AssistantRuntimeContractShadowDecision["truth_gate_contract_status"] { + if (input.groundingStatus === "route_mismatch_blocked" || input.debug.route_expectation_status === "mismatch") { + return "blocked_route_expectation_failure"; + } + if (input.debug.limited_reason_category === "execution_error") { + return "blocked_execution_error"; + } + if (input.debug.limited_reason_category === "missing_anchor") { + return "blocked_missing_anchor"; + } + if ( + input.debug.temporal_guard_outcome === "ambiguous_limited" || + input.debug.temporal_alignment_status === "conflicting" + ) { + return "limited_temporal_or_contextual"; + } + if (input.debug.limited_reason_category || input.groundingStatus === "partial") { + return "partial_supported"; + } + const rowsMatched = asNumber(input.debug.rows_matched); + const routeExpectationStatus = toNonEmptyString(input.debug.route_expectation_status); + if ((input.groundingStatus === "grounded" || (rowsMatched !== null && rowsMatched > 0)) && (!routeExpectationStatus || routeExpectationStatus === "matched")) { + return "full_confirmed"; + } + return "unknown"; +} + +function carryoverEligibilityFor( + transitionId: AssistantTransitionClassId | null, + truthGateStatus: AssistantRuntimeContractShadowDecision["truth_gate_contract_status"] +): AssistantCarryoverDepth { + if (truthGateStatus.startsWith("blocked")) { + return "none"; + } + if (transitionId === "T3" || transitionId === "T4" || transitionId === "T5") { + return "object_only"; + } + if (transitionId === "T8" || transitionId === "T9") { + return "meta_only"; + } + if (transitionId === "T1" || transitionId === "T2" || transitionId === "T6") { + return "root_only"; + } + if (transitionId === "T7") { + return "full"; + } + return "none"; +} + +export function resolveAssistantRuntimeContractShadow( + input: ResolveAssistantRuntimeContractShadowInput +): AssistantRuntimeContractShadowDecision { + const debug = toRecordObject(input.addressDebug) ?? {}; + const meta = runtimeMetaFrom(input); + const groundingStatus = + toNonEmptyString(input.groundingStatus) ?? + toNonEmptyString(toRecordObject(debug.answer_grounding_check)?.status); + const capability = resolveCapabilityContractId(debug, meta); + const transition = resolveTransitionId({ + debug, + meta, + capabilityId: capability.capabilityId, + groundingStatus + }); + const transitionContract = transition.transitionId ? getAssistantTransitionContract(transition.transitionId) : null; + const truthGateStatus = resolveTruthGateStatus({ debug, groundingStatus }); + return { + schema_version: ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION, + transition_contract_id: transition.transitionId, + transition_contract_title: transitionContract?.title ?? null, + transition_contract_reason: transition.reasons, + capability_contract_id: capability.capabilityId, + capability_contract_reason: capability.reasons, + truth_gate_contract_status: truthGateStatus, + carryover_eligibility: carryoverEligibilityFor(transition.transitionId, truthGateStatus) + }; +} + +export function buildAssistantRuntimeContractShadowFields( + input: ResolveAssistantRuntimeContractShadowInput +): AssistantRuntimeContractShadowFields { + const decision = resolveAssistantRuntimeContractShadow(input); + return { + assistant_runtime_contract_v1: decision, + transition_contract_id: decision.transition_contract_id, + capability_contract_id: decision.capability_contract_id, + truth_gate_contract_status: decision.truth_gate_contract_status + }; +} + +export function attachAssistantRuntimeContractShadow>( + debugPayload: T, + input: Omit +): T & AssistantRuntimeContractShadowFields { + return { + ...debugPayload, + ...buildAssistantRuntimeContractShadowFields({ + ...input, + addressDebug: debugPayload + }) + }; +} diff --git a/llm_normalizer/backend/src/types/assistant.ts b/llm_normalizer/backend/src/types/assistant.ts index 68a6132..1aafca4 100644 --- a/llm_normalizer/backend/src/types/assistant.ts +++ b/llm_normalizer/backend/src/types/assistant.ts @@ -17,6 +17,10 @@ import type { } from "./stage2ProblemUnits"; import type { AccountingGraphBuildResult } from "./stage4Graph"; import type { AddressNavigationState } from "./addressNavigation"; +import type { + AssistantRuntimeContractShadowDecision, + AssistantTransitionClassId +} from "./assistantRuntimeContracts"; export type AssistantFallbackType = "none" | "out_of_scope" | "clarification" | "partial" | "unknown"; export type AssistantReplyType = @@ -446,6 +450,10 @@ export interface AssistantDebugPayload { route_expectation_expected_selected_recipes?: string[]; route_expectation_expected_requested_result_modes?: Array<"heuristic_candidates" | "confirmed_balance">; route_expectation_expected_result_modes?: Array<"heuristic_candidates" | "confirmed_balance">; + assistant_runtime_contract_v1?: AssistantRuntimeContractShadowDecision; + transition_contract_id?: AssistantTransitionClassId | null; + capability_contract_id?: string | null; + truth_gate_contract_status?: AssistantRuntimeContractShadowDecision["truth_gate_contract_status"]; execution_lane?: "address_query" | "deep_analysis"; llm_decomposition_applied?: boolean; llm_decomposition_attempted?: boolean; diff --git a/llm_normalizer/backend/src/types/assistantRuntimeContracts.ts b/llm_normalizer/backend/src/types/assistantRuntimeContracts.ts index 17cc204..47aba41 100644 --- a/llm_normalizer/backend/src/types/assistantRuntimeContracts.ts +++ b/llm_normalizer/backend/src/types/assistantRuntimeContracts.ts @@ -152,3 +152,21 @@ export interface AssistantCapabilityContract { required_transition_tests: string[]; required_scenario_families: string[]; } + +export interface AssistantRuntimeContractShadowDecision { + schema_version: typeof ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION; + transition_contract_id: AssistantTransitionClassId | null; + transition_contract_title: string | null; + transition_contract_reason: string[]; + capability_contract_id: string | null; + capability_contract_reason: string[]; + truth_gate_contract_status: + | "full_confirmed" + | "partial_supported" + | "blocked_missing_anchor" + | "blocked_route_expectation_failure" + | "blocked_execution_error" + | "limited_temporal_or_contextual" + | "unknown"; + carryover_eligibility: AssistantCarryoverDepth; +} diff --git a/llm_normalizer/backend/tests/assistantAddressLaneResponseRuntimeAdapter.test.ts b/llm_normalizer/backend/tests/assistantAddressLaneResponseRuntimeAdapter.test.ts index 334a1b1..dd8521c 100644 --- a/llm_normalizer/backend/tests/assistantAddressLaneResponseRuntimeAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantAddressLaneResponseRuntimeAdapter.test.ts @@ -61,12 +61,15 @@ describe("assistant address lane response runtime adapter", () => { expect.objectContaining({ assistant_known_organizations: ["ООО Ромашка", "ООО Лютик"], assistant_active_organization: "ООО Ромашка", - address_followup_offer: { suggestion: "continue_previous" } + address_followup_offer: { suggestion: "continue_previous" }, + assistant_runtime_contract_v1: expect.objectContaining({ + schema_version: "assistant_runtime_contracts_v1" + }) }) ); }); - it("keeps debug minimal when optional enrichment is absent", () => { + it("keeps debug bounded to shadow contracts when optional enrichment is absent", () => { const runtime = runAssistantAddressLaneResponseRuntime({ sessionId: "asst-2", userMessage: "raw", @@ -97,7 +100,18 @@ describe("assistant address lane response runtime adapter", () => { }) }); - expect(runtime.debug).toEqual({}); + expect(runtime.debug).toEqual( + expect.objectContaining({ + assistant_runtime_contract_v1: expect.objectContaining({ + transition_contract_id: null, + capability_contract_id: null, + truth_gate_contract_status: "unknown" + }), + transition_contract_id: null, + capability_contract_id: null, + truth_gate_contract_status: "unknown" + }) + ); expect(runtime.response).toEqual({ ok: true }); }); }); diff --git a/llm_normalizer/backend/tests/assistantRuntimeContractRegistry.test.ts b/llm_normalizer/backend/tests/assistantRuntimeContractRegistry.test.ts index 39618ef..d5fc71f 100644 --- a/llm_normalizer/backend/tests/assistantRuntimeContractRegistry.test.ts +++ b/llm_normalizer/backend/tests/assistantRuntimeContractRegistry.test.ts @@ -7,6 +7,7 @@ import { listAssistantTransitionContracts, listInventoryCapabilityContracts } from "../src/services/assistantRuntimeContractRegistry"; +import { resolveAssistantRuntimeContractShadow } from "../src/services/assistantRuntimeContractResolver"; describe("assistant runtime contract registry", () => { it("declares the architecture turnaround transition set T1-T10", () => { @@ -81,4 +82,65 @@ describe("assistant runtime contract registry", () => { expect(contract.execution_error_behavior).toBe("blocked_execution_error"); } }); + + it("resolves shadow contract ids for selected-object provenance debug", () => { + const decision = resolveAssistantRuntimeContractShadow({ + addressDebug: { + detected_intent: "inventory_purchase_provenance_for_item", + capability_id: "inventory_inventory_purchase_provenance_for_item", + rows_matched: 1, + route_expectation_status: "matched" + }, + addressRuntimeMeta: { + dialogContinuationContract: { + decision: "continue_previous", + target_intent: "inventory_purchase_provenance_for_item" + } + }, + groundingStatus: "grounded" + }); + + expect(decision.transition_contract_id).toBe("T4"); + expect(decision.capability_contract_id).toBe("inventory_inventory_purchase_provenance_for_item"); + expect(decision.truth_gate_contract_status).toBe("full_confirmed"); + expect(decision.carryover_eligibility).toBe("object_only"); + }); + + it("resolves meta follow-up and blocked route expectation in shadow mode", () => { + const metaDecision = resolveAssistantRuntimeContractShadow({ + addressRuntimeMeta: { + toolGateReason: "assistant_capability_query_detected", + orchestrationContract: { + hard_meta_mode: "capability", + address_intent: "inventory_on_hand_as_of_date" + } + } + }); + expect(metaDecision.transition_contract_id).toBe("T8"); + expect(metaDecision.capability_contract_id).toBe("confirmed_inventory_on_hand_as_of_date"); + expect(metaDecision.carryover_eligibility).toBe("meta_only"); + + const blockedDecision = resolveAssistantRuntimeContractShadow({ + addressDebug: { + capability_id: "confirmed_inventory_on_hand_as_of_date", + route_expectation_status: "mismatch" + }, + groundingStatus: "route_mismatch_blocked" + }); + expect(blockedDecision.transition_contract_id).toBe("T10"); + expect(blockedDecision.truth_gate_contract_status).toBe("blocked_route_expectation_failure"); + expect(blockedDecision.carryover_eligibility).toBe("none"); + }); + + it("classifies temporal limitations as a distinct truth gate status", () => { + const decision = resolveAssistantRuntimeContractShadow({ + addressDebug: { + capability_id: "confirmed_inventory_on_hand_as_of_date", + temporal_guard_outcome: "ambiguous_limited" + }, + groundingStatus: "partial" + }); + + expect(decision.truth_gate_contract_status).toBe("limited_temporal_or_contextual"); + }); });