diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js index 38e0289..9811bee 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js @@ -1109,6 +1109,14 @@ function statusFrom(entryPoint) { } return "not_applicable"; } +function hasGuardedHotHandoff(entryPoint) { + const bridge = toRecordObject(entryPoint?.bridge); + const handoff = toRecordObject(bridge?.execution_handoff); + return (entryPoint?.hot_runtime_wired === true && + bridge?.hot_runtime_wired === true && + handoff?.can_use_guarded_response === true && + handoff?.must_keep_internal_mechanics_hidden === true); +} function replyTypeFor(status) { if (status === "clarification_candidate") { return "clarification_required"; @@ -1149,9 +1157,12 @@ function buildReplyText(entryPoint, status) { function buildAssistantMcpDiscoveryResponseCandidate(entryPoint) { const entry = entryPoint ?? null; const status = statusFrom(entry); + const guardedHotHandoff = hasGuardedHotHandoff(entry); const reasonCodes = uniqueStrings(entry?.reason_codes ?? []); pushReason(reasonCodes, `mcp_discovery_response_candidate_${status}`); - pushReason(reasonCodes, "mcp_discovery_response_candidate_not_hot_wired"); + pushReason(reasonCodes, guardedHotHandoff + ? "mcp_discovery_response_candidate_guarded_hot_wired" + : "mcp_discovery_response_candidate_not_hot_wired"); const replyText = entry && (status === "ready_for_guarded_use" || status === "checked_sources_only_candidate" || status === "clarification_candidate") ? buildReplyText(entry, status) : null; @@ -1159,10 +1170,11 @@ function buildAssistantMcpDiscoveryResponseCandidate(entryPoint) { schema_version: exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION, policy_owner: "assistantMcpDiscoveryResponseCandidate", candidate_status: replyText ? status : status === "clarification_candidate" ? status : status, - hot_runtime_wired: false, + hot_runtime_wired: guardedHotHandoff, reply_type: replyTypeFor(status), reply_text: replyText, - eligible_for_future_hot_runtime: Boolean(replyText) && (status === "ready_for_guarded_use" || status === "checked_sources_only_candidate" || status === "clarification_candidate"), + eligible_for_future_hot_runtime: Boolean(replyText) && + (status === "ready_for_guarded_use" || status === "checked_sources_only_candidate" || status === "clarification_candidate"), must_keep_internal_mechanics_hidden: true, reason_codes: reasonCodes }; diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponsePolicy.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponsePolicy.js index a476961..a88fd8c 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponsePolicy.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponsePolicy.js @@ -675,6 +675,9 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) { if (!candidate.eligible_for_future_hot_runtime) { pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_not_eligible"); } + if (candidate.hot_runtime_wired) { + pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_guarded_hot_wired"); + } if (!toNonEmptyString(candidate.reply_text)) { pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_missing_reply_text"); } diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryRuntimeBridge.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryRuntimeBridge.js index d4cd882..c5314cb 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryRuntimeBridge.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryRuntimeBridge.js @@ -283,7 +283,7 @@ async function runAssistantMcpDiscoveryRuntimeBridge(input) { schema_version: exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION, policy_owner: "assistantMcpDiscoveryRuntimeBridge", bridge_status: bridgeStatus, - hot_runtime_wired: false, + hot_runtime_wired: executionHandoff.can_use_guarded_response, planner, pilot, answer_draft: answerDraft, diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryRuntimeEntryPoint.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryRuntimeEntryPoint.js index f04d016..b5294ff 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryRuntimeEntryPoint.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryRuntimeEntryPoint.js @@ -69,12 +69,14 @@ async function runAssistantMcpDiscoveryRuntimeEntryPoint(input) { }); const reasonCodes = uniqueStrings([...turnInput.reason_codes, ...bridge.reason_codes]); pushReason(reasonCodes, "runtime_entry_point_bridge_executed"); - pushReason(reasonCodes, "runtime_entry_point_not_wired_to_hot_assistant_answer"); + pushReason(reasonCodes, bridge.hot_runtime_wired + ? "runtime_entry_point_wired_to_guarded_hot_assistant_answer" + : "runtime_entry_point_not_wired_to_hot_assistant_answer"); return { schema_version: exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_ENTRY_POINT_SCHEMA_VERSION, policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint", entry_status: "bridge_executed", - hot_runtime_wired: false, + hot_runtime_wired: bridge.hot_runtime_wired, discovery_attempted: true, turn_input: turnInput, bridge, diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts index 24d37c0..559a249 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts @@ -16,7 +16,7 @@ export interface AssistantMcpDiscoveryResponseCandidateContract { schema_version: typeof ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION; policy_owner: "assistantMcpDiscoveryResponseCandidate"; candidate_status: AssistantMcpDiscoveryResponseCandidateStatus; - hot_runtime_wired: false; + hot_runtime_wired: boolean; reply_type: "partial_coverage" | "clarification_required" | "no_grounded_answer"; reply_text: string | null; eligible_for_future_hot_runtime: boolean; @@ -1334,6 +1334,17 @@ function statusFrom(entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | return "not_applicable"; } +function hasGuardedHotHandoff(entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null): boolean { + const bridge = toRecordObject(entryPoint?.bridge); + const handoff = toRecordObject(bridge?.execution_handoff); + return ( + entryPoint?.hot_runtime_wired === true && + bridge?.hot_runtime_wired === true && + handoff?.can_use_guarded_response === true && + handoff?.must_keep_internal_mechanics_hidden === true + ); +} + function replyTypeFor( status: AssistantMcpDiscoveryResponseCandidateStatus ): AssistantMcpDiscoveryResponseCandidateContract["reply_type"] { @@ -1384,9 +1395,15 @@ export function buildAssistantMcpDiscoveryResponseCandidate( ): AssistantMcpDiscoveryResponseCandidateContract { const entry = entryPoint ?? null; const status = statusFrom(entry); + const guardedHotHandoff = hasGuardedHotHandoff(entry); const reasonCodes = uniqueStrings(entry?.reason_codes ?? []); pushReason(reasonCodes, `mcp_discovery_response_candidate_${status}`); - pushReason(reasonCodes, "mcp_discovery_response_candidate_not_hot_wired"); + pushReason( + reasonCodes, + guardedHotHandoff + ? "mcp_discovery_response_candidate_guarded_hot_wired" + : "mcp_discovery_response_candidate_not_hot_wired" + ); const replyText = entry && (status === "ready_for_guarded_use" || status === "checked_sources_only_candidate" || status === "clarification_candidate") @@ -1397,11 +1414,12 @@ export function buildAssistantMcpDiscoveryResponseCandidate( schema_version: ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION, policy_owner: "assistantMcpDiscoveryResponseCandidate", candidate_status: replyText ? status : status === "clarification_candidate" ? status : status, - hot_runtime_wired: false, + hot_runtime_wired: guardedHotHandoff, reply_type: replyTypeFor(status), reply_text: replyText, eligible_for_future_hot_runtime: - Boolean(replyText) && (status === "ready_for_guarded_use" || status === "checked_sources_only_candidate" || status === "clarification_candidate"), + Boolean(replyText) && + (status === "ready_for_guarded_use" || status === "checked_sources_only_candidate" || status === "clarification_candidate"), must_keep_internal_mechanics_hidden: true, reason_codes: reasonCodes }; diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponsePolicy.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponsePolicy.ts index b8d0666..5bb04bb 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponsePolicy.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponsePolicy.ts @@ -910,6 +910,9 @@ export function applyAssistantMcpDiscoveryResponsePolicy( if (!candidate.eligible_for_future_hot_runtime) { pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_not_eligible"); } + if (candidate.hot_runtime_wired) { + pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_guarded_hot_wired"); + } if (!toNonEmptyString(candidate.reply_text)) { pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_missing_reply_text"); } diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryRuntimeBridge.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryRuntimeBridge.ts index 01eefbe..e26d064 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryRuntimeBridge.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryRuntimeBridge.ts @@ -99,7 +99,7 @@ export interface AssistantMcpDiscoveryRuntimeBridgeContract { schema_version: typeof ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION; policy_owner: "assistantMcpDiscoveryRuntimeBridge"; bridge_status: AssistantMcpDiscoveryRuntimeBridgeStatus; - hot_runtime_wired: false; + hot_runtime_wired: boolean; planner: AssistantMcpDiscoveryPlannerContract; pilot: AssistantMcpDiscoveryPilotExecutionContract; answer_draft: AssistantMcpDiscoveryAnswerDraftContract; @@ -445,7 +445,7 @@ export async function runAssistantMcpDiscoveryRuntimeBridge( schema_version: ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION, policy_owner: "assistantMcpDiscoveryRuntimeBridge", bridge_status: bridgeStatus, - hot_runtime_wired: false, + hot_runtime_wired: executionHandoff.can_use_guarded_response, planner, pilot, answer_draft: answerDraft, diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryRuntimeEntryPoint.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryRuntimeEntryPoint.ts index ca8b2db..5e07e1f 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryRuntimeEntryPoint.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryRuntimeEntryPoint.ts @@ -26,7 +26,7 @@ export interface AssistantMcpDiscoveryRuntimeEntryPointContract { schema_version: typeof ASSISTANT_MCP_DISCOVERY_RUNTIME_ENTRY_POINT_SCHEMA_VERSION; policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint"; entry_status: AssistantMcpDiscoveryRuntimeEntryPointStatus; - hot_runtime_wired: false; + hot_runtime_wired: boolean; discovery_attempted: boolean; turn_input: AssistantMcpDiscoveryTurnInputContract; bridge: AssistantMcpDiscoveryRuntimeBridgeContract | null; @@ -108,13 +108,18 @@ export async function runAssistantMcpDiscoveryRuntimeEntryPoint( }); const reasonCodes = uniqueStrings([...turnInput.reason_codes, ...bridge.reason_codes]); pushReason(reasonCodes, "runtime_entry_point_bridge_executed"); - pushReason(reasonCodes, "runtime_entry_point_not_wired_to_hot_assistant_answer"); + pushReason( + reasonCodes, + bridge.hot_runtime_wired + ? "runtime_entry_point_wired_to_guarded_hot_assistant_answer" + : "runtime_entry_point_not_wired_to_hot_assistant_answer" + ); return { schema_version: ASSISTANT_MCP_DISCOVERY_RUNTIME_ENTRY_POINT_SCHEMA_VERSION, policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint", entry_status: "bridge_executed", - hot_runtime_wired: false, + hot_runtime_wired: bridge.hot_runtime_wired, discovery_attempted: true, turn_input: turnInput, bridge, diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts index c39c0cf..4be5bf8 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts @@ -29,6 +29,45 @@ function entryPoint(overrides: Record = {}) { } as any; } +function hotValueFlowEntryPoint(overrides: Record = {}) { + const base = entryPoint({ + hot_runtime_wired: true, + bridge: { + bridge_status: "answer_draft_ready", + hot_runtime_wired: true, + user_facing_response_allowed: true, + business_fact_answer_allowed: true, + requires_user_clarification: false, + execution_handoff: { + schema_version: "assistant_mcp_discovery_execution_handoff_v1", + policy_owner: "assistantMcpDiscoveryExecutionHandoff", + handoff_status: "ready_for_guarded_response", + selected_chain_id: "value_flow", + route_candidate_status: "ready_for_reviewed_execution", + evidence_answer_mode: "confirmed_business_answer", + evidence_expected_coverage: "confirmed_coverage", + pilot_status: "executed", + answer_mode: "confirmed_with_bounded_inference", + mcp_execution_performed: true, + allowed_hot_chain: true, + can_use_guarded_response: true, + must_keep_internal_mechanics_hidden: true, + reason_codes: ["execution_handoff_guarded_response_ready"] + }, + answer_draft: { + answer_mode: "confirmed_with_bounded_inference", + headline: "Confirmed value flow.", + confirmed_lines: ["1C value-flow rows were found for counterparty SVK", "Confirmed scoped amount: 3 750 руб."], + inference_lines: ["Counterparty value-flow total was calculated from confirmed 1C movement rows"], + unknown_lines: [], + limitation_lines: [], + next_step_line: null + } + } + }); + return { ...base, ...overrides } as any; +} + describe("assistant MCP discovery response candidate", () => { it("builds a Russian guarded candidate from a confirmed discovery draft", () => { const candidate = buildAssistantMcpDiscoveryResponseCandidate(entryPoint()); @@ -44,6 +83,16 @@ describe("assistant MCP discovery response candidate", () => { expect(candidate.reply_text).not.toContain("primitive"); }); + it("marks an allowlisted value-flow handoff as hot-wired for guarded runtime use", () => { + const candidate = buildAssistantMcpDiscoveryResponseCandidate(hotValueFlowEntryPoint()); + + expect(candidate.candidate_status).toBe("ready_for_guarded_use"); + expect(candidate.hot_runtime_wired).toBe(true); + expect(candidate.eligible_for_future_hot_runtime).toBe(true); + expect(candidate.reason_codes).toContain("mcp_discovery_response_candidate_guarded_hot_wired"); + expect(candidate.reply_text).toContain("3 750"); + }); + it("keeps inventory reserve boundary answers direct instead of compacting into a money overview", () => { const candidate = buildAssistantMcpDiscoveryResponseCandidate( entryPoint({ diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeEntryPoint.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeEntryPoint.test.ts index c7be584..4c34119 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeEntryPoint.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeEntryPoint.test.ts @@ -126,7 +126,10 @@ describe("assistant MCP discovery runtime entry point", () => { expect(result.bridge?.bridge_status).toBe("answer_draft_ready"); expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_value_flow_query_movements_v1"); expect(result.bridge?.pilot.derived_value_flow?.total_amount).toBe(3750); - expect(result.bridge?.hot_runtime_wired).toBe(false); + expect(result.bridge?.hot_runtime_wired).toBe(true); + expect(result.bridge?.execution_handoff.can_use_guarded_response).toBe(true); + expect(result.hot_runtime_wired).toBe(true); + expect(result.reason_codes).toContain("runtime_entry_point_wired_to_guarded_hot_assistant_answer"); expect(result.reason_codes).toContain("mcp_discovery_unsupported_but_understood_turn"); });