From 1c928a5d68612ffe9b2ff5c3715a6052f8877d5e Mon Sep 17 00:00:00 2001 From: dctouch Date: Wed, 15 Apr 2026 23:41:57 +0300 Subject: [PATCH] =?UTF-8?q?=D0=90=D0=A0=D0=A7=20=D0=90=D0=9F11=20-=20?= =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20capability?= =?UTF-8?q?=20runtime=20binding=20=D0=B4=D0=BB=D1=8F=20assistant=20contrac?= =?UTF-8?q?ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...istantAddressLaneResponseRuntimeAdapter.js | 9 +- ...ssistantCapabilityRuntimeBindingAdapter.js | 300 +++++++++++++ .../assistantDebugPayloadAssembler.js | 9 +- .../dist/types/assistantRuntimeContracts.js | 3 +- ...istantAddressLaneResponseRuntimeAdapter.ts | 9 +- ...ssistantCapabilityRuntimeBindingAdapter.ts | 394 ++++++++++++++++++ .../assistantDebugPayloadAssembler.ts | 9 +- llm_normalizer/backend/src/types/assistant.ts | 9 + .../src/types/assistantRuntimeContracts.ts | 37 ++ ...tAddressLaneResponseRuntimeAdapter.test.ts | 12 +- ...antCapabilityRuntimeBindingAdapter.test.ts | 146 +++++++ .../assistantDebugPayloadAssembler.test.ts | 8 + 12 files changed, 937 insertions(+), 8 deletions(-) create mode 100644 llm_normalizer/backend/dist/services/assistantCapabilityRuntimeBindingAdapter.js create mode 100644 llm_normalizer/backend/src/services/assistantCapabilityRuntimeBindingAdapter.ts create mode 100644 llm_normalizer/backend/tests/assistantCapabilityRuntimeBindingAdapter.test.ts diff --git a/llm_normalizer/backend/dist/services/assistantAddressLaneResponseRuntimeAdapter.js b/llm_normalizer/backend/dist/services/assistantAddressLaneResponseRuntimeAdapter.js index 1d1637a..e158b31 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 assistantCapabilityRuntimeBindingAdapter_1 = require("./assistantCapabilityRuntimeBindingAdapter"); const assistantRuntimeContractResolver_1 = require("./assistantRuntimeContractResolver"); const assistantStateTransitionRuntimeAdapter_1 = require("./assistantStateTransitionRuntimeAdapter"); const assistantTruthAnswerPolicyRuntimeAdapter_1 = require("./assistantTruthAnswerPolicyRuntimeAdapter"); @@ -202,6 +203,10 @@ function runAssistantAddressLaneResponseRuntime(input) { addressRuntimeMeta: input.llmPreDecomposeMeta, replyType: normalizeAddressReplyType(input.addressLane.reply_type) }); + const debugWithCapabilityBinding = (0, assistantCapabilityRuntimeBindingAdapter_1.attachAssistantCapabilityRuntimeBinding)(debugWithStateTransition, { + addressRuntimeMeta: input.llmPreDecomposeMeta, + replyType: normalizeAddressReplyType(input.addressLane.reply_type) + }); const finalization = finalizeAddressTurnSafe({ sessionId: input.sessionId, userMessage: input.userMessage, @@ -209,7 +214,7 @@ function runAssistantAddressLaneResponseRuntime(input) { assistantReply: safeAddressReply, replyType: normalizeAddressReplyType(input.addressLane.reply_type), addressLaneDebug: normalizeAddressLaneDebug(input.addressLane.debug), - debug: debugWithStateTransition, + debug: debugWithCapabilityBinding, carryoverMeta: normalizeCarryoverMeta(input.carryoverMeta), llmPreDecomposeMeta: normalizeLlmPreDecomposeMeta(input.llmPreDecomposeMeta), appendItem: input.appendItem, @@ -221,6 +226,6 @@ function runAssistantAddressLaneResponseRuntime(input) { }); return { response: finalization.response, - debug: debugWithStateTransition + debug: debugWithCapabilityBinding }; } diff --git a/llm_normalizer/backend/dist/services/assistantCapabilityRuntimeBindingAdapter.js b/llm_normalizer/backend/dist/services/assistantCapabilityRuntimeBindingAdapter.js new file mode 100644 index 0000000..300c7d2 --- /dev/null +++ b/llm_normalizer/backend/dist/services/assistantCapabilityRuntimeBindingAdapter.js @@ -0,0 +1,300 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.resolveAssistantCapabilityRuntimeBinding = resolveAssistantCapabilityRuntimeBinding; +exports.buildAssistantCapabilityRuntimeBindingFields = buildAssistantCapabilityRuntimeBindingFields; +exports.attachAssistantCapabilityRuntimeBinding = attachAssistantCapabilityRuntimeBinding; +const assistantRuntimeContracts_1 = require("../types/assistantRuntimeContracts"); +const assistantRuntimeContractRegistry_1 = require("./assistantRuntimeContractRegistry"); +const assistantRuntimeContractResolver_1 = require("./assistantRuntimeContractResolver"); +const assistantStateTransitionRuntimeAdapter_1 = require("./assistantStateTransitionRuntimeAdapter"); +const assistantTruthAnswerPolicyRuntimeAdapter_1 = require("./assistantTruthAnswerPolicyRuntimeAdapter"); +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 toStringList(value) { + if (!Array.isArray(value)) { + return []; + } + return value + .map((item) => toNonEmptyString(item)) + .filter((item) => Boolean(item)); +} +function uniqueStrings(values) { + return Array.from(new Set(values.filter((item) => item.trim().length > 0))); +} +function addViolation(target, violation) { + if (!target.includes(violation)) { + target.push(violation); + } +} +function resolveShadow(input, debug) { + return (input.runtimeContractShadow ?? + toRecordObject(debug.assistant_runtime_contract_v1) ?? + (0, assistantRuntimeContractResolver_1.resolveAssistantRuntimeContractShadow)({ + addressDebug: debug, + addressRuntimeMeta: input.addressRuntimeMeta, + groundingStatus: input.groundingStatus + })); +} +function resolveTruthPolicy(input, debug) { + return (input.truthAnswerPolicy ?? + toRecordObject(debug.assistant_truth_answer_policy_v1) ?? + (0, assistantTruthAnswerPolicyRuntimeAdapter_1.resolveAssistantTruthAnswerPolicyRuntime)({ + addressDebug: debug, + addressRuntimeMeta: input.addressRuntimeMeta, + groundingStatus: input.groundingStatus, + coverageReport: input.coverageReport, + replyType: input.replyType + })); +} +function resolveStateTransition(input, debug) { + return (input.stateTransition ?? + toRecordObject(debug.assistant_state_transition_v1) ?? + (0, assistantStateTransitionRuntimeAdapter_1.resolveAssistantStateTransitionRuntime)({ + addressDebug: debug, + addressRuntimeMeta: input.addressRuntimeMeta, + groundingStatus: input.groundingStatus, + coverageReport: input.coverageReport, + replyType: input.replyType, + runtimeContractShadow: input.runtimeContractShadow, + truthAnswerPolicy: input.truthAnswerPolicy + })); +} +function hasValue(value) { + return toNonEmptyString(value) !== null; +} +function collectProvidedAnchors(debug) { + const anchors = []; + const filters = toRecordObject(debug.extracted_filters); + const semanticFrame = toRecordObject(debug.semantic_frame); + const rootFrame = toRecordObject(debug.address_root_frame_context); + const anchorType = toNonEmptyString(debug.anchor_type); + const anchorValue = toNonEmptyString(debug.anchor_value_resolved) ?? toNonEmptyString(debug.anchor_value_raw); + if (filters) { + for (const [key, value] of Object.entries(filters)) { + if (hasValue(value)) { + anchors.push(key); + } + } + } + if (anchorType && anchorType !== "unknown" && anchorValue) { + anchors.push(anchorType); + } + if (semanticFrame) { + const anchorKind = toNonEmptyString(semanticFrame.anchor_kind); + const anchorValueFromFrame = toNonEmptyString(semanticFrame.anchor_value); + if (anchorKind && anchorKind !== "none" && (anchorValueFromFrame || semanticFrame.selected_object_scope_detected === true)) { + anchors.push(anchorKind); + } + if (semanticFrame.selected_object_scope_detected === true) { + anchors.push("selected_object"); + } + } + if (toNonEmptyString(rootFrame?.current_frame_kind)?.includes("drilldown")) { + anchors.push("selected_object"); + } + return uniqueStrings(anchors); +} +function anchorSatisfied(requiredAnchor, providedAnchors, debug) { + const filters = toRecordObject(debug.extracted_filters); + if (providedAnchors.includes(requiredAnchor)) { + return true; + } + if (requiredAnchor === "item") { + return (providedAnchors.includes("selected_object") || + providedAnchors.includes("anchor_item") || + hasValue(filters?.item) || + toNonEmptyString(debug.anchor_type) === "item"); + } + if (requiredAnchor === "supplier") { + return providedAnchors.includes("counterparty") || hasValue(filters?.supplier) || hasValue(filters?.counterparty); + } + return false; +} +function runtimeLaneObserved(debug) { + const capabilityRouteMode = toNonEmptyString(debug.capability_route_mode); + const capabilityLayer = toNonEmptyString(debug.capability_layer); + const detectedMode = toNonEmptyString(debug.detected_mode); + if (capabilityRouteMode === "exact" && (capabilityLayer === "compute" || detectedMode === "address_query")) { + return "address_exact"; + } + if (detectedMode === "assistant_data_scope") { + return "assistant_data_scope"; + } + if (capabilityLayer === "conversational") { + return "chat"; + } + return "unknown"; +} +function focusObjectBindingStatus(input) { + if (!input.requiresFocusObject) { + return "not_required"; + } + if (input.providedAnchors.includes("selected_object")) { + return "bound"; + } + if (!input.missingAnchors.includes("item") && input.providedAnchors.includes("item")) { + return "inferred_from_anchor"; + } + return "missing"; +} +function truthFallbackAllowed(contractTruthFallbacks, truthMode) { + return truthMode === "confirmed" || contractTruthFallbacks.includes(truthMode); +} +function bindingStatusFor(input) { + if (!input.hasCapabilityId) { + return "not_applicable"; + } + if (!input.hasContract) { + return "contract_missing"; + } + if (input.violations.includes("transition_not_supported_by_capability") || + input.violations.includes("required_anchor_missing") || + input.violations.includes("focus_object_required_but_unbound") || + input.violations.includes("runtime_lane_mismatch")) { + return "blocked"; + } + if (input.violations.length > 0 || input.truthMode !== "confirmed") { + return "bound_with_limits"; + } + return "bound"; +} +function bindingActionFor(status, missingAnchors) { + if (status === "not_applicable" || status === "contract_missing") { + return "observe_only"; + } + if (status === "blocked" && missingAnchors.length > 0) { + return "clarify"; + } + if (status === "blocked") { + return "block"; + } + if (status === "bound_with_limits") { + return "limit"; + } + return "allow"; +} +function resolveAssistantCapabilityRuntimeBinding(input) { + const debug = toRecordObject(input.addressDebug) ?? {}; + const shadow = resolveShadow(input, debug); + const truthPolicy = resolveTruthPolicy(input, debug); + const stateTransition = resolveStateTransition(input, debug); + const capabilityId = shadow.capability_contract_id ?? + toNonEmptyString(debug.capability_contract_id) ?? + toNonEmptyString(debug.capability_id) ?? + truthPolicy.answer_shape.capability_contract_id; + const capabilityContract = capabilityId ? (0, assistantRuntimeContractRegistry_1.getAssistantCapabilityContract)(capabilityId) : null; + const violations = []; + if (capabilityId && !capabilityContract) { + addViolation(violations, "capability_contract_missing"); + } + const transitionAllowed = capabilityContract && stateTransition.transition_id + ? capabilityContract.supported_transition_classes.includes(stateTransition.transition_id) + : capabilityContract + ? null + : false; + if (capabilityContract && stateTransition.transition_id && !transitionAllowed) { + addViolation(violations, "transition_not_supported_by_capability"); + } + const providedAnchors = collectProvidedAnchors(debug); + const requiredAnchors = capabilityContract?.required_anchors ?? []; + const missingAnchors = requiredAnchors.filter((anchor) => !anchorSatisfied(anchor, providedAnchors, debug)); + if (missingAnchors.length > 0) { + addViolation(violations, "required_anchor_missing"); + } + const focusStatus = focusObjectBindingStatus({ + requiresFocusObject: capabilityContract?.requires_focus_object ?? false, + providedAnchors, + missingAnchors + }); + if (capabilityContract?.requires_focus_object && focusStatus === "missing") { + addViolation(violations, "focus_object_required_but_unbound"); + } + const observedLane = runtimeLaneObserved(debug); + const laneMatches = !capabilityContract || observedLane === "unknown" || capabilityContract.runtime_lane === observedLane; + if (capabilityContract && !laneMatches) { + addViolation(violations, "runtime_lane_mismatch"); + } + const truthAllowed = capabilityContract + ? truthFallbackAllowed(capabilityContract.truth_mode_fallbacks, truthPolicy.truth_gate.truth_mode) + : null; + if (truthAllowed === false) { + addViolation(violations, "truth_fallback_not_declared"); + } + const answerShapeCompatible = capabilityContract && truthPolicy.answer_shape.capability_contract_id + ? truthPolicy.answer_shape.capability_contract_id === capabilityContract.capability_id + : capabilityContract + ? null + : false; + if (answerShapeCompatible === false && capabilityContract) { + addViolation(violations, "answer_shape_capability_mismatch"); + } + const status = bindingStatusFor({ + hasCapabilityId: Boolean(capabilityId), + hasContract: Boolean(capabilityContract), + violations, + truthMode: truthPolicy.truth_gate.truth_mode + }); + const action = bindingActionFor(status, missingAnchors); + return { + schema_version: assistantRuntimeContracts_1.ASSISTANT_CAPABILITY_RUNTIME_BINDING_SCHEMA_VERSION, + binding_owner: "assistantCapabilityRuntimeBindingAdapter", + capability_id: capabilityId, + capability_contract_id: capabilityContract?.capability_id ?? null, + binding_status: status, + binding_action: action, + runtime_lane_expected: capabilityContract?.runtime_lane ?? null, + runtime_lane_observed: observedLane, + execution_adapter: capabilityContract?.execution_adapter ?? null, + transition_id: stateTransition.transition_id, + transition_allowed: transitionAllowed, + required_anchors: requiredAnchors, + provided_anchors: providedAnchors, + missing_anchors: missingAnchors, + requires_focus_object: capabilityContract?.requires_focus_object ?? false, + focus_object_binding_status: focusStatus, + result_shape: capabilityContract?.result_shape ?? null, + answer_object_shape: capabilityContract?.answer_object_shape ?? null, + truth_gate_behavior: capabilityContract?.coverage_gate_behavior ?? null, + truth_fallback_allowed: truthAllowed, + answer_shape_compatible: answerShapeCompatible, + violations, + reason_codes: uniqueStrings([ + `binding_status_${status}`, + `binding_action_${action}`, + ...violations, + ...stateTransition.reason_codes, + ...truthPolicy.truth_gate.reason_codes, + ...shadow.capability_contract_reason + ]).slice(0, 48) + }; +} +function buildAssistantCapabilityRuntimeBindingFields(input) { + const binding = resolveAssistantCapabilityRuntimeBinding(input); + return { + assistant_capability_binding_v1: binding, + capability_binding_contract: binding, + capability_binding_status: binding.binding_status, + capability_binding_action: binding.binding_action, + capability_binding_violations: binding.violations + }; +} +function attachAssistantCapabilityRuntimeBinding(debugPayload, input) { + return { + ...debugPayload, + ...buildAssistantCapabilityRuntimeBindingFields({ + ...input, + addressDebug: debugPayload + }) + }; +} diff --git a/llm_normalizer/backend/dist/services/assistantDebugPayloadAssembler.js b/llm_normalizer/backend/dist/services/assistantDebugPayloadAssembler.js index 52b40a3..2f76e0c 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 assistantCapabilityRuntimeBindingAdapter_1 = require("./assistantCapabilityRuntimeBindingAdapter"); const assistantRuntimeContractResolver_1 = require("./assistantRuntimeContractResolver"); const assistantStateTransitionRuntimeAdapter_1 = require("./assistantStateTransitionRuntimeAdapter"); const assistantTruthAnswerPolicyRuntimeAdapter_1 = require("./assistantTruthAnswerPolicyRuntimeAdapter"); @@ -106,7 +107,13 @@ function buildDeepAnalysisDebugPayload(input) { coverageReport: input.coverageReport, replyType: "deep_analysis" }); - return (0, assistantStateTransitionRuntimeAdapter_1.attachAssistantStateTransition)(debugWithTruthAnswerPolicy, { + const debugWithStateTransition = (0, assistantStateTransitionRuntimeAdapter_1.attachAssistantStateTransition)(debugWithTruthAnswerPolicy, { + addressRuntimeMeta: input.addressRuntimeMetaForDeep, + groundingStatus: input.groundingCheck.status, + coverageReport: input.coverageReport, + replyType: "deep_analysis" + }); + return (0, assistantCapabilityRuntimeBindingAdapter_1.attachAssistantCapabilityRuntimeBinding)(debugWithStateTransition, { addressRuntimeMeta: input.addressRuntimeMetaForDeep, groundingStatus: input.groundingCheck.status, coverageReport: input.coverageReport, diff --git a/llm_normalizer/backend/dist/types/assistantRuntimeContracts.js b/llm_normalizer/backend/dist/types/assistantRuntimeContracts.js index 9a42065..9c963d7 100644 --- a/llm_normalizer/backend/dist/types/assistantRuntimeContracts.js +++ b/llm_normalizer/backend/dist/types/assistantRuntimeContracts.js @@ -1,6 +1,7 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.ASSISTANT_STATE_TRANSITION_RUNTIME_SCHEMA_VERSION = exports.ASSISTANT_TRUTH_ANSWER_POLICY_RUNTIME_SCHEMA_VERSION = exports.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION = void 0; +exports.ASSISTANT_CAPABILITY_RUNTIME_BINDING_SCHEMA_VERSION = exports.ASSISTANT_STATE_TRANSITION_RUNTIME_SCHEMA_VERSION = exports.ASSISTANT_TRUTH_ANSWER_POLICY_RUNTIME_SCHEMA_VERSION = exports.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION = void 0; exports.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION = "assistant_runtime_contracts_v1"; exports.ASSISTANT_TRUTH_ANSWER_POLICY_RUNTIME_SCHEMA_VERSION = "assistant_truth_answer_policy_runtime_v1"; exports.ASSISTANT_STATE_TRANSITION_RUNTIME_SCHEMA_VERSION = "assistant_state_transition_runtime_v1"; +exports.ASSISTANT_CAPABILITY_RUNTIME_BINDING_SCHEMA_VERSION = "assistant_capability_runtime_binding_v1"; diff --git a/llm_normalizer/backend/src/services/assistantAddressLaneResponseRuntimeAdapter.ts b/llm_normalizer/backend/src/services/assistantAddressLaneResponseRuntimeAdapter.ts index 4a2adf2..0afd26b 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 { attachAssistantCapabilityRuntimeBinding } from "./assistantCapabilityRuntimeBindingAdapter"; import { attachAssistantRuntimeContractShadow } from "./assistantRuntimeContractResolver"; import { attachAssistantStateTransition } from "./assistantStateTransitionRuntimeAdapter"; import { attachAssistantTruthAnswerPolicy } from "./assistantTruthAnswerPolicyRuntimeAdapter"; @@ -261,6 +262,10 @@ export function runAssistantAddressLaneResponseRuntime | null; + addressRuntimeMeta?: Record | null; + groundingStatus?: unknown; + coverageReport?: Record | null; + replyType?: unknown; + runtimeContractShadow?: AssistantRuntimeContractShadowDecision | null; + truthAnswerPolicy?: AssistantTruthAnswerPolicyRuntimeContract | null; + stateTransition?: AssistantStateTransitionRuntimeContract | null; +} + +export interface AssistantCapabilityRuntimeBindingFields { + assistant_capability_binding_v1: AssistantCapabilityRuntimeBindingContract; + capability_binding_contract: AssistantCapabilityRuntimeBindingContract; + capability_binding_status: AssistantCapabilityBindingStatus; + capability_binding_action: AssistantCapabilityBindingAction; + capability_binding_violations: AssistantCapabilityBindingViolation[]; +} + +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 toStringList(value: unknown): string[] { + if (!Array.isArray(value)) { + return []; + } + return value + .map((item) => toNonEmptyString(item)) + .filter((item): item is string => Boolean(item)); +} + +function uniqueStrings(values: string[]): string[] { + return Array.from(new Set(values.filter((item) => item.trim().length > 0))); +} + +function addViolation(target: AssistantCapabilityBindingViolation[], violation: AssistantCapabilityBindingViolation): void { + if (!target.includes(violation)) { + target.push(violation); + } +} + +function resolveShadow( + input: ResolveAssistantCapabilityRuntimeBindingInput, + debug: Record +): AssistantRuntimeContractShadowDecision { + return ( + input.runtimeContractShadow ?? + (toRecordObject(debug.assistant_runtime_contract_v1) as AssistantRuntimeContractShadowDecision | null) ?? + resolveAssistantRuntimeContractShadow({ + addressDebug: debug, + addressRuntimeMeta: input.addressRuntimeMeta, + groundingStatus: input.groundingStatus + }) + ); +} + +function resolveTruthPolicy( + input: ResolveAssistantCapabilityRuntimeBindingInput, + debug: Record +): AssistantTruthAnswerPolicyRuntimeContract { + return ( + input.truthAnswerPolicy ?? + (toRecordObject(debug.assistant_truth_answer_policy_v1) as AssistantTruthAnswerPolicyRuntimeContract | null) ?? + resolveAssistantTruthAnswerPolicyRuntime({ + addressDebug: debug, + addressRuntimeMeta: input.addressRuntimeMeta, + groundingStatus: input.groundingStatus, + coverageReport: input.coverageReport, + replyType: input.replyType + }) + ); +} + +function resolveStateTransition( + input: ResolveAssistantCapabilityRuntimeBindingInput, + debug: Record +): AssistantStateTransitionRuntimeContract { + return ( + input.stateTransition ?? + (toRecordObject(debug.assistant_state_transition_v1) as AssistantStateTransitionRuntimeContract | null) ?? + resolveAssistantStateTransitionRuntime({ + addressDebug: debug, + addressRuntimeMeta: input.addressRuntimeMeta, + groundingStatus: input.groundingStatus, + coverageReport: input.coverageReport, + replyType: input.replyType, + runtimeContractShadow: input.runtimeContractShadow, + truthAnswerPolicy: input.truthAnswerPolicy + }) + ); +} + +function hasValue(value: unknown): boolean { + return toNonEmptyString(value) !== null; +} + +function collectProvidedAnchors(debug: Record): string[] { + const anchors: string[] = []; + const filters = toRecordObject(debug.extracted_filters); + const semanticFrame = toRecordObject(debug.semantic_frame); + const rootFrame = toRecordObject(debug.address_root_frame_context); + const anchorType = toNonEmptyString(debug.anchor_type); + const anchorValue = toNonEmptyString(debug.anchor_value_resolved) ?? toNonEmptyString(debug.anchor_value_raw); + + if (filters) { + for (const [key, value] of Object.entries(filters)) { + if (hasValue(value)) { + anchors.push(key); + } + } + } + if (anchorType && anchorType !== "unknown" && anchorValue) { + anchors.push(anchorType); + } + if (semanticFrame) { + const anchorKind = toNonEmptyString(semanticFrame.anchor_kind); + const anchorValueFromFrame = toNonEmptyString(semanticFrame.anchor_value); + if (anchorKind && anchorKind !== "none" && (anchorValueFromFrame || semanticFrame.selected_object_scope_detected === true)) { + anchors.push(anchorKind); + } + if (semanticFrame.selected_object_scope_detected === true) { + anchors.push("selected_object"); + } + } + if (toNonEmptyString(rootFrame?.current_frame_kind)?.includes("drilldown")) { + anchors.push("selected_object"); + } + return uniqueStrings(anchors); +} + +function anchorSatisfied(requiredAnchor: string, providedAnchors: string[], debug: Record): boolean { + const filters = toRecordObject(debug.extracted_filters); + if (providedAnchors.includes(requiredAnchor)) { + return true; + } + if (requiredAnchor === "item") { + return ( + providedAnchors.includes("selected_object") || + providedAnchors.includes("anchor_item") || + hasValue(filters?.item) || + toNonEmptyString(debug.anchor_type) === "item" + ); + } + if (requiredAnchor === "supplier") { + return providedAnchors.includes("counterparty") || hasValue(filters?.supplier) || hasValue(filters?.counterparty); + } + return false; +} + +function runtimeLaneObserved(debug: Record): AssistantRuntimeLane | "unknown" { + const capabilityRouteMode = toNonEmptyString(debug.capability_route_mode); + const capabilityLayer = toNonEmptyString(debug.capability_layer); + const detectedMode = toNonEmptyString(debug.detected_mode); + if (capabilityRouteMode === "exact" && (capabilityLayer === "compute" || detectedMode === "address_query")) { + return "address_exact"; + } + if (detectedMode === "assistant_data_scope") { + return "assistant_data_scope"; + } + if (capabilityLayer === "conversational") { + return "chat"; + } + return "unknown"; +} + +function focusObjectBindingStatus(input: { + requiresFocusObject: boolean; + providedAnchors: string[]; + missingAnchors: string[]; +}): AssistantCapabilityRuntimeBindingContract["focus_object_binding_status"] { + if (!input.requiresFocusObject) { + return "not_required"; + } + if (input.providedAnchors.includes("selected_object")) { + return "bound"; + } + if (!input.missingAnchors.includes("item") && input.providedAnchors.includes("item")) { + return "inferred_from_anchor"; + } + return "missing"; +} + +function truthFallbackAllowed( + contractTruthFallbacks: string[], + truthMode: AssistantTruthAnswerPolicyRuntimeContract["truth_gate"]["truth_mode"] +): boolean { + return truthMode === "confirmed" || contractTruthFallbacks.includes(truthMode); +} + +function bindingStatusFor(input: { + hasCapabilityId: boolean; + hasContract: boolean; + violations: AssistantCapabilityBindingViolation[]; + truthMode: AssistantTruthAnswerPolicyRuntimeContract["truth_gate"]["truth_mode"]; +}): AssistantCapabilityBindingStatus { + if (!input.hasCapabilityId) { + return "not_applicable"; + } + if (!input.hasContract) { + return "contract_missing"; + } + if ( + input.violations.includes("transition_not_supported_by_capability") || + input.violations.includes("required_anchor_missing") || + input.violations.includes("focus_object_required_but_unbound") || + input.violations.includes("runtime_lane_mismatch") + ) { + return "blocked"; + } + if (input.violations.length > 0 || input.truthMode !== "confirmed") { + return "bound_with_limits"; + } + return "bound"; +} + +function bindingActionFor(status: AssistantCapabilityBindingStatus, missingAnchors: string[]): AssistantCapabilityBindingAction { + if (status === "not_applicable" || status === "contract_missing") { + return "observe_only"; + } + if (status === "blocked" && missingAnchors.length > 0) { + return "clarify"; + } + if (status === "blocked") { + return "block"; + } + if (status === "bound_with_limits") { + return "limit"; + } + return "allow"; +} + +export function resolveAssistantCapabilityRuntimeBinding( + input: ResolveAssistantCapabilityRuntimeBindingInput +): AssistantCapabilityRuntimeBindingContract { + const debug = toRecordObject(input.addressDebug) ?? {}; + const shadow = resolveShadow(input, debug); + const truthPolicy = resolveTruthPolicy(input, debug); + const stateTransition = resolveStateTransition(input, debug); + const capabilityId = + shadow.capability_contract_id ?? + toNonEmptyString(debug.capability_contract_id) ?? + toNonEmptyString(debug.capability_id) ?? + truthPolicy.answer_shape.capability_contract_id; + const capabilityContract = capabilityId ? getAssistantCapabilityContract(capabilityId) : null; + const violations: AssistantCapabilityBindingViolation[] = []; + + if (capabilityId && !capabilityContract) { + addViolation(violations, "capability_contract_missing"); + } + + const transitionAllowed = + capabilityContract && stateTransition.transition_id + ? capabilityContract.supported_transition_classes.includes(stateTransition.transition_id) + : capabilityContract + ? null + : false; + if (capabilityContract && stateTransition.transition_id && !transitionAllowed) { + addViolation(violations, "transition_not_supported_by_capability"); + } + + const providedAnchors = collectProvidedAnchors(debug); + const requiredAnchors = capabilityContract?.required_anchors ?? []; + const missingAnchors = requiredAnchors.filter((anchor) => !anchorSatisfied(anchor, providedAnchors, debug)); + if (missingAnchors.length > 0) { + addViolation(violations, "required_anchor_missing"); + } + + const focusStatus = focusObjectBindingStatus({ + requiresFocusObject: capabilityContract?.requires_focus_object ?? false, + providedAnchors, + missingAnchors + }); + if (capabilityContract?.requires_focus_object && focusStatus === "missing") { + addViolation(violations, "focus_object_required_but_unbound"); + } + + const observedLane = runtimeLaneObserved(debug); + const laneMatches = !capabilityContract || observedLane === "unknown" || capabilityContract.runtime_lane === observedLane; + if (capabilityContract && !laneMatches) { + addViolation(violations, "runtime_lane_mismatch"); + } + + const truthAllowed = capabilityContract + ? truthFallbackAllowed(capabilityContract.truth_mode_fallbacks, truthPolicy.truth_gate.truth_mode) + : null; + if (truthAllowed === false) { + addViolation(violations, "truth_fallback_not_declared"); + } + + const answerShapeCompatible = + capabilityContract && truthPolicy.answer_shape.capability_contract_id + ? truthPolicy.answer_shape.capability_contract_id === capabilityContract.capability_id + : capabilityContract + ? null + : false; + if (answerShapeCompatible === false && capabilityContract) { + addViolation(violations, "answer_shape_capability_mismatch"); + } + + const status = bindingStatusFor({ + hasCapabilityId: Boolean(capabilityId), + hasContract: Boolean(capabilityContract), + violations, + truthMode: truthPolicy.truth_gate.truth_mode + }); + const action = bindingActionFor(status, missingAnchors); + + return { + schema_version: ASSISTANT_CAPABILITY_RUNTIME_BINDING_SCHEMA_VERSION, + binding_owner: "assistantCapabilityRuntimeBindingAdapter", + capability_id: capabilityId, + capability_contract_id: capabilityContract?.capability_id ?? null, + binding_status: status, + binding_action: action, + runtime_lane_expected: capabilityContract?.runtime_lane ?? null, + runtime_lane_observed: observedLane, + execution_adapter: capabilityContract?.execution_adapter ?? null, + transition_id: stateTransition.transition_id, + transition_allowed: transitionAllowed, + required_anchors: requiredAnchors, + provided_anchors: providedAnchors, + missing_anchors: missingAnchors, + requires_focus_object: capabilityContract?.requires_focus_object ?? false, + focus_object_binding_status: focusStatus, + result_shape: capabilityContract?.result_shape ?? null, + answer_object_shape: capabilityContract?.answer_object_shape ?? null, + truth_gate_behavior: capabilityContract?.coverage_gate_behavior ?? null, + truth_fallback_allowed: truthAllowed, + answer_shape_compatible: answerShapeCompatible, + violations, + reason_codes: uniqueStrings([ + `binding_status_${status}`, + `binding_action_${action}`, + ...violations, + ...stateTransition.reason_codes, + ...truthPolicy.truth_gate.reason_codes, + ...shadow.capability_contract_reason + ]).slice(0, 48) + }; +} + +export function buildAssistantCapabilityRuntimeBindingFields( + input: ResolveAssistantCapabilityRuntimeBindingInput +): AssistantCapabilityRuntimeBindingFields { + const binding = resolveAssistantCapabilityRuntimeBinding(input); + return { + assistant_capability_binding_v1: binding, + capability_binding_contract: binding, + capability_binding_status: binding.binding_status, + capability_binding_action: binding.binding_action, + capability_binding_violations: binding.violations + }; +} + +export function attachAssistantCapabilityRuntimeBinding>( + debugPayload: T, + input: Omit +): T & AssistantCapabilityRuntimeBindingFields { + return { + ...debugPayload, + ...buildAssistantCapabilityRuntimeBindingFields({ + ...input, + addressDebug: debugPayload + }) + }; +} diff --git a/llm_normalizer/backend/src/services/assistantDebugPayloadAssembler.ts b/llm_normalizer/backend/src/services/assistantDebugPayloadAssembler.ts index 29e26b5..1d658fa 100644 --- a/llm_normalizer/backend/src/services/assistantDebugPayloadAssembler.ts +++ b/llm_normalizer/backend/src/services/assistantDebugPayloadAssembler.ts @@ -23,6 +23,7 @@ import type { GroundedAnswerEligibilityAudit, TemporalGuardAudit } from "./assistantRuntimeGuards"; +import { attachAssistantCapabilityRuntimeBinding } from "./assistantCapabilityRuntimeBindingAdapter"; import { attachAssistantRuntimeContractShadow } from "./assistantRuntimeContractResolver"; import { attachAssistantStateTransition } from "./assistantStateTransitionRuntimeAdapter"; import { attachAssistantTruthAnswerPolicy } from "./assistantTruthAnswerPolicyRuntimeAdapter"; @@ -186,7 +187,13 @@ export function buildDeepAnalysisDebugPayload(input: DeepAnalysisDebugPayloadInp coverageReport: input.coverageReport as unknown as Record, replyType: "deep_analysis" }); - return attachAssistantStateTransition(debugWithTruthAnswerPolicy, { + const debugWithStateTransition = attachAssistantStateTransition(debugWithTruthAnswerPolicy, { + addressRuntimeMeta: input.addressRuntimeMetaForDeep as unknown as Record | null | undefined, + groundingStatus: input.groundingCheck.status, + coverageReport: input.coverageReport as unknown as Record, + replyType: "deep_analysis" + }); + return attachAssistantCapabilityRuntimeBinding(debugWithStateTransition, { addressRuntimeMeta: input.addressRuntimeMetaForDeep as unknown as Record | null | undefined, groundingStatus: input.groundingCheck.status, coverageReport: input.coverageReport as unknown as Record, diff --git a/llm_normalizer/backend/src/types/assistant.ts b/llm_normalizer/backend/src/types/assistant.ts index 5b272dd..de45d2f 100644 --- a/llm_normalizer/backend/src/types/assistant.ts +++ b/llm_normalizer/backend/src/types/assistant.ts @@ -19,6 +19,10 @@ import type { AccountingGraphBuildResult } from "./stage4Graph"; import type { AddressNavigationState } from "./addressNavigation"; import type { AssistantAnswerShapeKind, + AssistantCapabilityBindingAction, + AssistantCapabilityBindingStatus, + AssistantCapabilityBindingViolation, + AssistantCapabilityRuntimeBindingContract, AssistantRuntimeContractShadowDecision, AssistantStateTransitionApplicationStatus, AssistantStateTransitionRuntimeContract, @@ -470,6 +474,11 @@ export interface AssistantDebugPayload { state_transition_id?: AssistantTransitionClassId | null; state_transition_status?: AssistantStateTransitionApplicationStatus; effective_carryover_depth?: AssistantStateTransitionRuntimeContract["effective_carryover_depth"]; + assistant_capability_binding_v1?: AssistantCapabilityRuntimeBindingContract; + capability_binding_contract?: AssistantCapabilityRuntimeBindingContract; + capability_binding_status?: AssistantCapabilityBindingStatus; + capability_binding_action?: AssistantCapabilityBindingAction; + capability_binding_violations?: AssistantCapabilityBindingViolation[]; 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 c500ee9..9b5bf77 100644 --- a/llm_normalizer/backend/src/types/assistantRuntimeContracts.ts +++ b/llm_normalizer/backend/src/types/assistantRuntimeContracts.ts @@ -3,6 +3,7 @@ import type { AddressIntent } from "./addressQuery"; export const ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION = "assistant_runtime_contracts_v1" as const; export const ASSISTANT_TRUTH_ANSWER_POLICY_RUNTIME_SCHEMA_VERSION = "assistant_truth_answer_policy_runtime_v1" as const; export const ASSISTANT_STATE_TRANSITION_RUNTIME_SCHEMA_VERSION = "assistant_state_transition_runtime_v1" as const; +export const ASSISTANT_CAPABILITY_RUNTIME_BINDING_SCHEMA_VERSION = "assistant_capability_runtime_binding_v1" as const; export type AssistantLivingMode = "address_data" | "assistant_data_scope" | "chat" | "meta_followup" | "clarification"; export type AssistantFrameStatus = "active" | "suspended" | "closed" | "blocked"; @@ -39,6 +40,16 @@ export type AssistantAnswerShapeKind = export type AssistantAnswerShapeReplyType = "factual" | "partial_coverage" | "deep_analysis" | "unknown"; export type AssistantStateTransitionApplicationStatus = "applied" | "clarification_required" | "blocked" | "unresolved"; export type AssistantStateFrameAction = "create" | "update" | "preserve" | "reuse" | "clear" | "block" | "none"; +export type AssistantCapabilityBindingStatus = "bound" | "bound_with_limits" | "blocked" | "contract_missing" | "not_applicable"; +export type AssistantCapabilityBindingAction = "allow" | "limit" | "clarify" | "block" | "observe_only"; +export type AssistantCapabilityBindingViolation = + | "capability_contract_missing" + | "transition_not_supported_by_capability" + | "required_anchor_missing" + | "focus_object_required_but_unbound" + | "runtime_lane_mismatch" + | "truth_fallback_not_declared" + | "answer_shape_capability_mismatch"; export interface AssistantDateScopeState { as_of_date: string | null; @@ -144,6 +155,32 @@ export interface AssistantStateTransitionRuntimeContract { reason_codes: string[]; } +export interface AssistantCapabilityRuntimeBindingContract { + schema_version: typeof ASSISTANT_CAPABILITY_RUNTIME_BINDING_SCHEMA_VERSION; + binding_owner: "assistantCapabilityRuntimeBindingAdapter"; + capability_id: string | null; + capability_contract_id: string | null; + binding_status: AssistantCapabilityBindingStatus; + binding_action: AssistantCapabilityBindingAction; + runtime_lane_expected: AssistantRuntimeLane | null; + runtime_lane_observed: AssistantRuntimeLane | "unknown"; + execution_adapter: string | null; + transition_id: AssistantTransitionClassId | null; + transition_allowed: boolean | null; + required_anchors: string[]; + provided_anchors: string[]; + missing_anchors: string[]; + requires_focus_object: boolean; + focus_object_binding_status: "bound" | "inferred_from_anchor" | "missing" | "not_required"; + result_shape: string | null; + answer_object_shape: string | null; + truth_gate_behavior: AssistantCoverageGateBehavior | null; + truth_fallback_allowed: boolean | null; + answer_shape_compatible: boolean | null; + violations: AssistantCapabilityBindingViolation[]; + reason_codes: string[]; +} + export interface AssistantSessionAggregateState { schema_version: typeof ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION; living_mode_state: { diff --git a/llm_normalizer/backend/tests/assistantAddressLaneResponseRuntimeAdapter.test.ts b/llm_normalizer/backend/tests/assistantAddressLaneResponseRuntimeAdapter.test.ts index e733cba..a786548 100644 --- a/llm_normalizer/backend/tests/assistantAddressLaneResponseRuntimeAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantAddressLaneResponseRuntimeAdapter.test.ts @@ -81,6 +81,13 @@ describe("assistant address lane response runtime adapter", () => { }), state_transition_contract: expect.objectContaining({ schema_version: "assistant_state_transition_runtime_v1" + }), + assistant_capability_binding_v1: expect.objectContaining({ + schema_version: "assistant_capability_runtime_binding_v1", + binding_owner: "assistantCapabilityRuntimeBindingAdapter" + }), + capability_binding_contract: expect.objectContaining({ + schema_version: "assistant_capability_runtime_binding_v1" }) }) ); @@ -145,7 +152,10 @@ describe("assistant address lane response runtime adapter", () => { carryover_eligibility: "none", state_transition_id: null, state_transition_status: "unresolved", - effective_carryover_depth: "none" + effective_carryover_depth: "none", + capability_binding_status: "not_applicable", + capability_binding_action: "observe_only", + capability_binding_violations: [] }) ); expect(runtime.response).toEqual({ ok: true }); diff --git a/llm_normalizer/backend/tests/assistantCapabilityRuntimeBindingAdapter.test.ts b/llm_normalizer/backend/tests/assistantCapabilityRuntimeBindingAdapter.test.ts new file mode 100644 index 0000000..554d441 --- /dev/null +++ b/llm_normalizer/backend/tests/assistantCapabilityRuntimeBindingAdapter.test.ts @@ -0,0 +1,146 @@ +import { describe, expect, it } from "vitest"; +import { + attachAssistantCapabilityRuntimeBinding, + resolveAssistantCapabilityRuntimeBinding +} from "../src/services/assistantCapabilityRuntimeBindingAdapter"; + +describe("assistant capability runtime binding adapter", () => { + it("binds a grounded root inventory capability to its contract", () => { + const binding = resolveAssistantCapabilityRuntimeBinding({ + addressDebug: { + capability_id: "confirmed_inventory_on_hand_as_of_date", + detected_intent: "inventory_on_hand_as_of_date", + detected_mode: "address_query", + capability_layer: "compute", + capability_route_mode: "exact", + rows_matched: 3, + route_expectation_status: "matched" + }, + groundingStatus: "grounded", + replyType: "factual" + }); + + expect(binding).toEqual( + expect.objectContaining({ + schema_version: "assistant_capability_runtime_binding_v1", + binding_owner: "assistantCapabilityRuntimeBindingAdapter", + capability_id: "confirmed_inventory_on_hand_as_of_date", + capability_contract_id: "confirmed_inventory_on_hand_as_of_date", + binding_status: "bound", + binding_action: "allow", + runtime_lane_expected: "address_exact", + runtime_lane_observed: "address_exact", + transition_id: "T1", + transition_allowed: true, + missing_anchors: [], + focus_object_binding_status: "not_required", + result_shape: "item_list_with_quantity_cost_warehouse_organization", + answer_object_shape: "inventory_stock_snapshot", + truth_fallback_allowed: true, + answer_shape_compatible: true, + violations: [] + }) + ); + }); + + it("binds selected-object follow-ups through item anchor when focus object is implicit", () => { + const binding = resolveAssistantCapabilityRuntimeBinding({ + addressDebug: { + capability_id: "inventory_inventory_purchase_provenance_for_item", + detected_intent: "inventory_purchase_provenance_for_item", + detected_mode: "address_query", + capability_layer: "compute", + capability_route_mode: "exact", + extracted_filters: { + item: "Диван трехместный" + }, + rows_matched: 1, + route_expectation_status: "matched" + }, + addressRuntimeMeta: { + dialogContinuationContract: { + decision: "continue_previous", + target_intent: "inventory_purchase_provenance_for_item" + } + }, + groundingStatus: "grounded", + replyType: "factual" + }); + + expect(binding.binding_status).toBe("bound"); + expect(binding.transition_id).toBe("T4"); + expect(binding.transition_allowed).toBe(true); + expect(binding.required_anchors).toEqual(["item"]); + expect(binding.provided_anchors).toContain("item"); + expect(binding.focus_object_binding_status).toBe("inferred_from_anchor"); + expect(binding.violations).toEqual([]); + }); + + it("blocks selected-object capabilities when required anchors are missing", () => { + const binding = resolveAssistantCapabilityRuntimeBinding({ + addressDebug: { + capability_id: "inventory_inventory_purchase_provenance_for_item", + limited_reason_category: "missing_anchor", + missing_required_filters: ["item"] + }, + replyType: "partial_coverage" + }); + + expect(binding.binding_status).toBe("blocked"); + expect(binding.binding_action).toBe("clarify"); + expect(binding.missing_anchors).toEqual(["item"]); + expect(binding.focus_object_binding_status).toBe("missing"); + expect(binding.violations).toEqual( + expect.arrayContaining(["required_anchor_missing", "focus_object_required_but_unbound"]) + ); + }); + + it("blocks capabilities entered through unsupported transition classes", () => { + const binding = resolveAssistantCapabilityRuntimeBinding({ + addressDebug: { + capability_id: "confirmed_inventory_on_hand_as_of_date", + rows_matched: 1, + route_expectation_status: "matched" + }, + runtimeContractShadow: { + schema_version: "assistant_runtime_contracts_v1", + transition_contract_id: "T4", + transition_contract_title: "Short Action Follow-Up On Selected Object", + transition_contract_reason: ["test_forced_selected_object_transition"], + capability_contract_id: "confirmed_inventory_on_hand_as_of_date", + capability_contract_reason: ["debug_capability_id_matched_contract"], + truth_gate_contract_status: "full_confirmed", + carryover_eligibility: "object_only" + }, + groundingStatus: "grounded", + replyType: "factual" + }); + + expect(binding.binding_status).toBe("blocked"); + expect(binding.binding_action).toBe("block"); + expect(binding.transition_id).toBe("T4"); + expect(binding.transition_allowed).toBe(false); + expect(binding.violations).toContain("transition_not_supported_by_capability"); + }); + + it("attaches compact debug fields and preserves the binding contract", () => { + const debug = attachAssistantCapabilityRuntimeBinding( + { + capability_id: "confirmed_inventory_on_hand_as_of_date", + detected_intent: "inventory_on_hand_as_of_date", + rows_matched: 2, + route_expectation_status: "matched" + }, + { + groundingStatus: "grounded", + replyType: "factual" + } + ); + + expect(debug.capability_binding_status).toBe("bound"); + expect(debug.capability_binding_action).toBe("allow"); + expect(debug.capability_binding_violations).toEqual([]); + expect(debug.assistant_capability_binding_v1.schema_version).toBe("assistant_capability_runtime_binding_v1"); + expect(debug.capability_binding_contract.capability_contract_id).toBe("confirmed_inventory_on_hand_as_of_date"); + }); +}); diff --git a/llm_normalizer/backend/tests/assistantDebugPayloadAssembler.test.ts b/llm_normalizer/backend/tests/assistantDebugPayloadAssembler.test.ts index f0bbb76..a5e06d2 100644 --- a/llm_normalizer/backend/tests/assistantDebugPayloadAssembler.test.ts +++ b/llm_normalizer/backend/tests/assistantDebugPayloadAssembler.test.ts @@ -146,6 +146,14 @@ describe("assistant debug payload assembler", () => { effective_carryover_depth: "none" }) ); + expect(payload.assistant_capability_binding_v1).toEqual( + expect.objectContaining({ + schema_version: "assistant_capability_runtime_binding_v1", + binding_owner: "assistantCapabilityRuntimeBindingAdapter", + binding_status: "not_applicable", + binding_action: "observe_only" + }) + ); }); it("omits optional fields when they are not provided", () => {