ARCH: проверить и расширить gate ответа MCP discovery

This commit is contained in:
2026-04-20 12:23:53 +03:00
parent c744308223
commit 72804557aa
6 changed files with 254 additions and 4 deletions
@@ -65,18 +65,30 @@ function isUnsupportedCurrentTurnBoundary(input) {
input.livingChatSource === "deterministic_unsupported_current_turn_boundary" ||
input.currentReplySource === "deterministic_unsupported_current_turn_boundary");
}
function isDiscoveryReadyChatCandidate(input, entryPoint) {
const turnInput = toRecordObject(entryPoint?.turn_input);
return (entryPoint?.entry_status === "bridge_executed" &&
entryPoint.discovery_attempted === true &&
turnInput?.should_run_discovery === true &&
(input.livingChatSource === "llm_chat" || input.currentReplySource === "llm_chat"));
}
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];
const unsupportedBoundary = isUnsupportedCurrentTurnBoundary(input);
const discoveryReadyChatCandidate = isDiscoveryReadyChatCandidate(input, entryPoint);
if (!entryPoint) {
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
}
if (!isUnsupportedCurrentTurnBoundary(input)) {
if (!unsupportedBoundary) {
pushReason(reasonCodes, "mcp_discovery_response_policy_not_unsupported_boundary");
}
if (!discoveryReadyChatCandidate) {
pushReason(reasonCodes, "mcp_discovery_response_policy_not_discovery_ready_chat_candidate");
}
if (!ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status)) {
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed");
}
@@ -90,7 +102,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_contains_internal_mechanics");
}
const canApply = Boolean(entryPoint) &&
isUnsupportedCurrentTurnBoundary(input) &&
(unsupportedBoundary || discoveryReadyChatCandidate) &&
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
candidate.eligible_for_future_hot_runtime &&
Boolean(toNonEmptyString(candidate.reply_text)) &&
@@ -109,6 +109,19 @@ function isUnsupportedCurrentTurnBoundary(input: ApplyAssistantMcpDiscoveryRespo
);
}
function isDiscoveryReadyChatCandidate(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
const turnInput = toRecordObject(entryPoint?.turn_input);
return (
entryPoint?.entry_status === "bridge_executed" &&
entryPoint.discovery_attempted === true &&
turnInput?.should_run_discovery === true &&
(input.livingChatSource === "llm_chat" || input.currentReplySource === "llm_chat")
);
}
export function applyAssistantMcpDiscoveryResponsePolicy(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput
): AssistantMcpDiscoveryResponsePolicyResult {
@@ -118,13 +131,18 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
const entryPoint = resolveEntryPoint(input);
const candidate = buildAssistantMcpDiscoveryResponseCandidate(entryPoint);
const reasonCodes = [...candidate.reason_codes];
const unsupportedBoundary = isUnsupportedCurrentTurnBoundary(input);
const discoveryReadyChatCandidate = isDiscoveryReadyChatCandidate(input, entryPoint);
if (!entryPoint) {
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
}
if (!isUnsupportedCurrentTurnBoundary(input)) {
if (!unsupportedBoundary) {
pushReason(reasonCodes, "mcp_discovery_response_policy_not_unsupported_boundary");
}
if (!discoveryReadyChatCandidate) {
pushReason(reasonCodes, "mcp_discovery_response_policy_not_discovery_ready_chat_candidate");
}
if (!ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status)) {
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed");
}
@@ -140,7 +158,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
const canApply =
Boolean(entryPoint) &&
isUnsupportedCurrentTurnBoundary(input) &&
(unsupportedBoundary || discoveryReadyChatCandidate) &&
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
candidate.eligible_for_future_hot_runtime &&
Boolean(toNonEmptyString(candidate.reply_text)) &&
@@ -229,6 +229,54 @@ describe("assistant living chat runtime adapter", () => {
expect(executeLlmChat).not.toHaveBeenCalled();
});
it("replaces discovery-ready llm chat business answer with guarded MCP discovery response", async () => {
const executeLlmChat = vi.fn(async () => "stale llm answer with old date");
const input = buildRuntimeInput({
userMessage: "how long has svk been active",
modeDecision: { mode: "chat", reason: "non_domain_query_indexed" },
addressRuntimeMeta: {
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",
should_run_discovery: true
},
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: [],
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("old date");
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(executeLlmChat).toHaveBeenCalledTimes(1);
});
it("adds proactive organization offer on first smalltalk turn when multiple organizations are available", async () => {
const resolveDataScopeProbe = vi.fn(async () => ({
status: "resolved",
@@ -63,6 +63,30 @@ describe("assistant MCP discovery response policy", () => {
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");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_not_discovery_ready_chat_candidate");
});
it("applies a guarded candidate for discovery-ready llm chat business answers", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "stale llm business answer",
currentReplySource: "llm_chat",
modeDecisionReason: "non_domain_query_indexed",
addressRuntimeMeta: {
mcpDiscoveryRuntimeEntryPoint: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true
}
})
}
});
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.reason_codes).toContain("mcp_discovery_response_policy_not_unsupported_boundary");
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_not_discovery_ready_chat_candidate");
});
it("keeps the current reply when the candidate has no grounded text", () => {