АРЧ АП11 - Добавить capability runtime binding для assistant contracts
This commit is contained in:
parent
5f628f427f
commit
1c928a5d68
|
|
@ -2,6 +2,7 @@
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.runAssistantAddressLaneResponseRuntime = runAssistantAddressLaneResponseRuntime;
|
exports.runAssistantAddressLaneResponseRuntime = runAssistantAddressLaneResponseRuntime;
|
||||||
const assistantAddressTurnFinalizeRuntimeAdapter_1 = require("./assistantAddressTurnFinalizeRuntimeAdapter");
|
const assistantAddressTurnFinalizeRuntimeAdapter_1 = require("./assistantAddressTurnFinalizeRuntimeAdapter");
|
||||||
|
const assistantCapabilityRuntimeBindingAdapter_1 = require("./assistantCapabilityRuntimeBindingAdapter");
|
||||||
const assistantRuntimeContractResolver_1 = require("./assistantRuntimeContractResolver");
|
const assistantRuntimeContractResolver_1 = require("./assistantRuntimeContractResolver");
|
||||||
const assistantStateTransitionRuntimeAdapter_1 = require("./assistantStateTransitionRuntimeAdapter");
|
const assistantStateTransitionRuntimeAdapter_1 = require("./assistantStateTransitionRuntimeAdapter");
|
||||||
const assistantTruthAnswerPolicyRuntimeAdapter_1 = require("./assistantTruthAnswerPolicyRuntimeAdapter");
|
const assistantTruthAnswerPolicyRuntimeAdapter_1 = require("./assistantTruthAnswerPolicyRuntimeAdapter");
|
||||||
|
|
@ -202,6 +203,10 @@ function runAssistantAddressLaneResponseRuntime(input) {
|
||||||
addressRuntimeMeta: input.llmPreDecomposeMeta,
|
addressRuntimeMeta: input.llmPreDecomposeMeta,
|
||||||
replyType: normalizeAddressReplyType(input.addressLane.reply_type)
|
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({
|
const finalization = finalizeAddressTurnSafe({
|
||||||
sessionId: input.sessionId,
|
sessionId: input.sessionId,
|
||||||
userMessage: input.userMessage,
|
userMessage: input.userMessage,
|
||||||
|
|
@ -209,7 +214,7 @@ function runAssistantAddressLaneResponseRuntime(input) {
|
||||||
assistantReply: safeAddressReply,
|
assistantReply: safeAddressReply,
|
||||||
replyType: normalizeAddressReplyType(input.addressLane.reply_type),
|
replyType: normalizeAddressReplyType(input.addressLane.reply_type),
|
||||||
addressLaneDebug: normalizeAddressLaneDebug(input.addressLane.debug),
|
addressLaneDebug: normalizeAddressLaneDebug(input.addressLane.debug),
|
||||||
debug: debugWithStateTransition,
|
debug: debugWithCapabilityBinding,
|
||||||
carryoverMeta: normalizeCarryoverMeta(input.carryoverMeta),
|
carryoverMeta: normalizeCarryoverMeta(input.carryoverMeta),
|
||||||
llmPreDecomposeMeta: normalizeLlmPreDecomposeMeta(input.llmPreDecomposeMeta),
|
llmPreDecomposeMeta: normalizeLlmPreDecomposeMeta(input.llmPreDecomposeMeta),
|
||||||
appendItem: input.appendItem,
|
appendItem: input.appendItem,
|
||||||
|
|
@ -221,6 +226,6 @@ function runAssistantAddressLaneResponseRuntime(input) {
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
response: finalization.response,
|
response: finalization.response,
|
||||||
debug: debugWithStateTransition
|
debug: debugWithCapabilityBinding
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
300
llm_normalizer/backend/dist/services/assistantCapabilityRuntimeBindingAdapter.js
vendored
Normal file
300
llm_normalizer/backend/dist/services/assistantCapabilityRuntimeBindingAdapter.js
vendored
Normal file
|
|
@ -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
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.buildDeepAnalysisDebugPayload = buildDeepAnalysisDebugPayload;
|
exports.buildDeepAnalysisDebugPayload = buildDeepAnalysisDebugPayload;
|
||||||
|
const assistantCapabilityRuntimeBindingAdapter_1 = require("./assistantCapabilityRuntimeBindingAdapter");
|
||||||
const assistantRuntimeContractResolver_1 = require("./assistantRuntimeContractResolver");
|
const assistantRuntimeContractResolver_1 = require("./assistantRuntimeContractResolver");
|
||||||
const assistantStateTransitionRuntimeAdapter_1 = require("./assistantStateTransitionRuntimeAdapter");
|
const assistantStateTransitionRuntimeAdapter_1 = require("./assistantStateTransitionRuntimeAdapter");
|
||||||
const assistantTruthAnswerPolicyRuntimeAdapter_1 = require("./assistantTruthAnswerPolicyRuntimeAdapter");
|
const assistantTruthAnswerPolicyRuntimeAdapter_1 = require("./assistantTruthAnswerPolicyRuntimeAdapter");
|
||||||
|
|
@ -106,7 +107,13 @@ function buildDeepAnalysisDebugPayload(input) {
|
||||||
coverageReport: input.coverageReport,
|
coverageReport: input.coverageReport,
|
||||||
replyType: "deep_analysis"
|
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,
|
addressRuntimeMeta: input.addressRuntimeMetaForDeep,
|
||||||
groundingStatus: input.groundingCheck.status,
|
groundingStatus: input.groundingCheck.status,
|
||||||
coverageReport: input.coverageReport,
|
coverageReport: input.coverageReport,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
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_RUNTIME_CONTRACTS_SCHEMA_VERSION = "assistant_runtime_contracts_v1";
|
||||||
exports.ASSISTANT_TRUTH_ANSWER_POLICY_RUNTIME_SCHEMA_VERSION = "assistant_truth_answer_policy_runtime_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_STATE_TRANSITION_RUNTIME_SCHEMA_VERSION = "assistant_state_transition_runtime_v1";
|
||||||
|
exports.ASSISTANT_CAPABILITY_RUNTIME_BINDING_SCHEMA_VERSION = "assistant_capability_runtime_binding_v1";
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
type AddressLlmPreDecomposeMetaLogInput,
|
type AddressLlmPreDecomposeMetaLogInput,
|
||||||
type FinalizeAssistantAddressTurnInput
|
type FinalizeAssistantAddressTurnInput
|
||||||
} from "./assistantAddressTurnFinalizeRuntimeAdapter";
|
} from "./assistantAddressTurnFinalizeRuntimeAdapter";
|
||||||
|
import { attachAssistantCapabilityRuntimeBinding } from "./assistantCapabilityRuntimeBindingAdapter";
|
||||||
import { attachAssistantRuntimeContractShadow } from "./assistantRuntimeContractResolver";
|
import { attachAssistantRuntimeContractShadow } from "./assistantRuntimeContractResolver";
|
||||||
import { attachAssistantStateTransition } from "./assistantStateTransitionRuntimeAdapter";
|
import { attachAssistantStateTransition } from "./assistantStateTransitionRuntimeAdapter";
|
||||||
import { attachAssistantTruthAnswerPolicy } from "./assistantTruthAnswerPolicyRuntimeAdapter";
|
import { attachAssistantTruthAnswerPolicy } from "./assistantTruthAnswerPolicyRuntimeAdapter";
|
||||||
|
|
@ -261,6 +262,10 @@ export function runAssistantAddressLaneResponseRuntime<ResponseType = AssistantM
|
||||||
addressRuntimeMeta: input.llmPreDecomposeMeta,
|
addressRuntimeMeta: input.llmPreDecomposeMeta,
|
||||||
replyType: normalizeAddressReplyType(input.addressLane.reply_type)
|
replyType: normalizeAddressReplyType(input.addressLane.reply_type)
|
||||||
});
|
});
|
||||||
|
const debugWithCapabilityBinding = attachAssistantCapabilityRuntimeBinding(debugWithStateTransition, {
|
||||||
|
addressRuntimeMeta: input.llmPreDecomposeMeta,
|
||||||
|
replyType: normalizeAddressReplyType(input.addressLane.reply_type)
|
||||||
|
});
|
||||||
const finalization = finalizeAddressTurnSafe({
|
const finalization = finalizeAddressTurnSafe({
|
||||||
sessionId: input.sessionId,
|
sessionId: input.sessionId,
|
||||||
userMessage: input.userMessage,
|
userMessage: input.userMessage,
|
||||||
|
|
@ -268,7 +273,7 @@ export function runAssistantAddressLaneResponseRuntime<ResponseType = AssistantM
|
||||||
assistantReply: safeAddressReply,
|
assistantReply: safeAddressReply,
|
||||||
replyType: normalizeAddressReplyType(input.addressLane.reply_type),
|
replyType: normalizeAddressReplyType(input.addressLane.reply_type),
|
||||||
addressLaneDebug: normalizeAddressLaneDebug(input.addressLane.debug),
|
addressLaneDebug: normalizeAddressLaneDebug(input.addressLane.debug),
|
||||||
debug: debugWithStateTransition,
|
debug: debugWithCapabilityBinding,
|
||||||
carryoverMeta: normalizeCarryoverMeta(input.carryoverMeta),
|
carryoverMeta: normalizeCarryoverMeta(input.carryoverMeta),
|
||||||
llmPreDecomposeMeta: normalizeLlmPreDecomposeMeta(input.llmPreDecomposeMeta),
|
llmPreDecomposeMeta: normalizeLlmPreDecomposeMeta(input.llmPreDecomposeMeta),
|
||||||
appendItem: input.appendItem,
|
appendItem: input.appendItem,
|
||||||
|
|
@ -281,6 +286,6 @@ export function runAssistantAddressLaneResponseRuntime<ResponseType = AssistantM
|
||||||
|
|
||||||
return {
|
return {
|
||||||
response: finalization.response as ResponseType,
|
response: finalization.response as ResponseType,
|
||||||
debug: debugWithStateTransition
|
debug: debugWithCapabilityBinding
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,394 @@
|
||||||
|
import {
|
||||||
|
ASSISTANT_CAPABILITY_RUNTIME_BINDING_SCHEMA_VERSION,
|
||||||
|
type AssistantCapabilityBindingAction,
|
||||||
|
type AssistantCapabilityBindingStatus,
|
||||||
|
type AssistantCapabilityBindingViolation,
|
||||||
|
type AssistantCapabilityRuntimeBindingContract,
|
||||||
|
type AssistantRuntimeContractShadowDecision,
|
||||||
|
type AssistantRuntimeLane,
|
||||||
|
type AssistantStateTransitionRuntimeContract,
|
||||||
|
type AssistantTruthAnswerPolicyRuntimeContract
|
||||||
|
} from "../types/assistantRuntimeContracts";
|
||||||
|
import { getAssistantCapabilityContract } from "./assistantRuntimeContractRegistry";
|
||||||
|
import { resolveAssistantRuntimeContractShadow } from "./assistantRuntimeContractResolver";
|
||||||
|
import { resolveAssistantStateTransitionRuntime } from "./assistantStateTransitionRuntimeAdapter";
|
||||||
|
import { resolveAssistantTruthAnswerPolicyRuntime } from "./assistantTruthAnswerPolicyRuntimeAdapter";
|
||||||
|
|
||||||
|
export interface ResolveAssistantCapabilityRuntimeBindingInput {
|
||||||
|
addressDebug?: Record<string, unknown> | null;
|
||||||
|
addressRuntimeMeta?: Record<string, unknown> | null;
|
||||||
|
groundingStatus?: unknown;
|
||||||
|
coverageReport?: Record<string, unknown> | 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<string, unknown> | null {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<string, unknown>
|
||||||
|
): 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<string, unknown>
|
||||||
|
): 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<string, unknown>
|
||||||
|
): 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, unknown>): 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<string, unknown>): 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<string, unknown>): 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<T extends Record<string, unknown>>(
|
||||||
|
debugPayload: T,
|
||||||
|
input: Omit<ResolveAssistantCapabilityRuntimeBindingInput, "addressDebug">
|
||||||
|
): T & AssistantCapabilityRuntimeBindingFields {
|
||||||
|
return {
|
||||||
|
...debugPayload,
|
||||||
|
...buildAssistantCapabilityRuntimeBindingFields({
|
||||||
|
...input,
|
||||||
|
addressDebug: debugPayload
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,7 @@ import type {
|
||||||
GroundedAnswerEligibilityAudit,
|
GroundedAnswerEligibilityAudit,
|
||||||
TemporalGuardAudit
|
TemporalGuardAudit
|
||||||
} from "./assistantRuntimeGuards";
|
} from "./assistantRuntimeGuards";
|
||||||
|
import { attachAssistantCapabilityRuntimeBinding } from "./assistantCapabilityRuntimeBindingAdapter";
|
||||||
import { attachAssistantRuntimeContractShadow } from "./assistantRuntimeContractResolver";
|
import { attachAssistantRuntimeContractShadow } from "./assistantRuntimeContractResolver";
|
||||||
import { attachAssistantStateTransition } from "./assistantStateTransitionRuntimeAdapter";
|
import { attachAssistantStateTransition } from "./assistantStateTransitionRuntimeAdapter";
|
||||||
import { attachAssistantTruthAnswerPolicy } from "./assistantTruthAnswerPolicyRuntimeAdapter";
|
import { attachAssistantTruthAnswerPolicy } from "./assistantTruthAnswerPolicyRuntimeAdapter";
|
||||||
|
|
@ -186,7 +187,13 @@ export function buildDeepAnalysisDebugPayload(input: DeepAnalysisDebugPayloadInp
|
||||||
coverageReport: input.coverageReport as unknown as Record<string, unknown>,
|
coverageReport: input.coverageReport as unknown as Record<string, unknown>,
|
||||||
replyType: "deep_analysis"
|
replyType: "deep_analysis"
|
||||||
});
|
});
|
||||||
return attachAssistantStateTransition(debugWithTruthAnswerPolicy, {
|
const debugWithStateTransition = attachAssistantStateTransition(debugWithTruthAnswerPolicy, {
|
||||||
|
addressRuntimeMeta: input.addressRuntimeMetaForDeep as unknown as Record<string, unknown> | null | undefined,
|
||||||
|
groundingStatus: input.groundingCheck.status,
|
||||||
|
coverageReport: input.coverageReport as unknown as Record<string, unknown>,
|
||||||
|
replyType: "deep_analysis"
|
||||||
|
});
|
||||||
|
return attachAssistantCapabilityRuntimeBinding(debugWithStateTransition, {
|
||||||
addressRuntimeMeta: input.addressRuntimeMetaForDeep as unknown as Record<string, unknown> | null | undefined,
|
addressRuntimeMeta: input.addressRuntimeMetaForDeep as unknown as Record<string, unknown> | null | undefined,
|
||||||
groundingStatus: input.groundingCheck.status,
|
groundingStatus: input.groundingCheck.status,
|
||||||
coverageReport: input.coverageReport as unknown as Record<string, unknown>,
|
coverageReport: input.coverageReport as unknown as Record<string, unknown>,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,10 @@ import type { AccountingGraphBuildResult } from "./stage4Graph";
|
||||||
import type { AddressNavigationState } from "./addressNavigation";
|
import type { AddressNavigationState } from "./addressNavigation";
|
||||||
import type {
|
import type {
|
||||||
AssistantAnswerShapeKind,
|
AssistantAnswerShapeKind,
|
||||||
|
AssistantCapabilityBindingAction,
|
||||||
|
AssistantCapabilityBindingStatus,
|
||||||
|
AssistantCapabilityBindingViolation,
|
||||||
|
AssistantCapabilityRuntimeBindingContract,
|
||||||
AssistantRuntimeContractShadowDecision,
|
AssistantRuntimeContractShadowDecision,
|
||||||
AssistantStateTransitionApplicationStatus,
|
AssistantStateTransitionApplicationStatus,
|
||||||
AssistantStateTransitionRuntimeContract,
|
AssistantStateTransitionRuntimeContract,
|
||||||
|
|
@ -470,6 +474,11 @@ export interface AssistantDebugPayload {
|
||||||
state_transition_id?: AssistantTransitionClassId | null;
|
state_transition_id?: AssistantTransitionClassId | null;
|
||||||
state_transition_status?: AssistantStateTransitionApplicationStatus;
|
state_transition_status?: AssistantStateTransitionApplicationStatus;
|
||||||
effective_carryover_depth?: AssistantStateTransitionRuntimeContract["effective_carryover_depth"];
|
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";
|
execution_lane?: "address_query" | "deep_analysis";
|
||||||
llm_decomposition_applied?: boolean;
|
llm_decomposition_applied?: boolean;
|
||||||
llm_decomposition_attempted?: boolean;
|
llm_decomposition_attempted?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -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_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_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_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 AssistantLivingMode = "address_data" | "assistant_data_scope" | "chat" | "meta_followup" | "clarification";
|
||||||
export type AssistantFrameStatus = "active" | "suspended" | "closed" | "blocked";
|
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 AssistantAnswerShapeReplyType = "factual" | "partial_coverage" | "deep_analysis" | "unknown";
|
||||||
export type AssistantStateTransitionApplicationStatus = "applied" | "clarification_required" | "blocked" | "unresolved";
|
export type AssistantStateTransitionApplicationStatus = "applied" | "clarification_required" | "blocked" | "unresolved";
|
||||||
export type AssistantStateFrameAction = "create" | "update" | "preserve" | "reuse" | "clear" | "block" | "none";
|
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 {
|
export interface AssistantDateScopeState {
|
||||||
as_of_date: string | null;
|
as_of_date: string | null;
|
||||||
|
|
@ -144,6 +155,32 @@ export interface AssistantStateTransitionRuntimeContract {
|
||||||
reason_codes: string[];
|
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 {
|
export interface AssistantSessionAggregateState {
|
||||||
schema_version: typeof ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION;
|
schema_version: typeof ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION;
|
||||||
living_mode_state: {
|
living_mode_state: {
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,13 @@ describe("assistant address lane response runtime adapter", () => {
|
||||||
}),
|
}),
|
||||||
state_transition_contract: expect.objectContaining({
|
state_transition_contract: expect.objectContaining({
|
||||||
schema_version: "assistant_state_transition_runtime_v1"
|
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",
|
carryover_eligibility: "none",
|
||||||
state_transition_id: null,
|
state_transition_id: null,
|
||||||
state_transition_status: "unresolved",
|
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 });
|
expect(runtime.response).toEqual({ ok: true });
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -146,6 +146,14 @@ describe("assistant debug payload assembler", () => {
|
||||||
effective_carryover_depth: "none"
|
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", () => {
|
it("omits optional fields when they are not provided", () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue