ARCH: добавить policy gate ответа MCP discovery

This commit is contained in:
dctouch 2026-04-20 12:06:22 +03:00
parent 58c1358960
commit c744308223
7 changed files with 515 additions and 2 deletions

View File

@ -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 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
Do not implement this plan as:

View File

@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.runAssistantLivingChatRuntime = runAssistantLivingChatRuntime;
const assistantMemoryRecapPolicy_1 = require("./assistantMemoryRecapPolicy");
const assistantContinuityPolicy_1 = require("./assistantContinuityPolicy");
const assistantMcpDiscoveryDebugAttachment_1 = require("./assistantMcpDiscoveryDebugAttachment");
const assistantMcpDiscoveryResponsePolicy_1 = require("./assistantMcpDiscoveryResponsePolicy");
function hasPriorAssistantTurn(items) {
if (!Array.isArray(items)) {
return false;
@ -228,6 +230,17 @@ async function runAssistantLivingChatRuntime(input) {
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"
? addressRuntimeMeta.predecomposeContract
: null;
@ -245,6 +258,9 @@ async function runAssistantLivingChatRuntime(input) {
living_router_mode: input.modeDecision?.mode ?? "chat",
living_router_reason: input.modeDecision?.reason ?? "living_chat_signal_detected",
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_reason: livingChatScriptGuardReason,
living_chat_grounding_guard_applied: livingChatGroundingGuardApplied,
@ -278,6 +294,6 @@ async function runAssistantLivingChatRuntime(input) {
return {
handled: true,
chatText,
debug
debug: (0, assistantMcpDiscoveryDebugAttachment_1.attachAssistantMcpDiscoveryDebug)(debug, { addressRuntimeMeta })
};
}

View 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
};
}

View File

@ -5,6 +5,8 @@ import {
resolveAssistantLivingChatMemoryContext
} from "./assistantMemoryRecapPolicy";
import { resolveAssistantOrganizationAuthority } from "./assistantContinuityPolicy";
import { attachAssistantMcpDiscoveryDebug } from "./assistantMcpDiscoveryDebugAttachment";
import { applyAssistantMcpDiscoveryResponsePolicy } from "./assistantMcpDiscoveryResponsePolicy";
export interface AssistantLivingChatSessionScopeInput {
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 =
addressRuntimeMeta.predecomposeContract && typeof addressRuntimeMeta.predecomposeContract === "object"
? (addressRuntimeMeta.predecomposeContract as Record<string, unknown>)
@ -325,6 +339,9 @@ export async function runAssistantLivingChatRuntime(
living_router_mode: input.modeDecision?.mode ?? "chat",
living_router_reason: input.modeDecision?.reason ?? "living_chat_signal_detected",
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_reason: livingChatScriptGuardReason,
living_chat_grounding_guard_applied: livingChatGroundingGuardApplied,
@ -359,6 +376,6 @@ export async function runAssistantLivingChatRuntime(
return {
handled: true,
chatText,
debug
debug: attachAssistantMcpDiscoveryDebug(debug, { addressRuntimeMeta })
};
}

View File

@ -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
};
}

View File

@ -169,6 +169,66 @@ describe("assistant living chat runtime adapter", () => {
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 () => {
const resolveDataScopeProbe = vi.fn(async () => ({
status: "resolved",

View File

@ -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");
});
});