ARCH: добавить policy gate ответа MCP discovery
This commit is contained in:
parent
58c1358960
commit
c744308223
|
|
@ -970,6 +970,39 @@ Validation:
|
||||||
- `npm test -- assistantMcpDiscoveryResponseCandidate.test.ts assistantMcpDiscoveryRuntimeEntryPoint.test.ts assistantMcpDiscoveryDebugAttachment.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts assistantMcpDiscoveryPilotExecutor.test.ts` passed 17/17;
|
- `npm test -- assistantMcpDiscoveryResponseCandidate.test.ts assistantMcpDiscoveryRuntimeEntryPoint.test.ts assistantMcpDiscoveryDebugAttachment.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts assistantMcpDiscoveryPilotExecutor.test.ts` passed 17/17;
|
||||||
- `npm run build` passed.
|
- `npm run build` passed.
|
||||||
|
|
||||||
|
## Progress Update - 2026-04-20 MCP Discovery Response Policy Gate
|
||||||
|
|
||||||
|
The thirteenth implementation slice of Big Block 5 added the first explicit guarded answer-replacement policy:
|
||||||
|
|
||||||
|
- `assistantMcpDiscoveryResponsePolicy.ts`
|
||||||
|
- `assistantMcpDiscoveryResponsePolicy.test.ts`
|
||||||
|
- living-chat runtime integration for unsupported current-turn meaning boundaries.
|
||||||
|
|
||||||
|
This is the first slice where MCP discovery can affect the user-facing answer, but only through a narrow policy gate.
|
||||||
|
|
||||||
|
The policy can apply a discovery candidate only when all of the following are true:
|
||||||
|
|
||||||
|
- the current living-chat branch is `unsupported_current_turn_meaning_boundary` or its deterministic boundary reply;
|
||||||
|
- the runtime meta contains a valid `assistant_mcp_discovery_runtime_entry_point_v1` contract;
|
||||||
|
- the candidate status is one of `ready_for_guarded_use`, `checked_sources_only_candidate`, or `clarification_candidate`;
|
||||||
|
- `eligible_for_future_hot_runtime=true`;
|
||||||
|
- the candidate has non-empty user-facing text;
|
||||||
|
- the text does not expose internal primitive/query/runtime mechanics.
|
||||||
|
|
||||||
|
When the gate does not pass, the old honest boundary answer is preserved.
|
||||||
|
|
||||||
|
The living-chat debug surface now includes:
|
||||||
|
|
||||||
|
- `mcp_discovery_response_policy_v1`;
|
||||||
|
- `mcp_discovery_response_candidate_v1`;
|
||||||
|
- `mcp_discovery_response_applied`;
|
||||||
|
- the standard MCP discovery entry-point attachment fields on chat-path output.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- `npm test -- assistantMcpDiscoveryResponsePolicy.test.ts assistantLivingChatRuntimeAdapter.test.ts assistantMcpDiscoveryResponseCandidate.test.ts assistantMcpDiscoveryDebugAttachment.test.ts` passed 19/19;
|
||||||
|
- `npm run build` passed.
|
||||||
|
|
||||||
## Execution Rule
|
## Execution Rule
|
||||||
|
|
||||||
Do not implement this plan as:
|
Do not implement this plan as:
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.runAssistantLivingChatRuntime = runAssistantLivingChatRuntime;
|
exports.runAssistantLivingChatRuntime = runAssistantLivingChatRuntime;
|
||||||
const assistantMemoryRecapPolicy_1 = require("./assistantMemoryRecapPolicy");
|
const assistantMemoryRecapPolicy_1 = require("./assistantMemoryRecapPolicy");
|
||||||
const assistantContinuityPolicy_1 = require("./assistantContinuityPolicy");
|
const assistantContinuityPolicy_1 = require("./assistantContinuityPolicy");
|
||||||
|
const assistantMcpDiscoveryDebugAttachment_1 = require("./assistantMcpDiscoveryDebugAttachment");
|
||||||
|
const assistantMcpDiscoveryResponsePolicy_1 = require("./assistantMcpDiscoveryResponsePolicy");
|
||||||
function hasPriorAssistantTurn(items) {
|
function hasPriorAssistantTurn(items) {
|
||||||
if (!Array.isArray(items)) {
|
if (!Array.isArray(items)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -228,6 +230,17 @@ async function runAssistantLivingChatRuntime(input) {
|
||||||
debug: null
|
debug: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const mcpDiscoveryResponsePolicy = (0, assistantMcpDiscoveryResponsePolicy_1.applyAssistantMcpDiscoveryResponsePolicy)({
|
||||||
|
currentReply: chatText,
|
||||||
|
currentReplySource: livingChatSource,
|
||||||
|
livingChatSource,
|
||||||
|
modeDecisionReason: input.modeDecision?.reason ?? null,
|
||||||
|
addressRuntimeMeta
|
||||||
|
});
|
||||||
|
if (mcpDiscoveryResponsePolicy.applied) {
|
||||||
|
chatText = mcpDiscoveryResponsePolicy.reply_text;
|
||||||
|
livingChatSource = mcpDiscoveryResponsePolicy.reply_source;
|
||||||
|
}
|
||||||
const predecomposeContract = addressRuntimeMeta.predecomposeContract && typeof addressRuntimeMeta.predecomposeContract === "object"
|
const predecomposeContract = addressRuntimeMeta.predecomposeContract && typeof addressRuntimeMeta.predecomposeContract === "object"
|
||||||
? addressRuntimeMeta.predecomposeContract
|
? addressRuntimeMeta.predecomposeContract
|
||||||
: null;
|
: null;
|
||||||
|
|
@ -245,6 +258,9 @@ async function runAssistantLivingChatRuntime(input) {
|
||||||
living_router_mode: input.modeDecision?.mode ?? "chat",
|
living_router_mode: input.modeDecision?.mode ?? "chat",
|
||||||
living_router_reason: input.modeDecision?.reason ?? "living_chat_signal_detected",
|
living_router_reason: input.modeDecision?.reason ?? "living_chat_signal_detected",
|
||||||
living_chat_response_source: livingChatSource,
|
living_chat_response_source: livingChatSource,
|
||||||
|
mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy,
|
||||||
|
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
|
||||||
|
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied,
|
||||||
living_chat_script_guard_applied: livingChatScriptGuardApplied,
|
living_chat_script_guard_applied: livingChatScriptGuardApplied,
|
||||||
living_chat_script_guard_reason: livingChatScriptGuardReason,
|
living_chat_script_guard_reason: livingChatScriptGuardReason,
|
||||||
living_chat_grounding_guard_applied: livingChatGroundingGuardApplied,
|
living_chat_grounding_guard_applied: livingChatGroundingGuardApplied,
|
||||||
|
|
@ -278,6 +294,6 @@ async function runAssistantLivingChatRuntime(input) {
|
||||||
return {
|
return {
|
||||||
handled: true,
|
handled: true,
|
||||||
chatText,
|
chatText,
|
||||||
debug
|
debug: (0, assistantMcpDiscoveryDebugAttachment_1.attachAssistantMcpDiscoveryDebug)(debug, { addressRuntimeMeta })
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
122
llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponsePolicy.js
vendored
Normal file
122
llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponsePolicy.js
vendored
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION = void 0;
|
||||||
|
exports.applyAssistantMcpDiscoveryResponsePolicy = applyAssistantMcpDiscoveryResponsePolicy;
|
||||||
|
const assistantMcpDiscoveryResponseCandidate_1 = require("./assistantMcpDiscoveryResponseCandidate");
|
||||||
|
exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION = "assistant_mcp_discovery_response_policy_v1";
|
||||||
|
const ALLOWED_CANDIDATE_STATUSES = new Set([
|
||||||
|
"ready_for_guarded_use",
|
||||||
|
"checked_sources_only_candidate",
|
||||||
|
"clarification_candidate"
|
||||||
|
]);
|
||||||
|
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 normalizeReasonCode(value) {
|
||||||
|
const normalized = value
|
||||||
|
.trim()
|
||||||
|
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
|
||||||
|
.replace(/^_+|_+$/g, "")
|
||||||
|
.toLowerCase();
|
||||||
|
return normalized.length > 0 ? normalized.slice(0, 120) : null;
|
||||||
|
}
|
||||||
|
function pushReason(target, value) {
|
||||||
|
const normalized = normalizeReasonCode(value);
|
||||||
|
if (normalized && !target.includes(normalized)) {
|
||||||
|
target.push(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function hasInternalMechanics(value) {
|
||||||
|
const text = value.toLowerCase();
|
||||||
|
return (text.includes("query_documents") ||
|
||||||
|
text.includes("query_movements") ||
|
||||||
|
text.includes("primitive") ||
|
||||||
|
text.includes("runtime_") ||
|
||||||
|
text.includes("planner_") ||
|
||||||
|
text.includes("catalog_") ||
|
||||||
|
text.includes("select "));
|
||||||
|
}
|
||||||
|
function isMcpDiscoveryEntryPointContract(value) {
|
||||||
|
const record = toRecordObject(value);
|
||||||
|
return (record?.schema_version === "assistant_mcp_discovery_runtime_entry_point_v1" &&
|
||||||
|
record?.policy_owner === "assistantMcpDiscoveryRuntimeEntryPoint");
|
||||||
|
}
|
||||||
|
function resolveEntryPoint(input) {
|
||||||
|
if (isMcpDiscoveryEntryPointContract(input.entryPoint)) {
|
||||||
|
return input.entryPoint;
|
||||||
|
}
|
||||||
|
const runtimeMetaEntryPoint = input.addressRuntimeMeta?.mcpDiscoveryRuntimeEntryPoint ??
|
||||||
|
input.addressRuntimeMeta?.assistantMcpDiscoveryRuntimeEntryPoint ??
|
||||||
|
input.addressRuntimeMeta?.assistant_mcp_discovery_entry_point_v1;
|
||||||
|
return isMcpDiscoveryEntryPointContract(runtimeMetaEntryPoint) ? runtimeMetaEntryPoint : null;
|
||||||
|
}
|
||||||
|
function isUnsupportedCurrentTurnBoundary(input) {
|
||||||
|
return (input.modeDecisionReason === "unsupported_current_turn_meaning_boundary" ||
|
||||||
|
input.livingChatSource === "deterministic_unsupported_current_turn_boundary" ||
|
||||||
|
input.currentReplySource === "deterministic_unsupported_current_turn_boundary");
|
||||||
|
}
|
||||||
|
function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||||
|
const currentReply = String(input.currentReply ?? "");
|
||||||
|
const currentReplySource = toNonEmptyString(input.currentReplySource) ?? toNonEmptyString(input.livingChatSource) ?? "unknown";
|
||||||
|
const entryPoint = resolveEntryPoint(input);
|
||||||
|
const candidate = (0, assistantMcpDiscoveryResponseCandidate_1.buildAssistantMcpDiscoveryResponseCandidate)(entryPoint);
|
||||||
|
const reasonCodes = [...candidate.reason_codes];
|
||||||
|
if (!entryPoint) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
|
||||||
|
}
|
||||||
|
if (!isUnsupportedCurrentTurnBoundary(input)) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_not_unsupported_boundary");
|
||||||
|
}
|
||||||
|
if (!ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status)) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed");
|
||||||
|
}
|
||||||
|
if (!candidate.eligible_for_future_hot_runtime) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_not_eligible");
|
||||||
|
}
|
||||||
|
if (!toNonEmptyString(candidate.reply_text)) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_missing_reply_text");
|
||||||
|
}
|
||||||
|
if (candidate.reply_text && hasInternalMechanics(candidate.reply_text)) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_contains_internal_mechanics");
|
||||||
|
}
|
||||||
|
const canApply = Boolean(entryPoint) &&
|
||||||
|
isUnsupportedCurrentTurnBoundary(input) &&
|
||||||
|
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
||||||
|
candidate.eligible_for_future_hot_runtime &&
|
||||||
|
Boolean(toNonEmptyString(candidate.reply_text)) &&
|
||||||
|
!hasInternalMechanics(String(candidate.reply_text ?? ""));
|
||||||
|
if (!canApply) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_kept_current_reply");
|
||||||
|
return {
|
||||||
|
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION,
|
||||||
|
policy_owner: "assistantMcpDiscoveryResponsePolicy",
|
||||||
|
decision: "keep_current_reply",
|
||||||
|
applied: false,
|
||||||
|
reply_text: currentReply,
|
||||||
|
reply_source: currentReplySource,
|
||||||
|
candidate,
|
||||||
|
reason_codes: reasonCodes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_applied");
|
||||||
|
return {
|
||||||
|
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION,
|
||||||
|
policy_owner: "assistantMcpDiscoveryResponsePolicy",
|
||||||
|
decision: "apply_candidate",
|
||||||
|
applied: true,
|
||||||
|
reply_text: String(candidate.reply_text),
|
||||||
|
reply_source: "mcp_discovery_response_candidate_guarded",
|
||||||
|
candidate,
|
||||||
|
reason_codes: reasonCodes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,8 @@ import {
|
||||||
resolveAssistantLivingChatMemoryContext
|
resolveAssistantLivingChatMemoryContext
|
||||||
} from "./assistantMemoryRecapPolicy";
|
} from "./assistantMemoryRecapPolicy";
|
||||||
import { resolveAssistantOrganizationAuthority } from "./assistantContinuityPolicy";
|
import { resolveAssistantOrganizationAuthority } from "./assistantContinuityPolicy";
|
||||||
|
import { attachAssistantMcpDiscoveryDebug } from "./assistantMcpDiscoveryDebugAttachment";
|
||||||
|
import { applyAssistantMcpDiscoveryResponsePolicy } from "./assistantMcpDiscoveryResponsePolicy";
|
||||||
|
|
||||||
export interface AssistantLivingChatSessionScopeInput {
|
export interface AssistantLivingChatSessionScopeInput {
|
||||||
knownOrganizations?: unknown[];
|
knownOrganizations?: unknown[];
|
||||||
|
|
@ -305,6 +307,18 @@ export async function runAssistantLivingChatRuntime(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mcpDiscoveryResponsePolicy = applyAssistantMcpDiscoveryResponsePolicy({
|
||||||
|
currentReply: chatText,
|
||||||
|
currentReplySource: livingChatSource,
|
||||||
|
livingChatSource,
|
||||||
|
modeDecisionReason: input.modeDecision?.reason ?? null,
|
||||||
|
addressRuntimeMeta
|
||||||
|
});
|
||||||
|
if (mcpDiscoveryResponsePolicy.applied) {
|
||||||
|
chatText = mcpDiscoveryResponsePolicy.reply_text;
|
||||||
|
livingChatSource = mcpDiscoveryResponsePolicy.reply_source;
|
||||||
|
}
|
||||||
|
|
||||||
const predecomposeContract =
|
const predecomposeContract =
|
||||||
addressRuntimeMeta.predecomposeContract && typeof addressRuntimeMeta.predecomposeContract === "object"
|
addressRuntimeMeta.predecomposeContract && typeof addressRuntimeMeta.predecomposeContract === "object"
|
||||||
? (addressRuntimeMeta.predecomposeContract as Record<string, unknown>)
|
? (addressRuntimeMeta.predecomposeContract as Record<string, unknown>)
|
||||||
|
|
@ -325,6 +339,9 @@ export async function runAssistantLivingChatRuntime(
|
||||||
living_router_mode: input.modeDecision?.mode ?? "chat",
|
living_router_mode: input.modeDecision?.mode ?? "chat",
|
||||||
living_router_reason: input.modeDecision?.reason ?? "living_chat_signal_detected",
|
living_router_reason: input.modeDecision?.reason ?? "living_chat_signal_detected",
|
||||||
living_chat_response_source: livingChatSource,
|
living_chat_response_source: livingChatSource,
|
||||||
|
mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy,
|
||||||
|
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
|
||||||
|
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied,
|
||||||
living_chat_script_guard_applied: livingChatScriptGuardApplied,
|
living_chat_script_guard_applied: livingChatScriptGuardApplied,
|
||||||
living_chat_script_guard_reason: livingChatScriptGuardReason,
|
living_chat_script_guard_reason: livingChatScriptGuardReason,
|
||||||
living_chat_grounding_guard_applied: livingChatGroundingGuardApplied,
|
living_chat_grounding_guard_applied: livingChatGroundingGuardApplied,
|
||||||
|
|
@ -359,6 +376,6 @@ export async function runAssistantLivingChatRuntime(
|
||||||
return {
|
return {
|
||||||
handled: true,
|
handled: true,
|
||||||
chatText,
|
chatText,
|
||||||
debug
|
debug: attachAssistantMcpDiscoveryDebug(debug, { addressRuntimeMeta })
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
import {
|
||||||
|
buildAssistantMcpDiscoveryResponseCandidate,
|
||||||
|
type AssistantMcpDiscoveryResponseCandidateContract,
|
||||||
|
type AssistantMcpDiscoveryResponseCandidateStatus
|
||||||
|
} from "./assistantMcpDiscoveryResponseCandidate";
|
||||||
|
import type { AssistantMcpDiscoveryRuntimeEntryPointContract } from "./assistantMcpDiscoveryRuntimeEntryPoint";
|
||||||
|
|
||||||
|
export const ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION =
|
||||||
|
"assistant_mcp_discovery_response_policy_v1" as const;
|
||||||
|
|
||||||
|
export type AssistantMcpDiscoveryResponsePolicyDecision = "apply_candidate" | "keep_current_reply";
|
||||||
|
|
||||||
|
export interface ApplyAssistantMcpDiscoveryResponsePolicyInput {
|
||||||
|
currentReply: string;
|
||||||
|
currentReplySource?: string | null;
|
||||||
|
livingChatSource?: string | null;
|
||||||
|
modeDecisionReason?: string | null;
|
||||||
|
addressRuntimeMeta?: Record<string, unknown> | null;
|
||||||
|
entryPoint?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantMcpDiscoveryResponsePolicyResult {
|
||||||
|
schema_version: typeof ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION;
|
||||||
|
policy_owner: "assistantMcpDiscoveryResponsePolicy";
|
||||||
|
decision: AssistantMcpDiscoveryResponsePolicyDecision;
|
||||||
|
applied: boolean;
|
||||||
|
reply_text: string;
|
||||||
|
reply_source: string;
|
||||||
|
candidate: AssistantMcpDiscoveryResponseCandidateContract;
|
||||||
|
reason_codes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ALLOWED_CANDIDATE_STATUSES = new Set<AssistantMcpDiscoveryResponseCandidateStatus>([
|
||||||
|
"ready_for_guarded_use",
|
||||||
|
"checked_sources_only_candidate",
|
||||||
|
"clarification_candidate"
|
||||||
|
]);
|
||||||
|
|
||||||
|
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 normalizeReasonCode(value: string): string | null {
|
||||||
|
const normalized = value
|
||||||
|
.trim()
|
||||||
|
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
|
||||||
|
.replace(/^_+|_+$/g, "")
|
||||||
|
.toLowerCase();
|
||||||
|
return normalized.length > 0 ? normalized.slice(0, 120) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushReason(target: string[], value: string): void {
|
||||||
|
const normalized = normalizeReasonCode(value);
|
||||||
|
if (normalized && !target.includes(normalized)) {
|
||||||
|
target.push(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasInternalMechanics(value: string): boolean {
|
||||||
|
const text = value.toLowerCase();
|
||||||
|
return (
|
||||||
|
text.includes("query_documents") ||
|
||||||
|
text.includes("query_movements") ||
|
||||||
|
text.includes("primitive") ||
|
||||||
|
text.includes("runtime_") ||
|
||||||
|
text.includes("planner_") ||
|
||||||
|
text.includes("catalog_") ||
|
||||||
|
text.includes("select ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMcpDiscoveryEntryPointContract(value: unknown): value is AssistantMcpDiscoveryRuntimeEntryPointContract {
|
||||||
|
const record = toRecordObject(value);
|
||||||
|
return (
|
||||||
|
record?.schema_version === "assistant_mcp_discovery_runtime_entry_point_v1" &&
|
||||||
|
record?.policy_owner === "assistantMcpDiscoveryRuntimeEntryPoint"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveEntryPoint(
|
||||||
|
input: ApplyAssistantMcpDiscoveryResponsePolicyInput
|
||||||
|
): AssistantMcpDiscoveryRuntimeEntryPointContract | null {
|
||||||
|
if (isMcpDiscoveryEntryPointContract(input.entryPoint)) {
|
||||||
|
return input.entryPoint;
|
||||||
|
}
|
||||||
|
const runtimeMetaEntryPoint =
|
||||||
|
input.addressRuntimeMeta?.mcpDiscoveryRuntimeEntryPoint ??
|
||||||
|
input.addressRuntimeMeta?.assistantMcpDiscoveryRuntimeEntryPoint ??
|
||||||
|
input.addressRuntimeMeta?.assistant_mcp_discovery_entry_point_v1;
|
||||||
|
return isMcpDiscoveryEntryPointContract(runtimeMetaEntryPoint) ? runtimeMetaEntryPoint : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUnsupportedCurrentTurnBoundary(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): boolean {
|
||||||
|
return (
|
||||||
|
input.modeDecisionReason === "unsupported_current_turn_meaning_boundary" ||
|
||||||
|
input.livingChatSource === "deterministic_unsupported_current_turn_boundary" ||
|
||||||
|
input.currentReplySource === "deterministic_unsupported_current_turn_boundary"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||||
|
input: ApplyAssistantMcpDiscoveryResponsePolicyInput
|
||||||
|
): AssistantMcpDiscoveryResponsePolicyResult {
|
||||||
|
const currentReply = String(input.currentReply ?? "");
|
||||||
|
const currentReplySource =
|
||||||
|
toNonEmptyString(input.currentReplySource) ?? toNonEmptyString(input.livingChatSource) ?? "unknown";
|
||||||
|
const entryPoint = resolveEntryPoint(input);
|
||||||
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(entryPoint);
|
||||||
|
const reasonCodes = [...candidate.reason_codes];
|
||||||
|
|
||||||
|
if (!entryPoint) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
|
||||||
|
}
|
||||||
|
if (!isUnsupportedCurrentTurnBoundary(input)) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_not_unsupported_boundary");
|
||||||
|
}
|
||||||
|
if (!ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status)) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed");
|
||||||
|
}
|
||||||
|
if (!candidate.eligible_for_future_hot_runtime) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_not_eligible");
|
||||||
|
}
|
||||||
|
if (!toNonEmptyString(candidate.reply_text)) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_missing_reply_text");
|
||||||
|
}
|
||||||
|
if (candidate.reply_text && hasInternalMechanics(candidate.reply_text)) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_contains_internal_mechanics");
|
||||||
|
}
|
||||||
|
|
||||||
|
const canApply =
|
||||||
|
Boolean(entryPoint) &&
|
||||||
|
isUnsupportedCurrentTurnBoundary(input) &&
|
||||||
|
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
||||||
|
candidate.eligible_for_future_hot_runtime &&
|
||||||
|
Boolean(toNonEmptyString(candidate.reply_text)) &&
|
||||||
|
!hasInternalMechanics(String(candidate.reply_text ?? ""));
|
||||||
|
|
||||||
|
if (!canApply) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_kept_current_reply");
|
||||||
|
return {
|
||||||
|
schema_version: ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION,
|
||||||
|
policy_owner: "assistantMcpDiscoveryResponsePolicy",
|
||||||
|
decision: "keep_current_reply",
|
||||||
|
applied: false,
|
||||||
|
reply_text: currentReply,
|
||||||
|
reply_source: currentReplySource,
|
||||||
|
candidate,
|
||||||
|
reason_codes: reasonCodes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_applied");
|
||||||
|
return {
|
||||||
|
schema_version: ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION,
|
||||||
|
policy_owner: "assistantMcpDiscoveryResponsePolicy",
|
||||||
|
decision: "apply_candidate",
|
||||||
|
applied: true,
|
||||||
|
reply_text: String(candidate.reply_text),
|
||||||
|
reply_source: "mcp_discovery_response_candidate_guarded",
|
||||||
|
candidate,
|
||||||
|
reason_codes: reasonCodes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -169,6 +169,66 @@ describe("assistant living chat runtime adapter", () => {
|
||||||
expect(executeLlmChat).not.toHaveBeenCalled();
|
expect(executeLlmChat).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("replaces unsupported boundary with guarded MCP discovery response when policy allows it", async () => {
|
||||||
|
const executeLlmChat = vi.fn(async () => "raw-llm");
|
||||||
|
const input = buildRuntimeInput({
|
||||||
|
userMessage: "how long has svk been active",
|
||||||
|
modeDecision: { mode: "chat", reason: "unsupported_current_turn_meaning_boundary" },
|
||||||
|
addressRuntimeMeta: {
|
||||||
|
toolGateReason: "unsupported_current_turn_meaning_boundary",
|
||||||
|
orchestrationContract: {
|
||||||
|
unsupported_current_turn_meaning_boundary: true,
|
||||||
|
assistant_turn_meaning: {
|
||||||
|
unsupported_but_understood_family: "counterparty_lifecycle_or_age",
|
||||||
|
explicit_entity_candidates: [
|
||||||
|
{
|
||||||
|
type: "counterparty",
|
||||||
|
value: "SVK"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mcpDiscoveryRuntimeEntryPoint: {
|
||||||
|
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||||
|
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
|
||||||
|
entry_status: "bridge_executed",
|
||||||
|
hot_runtime_wired: false,
|
||||||
|
discovery_attempted: true,
|
||||||
|
turn_input: { adapter_status: "ready" },
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
user_facing_response_allowed: true,
|
||||||
|
business_fact_answer_allowed: true,
|
||||||
|
requires_user_clarification: false,
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference",
|
||||||
|
headline: "Confirmed scoped answer.",
|
||||||
|
confirmed_lines: ["Confirmed fact"],
|
||||||
|
inference_lines: ["Bounded inference"],
|
||||||
|
unknown_lines: ["Unconfirmed legal fact"],
|
||||||
|
limitation_lines: ["Limited source window"],
|
||||||
|
next_step_line: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reason_codes: ["runtime_entry_point_bridge_executed"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
executeLlmChat
|
||||||
|
});
|
||||||
|
|
||||||
|
const output = await runAssistantLivingChatRuntime(input);
|
||||||
|
|
||||||
|
expect(output.handled).toBe(true);
|
||||||
|
expect(output.chatText).toContain("Confirmed fact");
|
||||||
|
expect(output.chatText).not.toContain("route");
|
||||||
|
expect(output.chatText).not.toContain("query_documents");
|
||||||
|
expect(output.debug?.living_chat_response_source).toBe("mcp_discovery_response_candidate_guarded");
|
||||||
|
expect(output.debug?.mcp_discovery_response_applied).toBe(true);
|
||||||
|
expect(output.debug?.mcp_discovery_entry_status).toBe("bridge_executed");
|
||||||
|
expect(output.debug?.mcp_discovery_attempted).toBe(true);
|
||||||
|
expect(executeLlmChat).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("adds proactive organization offer on first smalltalk turn when multiple organizations are available", async () => {
|
it("adds proactive organization offer on first smalltalk turn when multiple organizations are available", async () => {
|
||||||
const resolveDataScopeProbe = vi.fn(async () => ({
|
const resolveDataScopeProbe = vi.fn(async () => ({
|
||||||
status: "resolved",
|
status: "resolved",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { applyAssistantMcpDiscoveryResponsePolicy } from "../src/services/assistantMcpDiscoveryResponsePolicy";
|
||||||
|
|
||||||
|
function entryPoint(overrides: Record<string, unknown> = {}) {
|
||||||
|
return {
|
||||||
|
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||||
|
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
|
||||||
|
entry_status: "bridge_executed",
|
||||||
|
hot_runtime_wired: false,
|
||||||
|
discovery_attempted: true,
|
||||||
|
turn_input: { adapter_status: "ready" },
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
user_facing_response_allowed: true,
|
||||||
|
business_fact_answer_allowed: true,
|
||||||
|
requires_user_clarification: false,
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference",
|
||||||
|
headline: "Confirmed scoped answer.",
|
||||||
|
confirmed_lines: ["Confirmed fact"],
|
||||||
|
inference_lines: ["Bounded inference"],
|
||||||
|
unknown_lines: ["Unconfirmed fact"],
|
||||||
|
limitation_lines: ["Limited source window"],
|
||||||
|
next_step_line: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reason_codes: ["runtime_entry_point_bridge_executed"],
|
||||||
|
...overrides
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("assistant MCP discovery response policy", () => {
|
||||||
|
it("applies a guarded candidate only for unsupported current-turn boundary replies", () => {
|
||||||
|
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||||
|
currentReply: "route is not wired",
|
||||||
|
currentReplySource: "deterministic_unsupported_current_turn_boundary",
|
||||||
|
modeDecisionReason: "unsupported_current_turn_meaning_boundary",
|
||||||
|
addressRuntimeMeta: {
|
||||||
|
mcpDiscoveryRuntimeEntryPoint: entryPoint()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.applied).toBe(true);
|
||||||
|
expect(result.decision).toBe("apply_candidate");
|
||||||
|
expect(result.reply_source).toBe("mcp_discovery_response_candidate_guarded");
|
||||||
|
expect(result.reply_text).toContain("Confirmed fact");
|
||||||
|
expect(result.reply_text).not.toContain("query_documents");
|
||||||
|
expect(result.reason_codes).toContain("mcp_discovery_response_policy_candidate_applied");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the current reply when the turn is not an unsupported boundary", () => {
|
||||||
|
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||||
|
currentReply: "regular chat",
|
||||||
|
currentReplySource: "llm_chat",
|
||||||
|
modeDecisionReason: "living_chat_signal_detected",
|
||||||
|
addressRuntimeMeta: {
|
||||||
|
mcpDiscoveryRuntimeEntryPoint: entryPoint()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.applied).toBe(false);
|
||||||
|
expect(result.decision).toBe("keep_current_reply");
|
||||||
|
expect(result.reply_text).toBe("regular chat");
|
||||||
|
expect(result.reply_source).toBe("llm_chat");
|
||||||
|
expect(result.reason_codes).toContain("mcp_discovery_response_policy_not_unsupported_boundary");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the current reply when the candidate has no grounded text", () => {
|
||||||
|
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||||
|
currentReply: "route is not wired",
|
||||||
|
currentReplySource: "deterministic_unsupported_current_turn_boundary",
|
||||||
|
modeDecisionReason: "unsupported_current_turn_meaning_boundary",
|
||||||
|
addressRuntimeMeta: {
|
||||||
|
mcpDiscoveryRuntimeEntryPoint: entryPoint({
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "unsupported",
|
||||||
|
user_facing_response_allowed: true,
|
||||||
|
business_fact_answer_allowed: false,
|
||||||
|
requires_user_clarification: false,
|
||||||
|
answer_draft: null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.applied).toBe(false);
|
||||||
|
expect(result.reply_text).toBe("route is not wired");
|
||||||
|
expect(result.reason_codes).toContain("mcp_discovery_response_policy_candidate_not_eligible");
|
||||||
|
expect(result.reason_codes).toContain("mcp_discovery_response_policy_kept_current_reply");
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue