diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryDebugAttachment.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryDebugAttachment.js index 69cfdd5..618b8af 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryDebugAttachment.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryDebugAttachment.js @@ -42,6 +42,11 @@ function isEvidencePlannerContract(value) { const record = toRecordObject(value); return record?.schema_version === "assistant_evidence_planner_v1" && record?.policy_owner === "assistantEvidencePlanner"; } +function isExecutionHandoffContract(value) { + const record = toRecordObject(value); + return (record?.schema_version === "assistant_mcp_discovery_execution_handoff_v1" && + record?.policy_owner === "assistantMcpDiscoveryExecutionHandoff"); +} function resolveEntryPoint(input) { if (isMcpDiscoveryEntryPointContract(input.entryPoint)) { return input.entryPoint; @@ -61,6 +66,7 @@ function buildAssistantMcpDiscoveryDebugAttachmentFields(input) { const evidencePlanAnswerContract = toRecordObject(evidencePlan?.answer_contract); const chainAlignment = toRecordObject(planner?.catalog_chain_template_alignment); const routeCandidate = isRouteCandidateContract(bridge?.route_candidate) ? bridge.route_candidate : null; + const executionHandoff = isExecutionHandoffContract(bridge?.execution_handoff) ? bridge.execution_handoff : null; const answerDraft = toRecordObject(bridge?.answer_draft); return { assistant_mcp_discovery_entry_point_v1: entryPoint, @@ -91,6 +97,10 @@ function buildAssistantMcpDiscoveryDebugAttachmentFields(input) { mcp_discovery_route_candidate_executable_now: routeCandidate?.executable_now === true, mcp_discovery_route_candidate_enablement_reason: toNonEmptyString(routeCandidate?.enablement_reason), mcp_discovery_route_candidate_next_action: toNonEmptyString(routeCandidate?.recommended_next_action), + mcp_discovery_execution_handoff_v1: executionHandoff, + mcp_discovery_execution_handoff_status: toNonEmptyString(executionHandoff?.handoff_status), + mcp_discovery_execution_handoff_allowed_hot_chain: executionHandoff?.allowed_hot_chain === true, + mcp_discovery_execution_handoff_can_use_guarded_response: executionHandoff?.can_use_guarded_response === true, mcp_discovery_answer_mode: toNonEmptyString(answerDraft?.answer_mode), mcp_discovery_business_fact_answer_allowed: bridge?.business_fact_answer_allowed === true, mcp_discovery_user_facing_response_allowed: bridge?.user_facing_response_allowed === true, diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryExecutionHandoff.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryExecutionHandoff.js new file mode 100644 index 0000000..30a5914 --- /dev/null +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryExecutionHandoff.js @@ -0,0 +1,83 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ASSISTANT_MCP_DISCOVERY_EXECUTION_HANDOFF_SCHEMA_VERSION = void 0; +exports.buildAssistantMcpDiscoveryExecutionHandoff = buildAssistantMcpDiscoveryExecutionHandoff; +exports.ASSISTANT_MCP_DISCOVERY_EXECUTION_HANDOFF_SCHEMA_VERSION = "assistant_mcp_discovery_execution_handoff_v1"; +const HOT_HANDOFF_CHAIN_ALLOWLIST = ["value_flow"]; +function uniqueStrings(values) { + const result = []; + for (const value of values) { + const text = String(value ?? "").trim(); + if (text && !result.includes(text)) { + result.push(text); + } + } + return result; +} +function handoffStatusFor(input, allowedHotChain) { + if (input.bridgeStatus === "needs_clarification" || input.routeCandidate.candidate_status === "needs_user_scope") { + return "awaiting_user_scope"; + } + if (input.bridgeStatus === "checked_sources_only") { + return "checked_sources_only"; + } + if (input.bridgeStatus === "blocked" || + input.bridgeStatus === "unsupported" || + input.routeCandidate.candidate_status === "blocked" || + input.routeCandidate.candidate_status === "needs_route_enablement") { + return "blocked"; + } + if (!allowedHotChain) { + return "not_enabled_for_chain"; + } + return "ready_for_guarded_response"; +} +function buildAssistantMcpDiscoveryExecutionHandoff(input) { + const allowedHotChain = HOT_HANDOFF_CHAIN_ALLOWLIST.includes(input.routeCandidate.selected_chain_id); + const baseStatus = handoffStatusFor(input, allowedHotChain); + const readinessChecksPassed = baseStatus === "ready_for_guarded_response" && + input.bridgeStatus === "answer_draft_ready" && + input.routeCandidate.candidate_status === "ready_for_reviewed_execution" && + input.routeCandidate.executable_now === true && + input.pilot.pilot_status === "executed" && + input.pilot.mcp_execution_performed === true && + input.businessFactAnswerAllowed === true && + input.userFacingResponseAllowed === true && + input.answerDraft.internal_mechanics_allowed === false; + const handoffStatus = readinessChecksPassed ? "ready_for_guarded_response" : baseStatus; + const reasonCodes = uniqueStrings([ + `execution_handoff_status_${handoffStatus}`, + allowedHotChain ? "execution_handoff_chain_allowlisted" : "execution_handoff_chain_not_allowlisted", + input.routeCandidate.executable_now + ? "execution_handoff_route_candidate_executable" + : "execution_handoff_route_candidate_not_executable", + input.pilot.mcp_execution_performed + ? "execution_handoff_mcp_execution_performed" + : "execution_handoff_mcp_execution_not_performed", + input.businessFactAnswerAllowed + ? "execution_handoff_business_fact_allowed" + : "execution_handoff_business_fact_not_allowed", + input.userFacingResponseAllowed + ? "execution_handoff_user_facing_allowed" + : "execution_handoff_user_facing_not_allowed", + readinessChecksPassed + ? "execution_handoff_guarded_response_ready" + : "execution_handoff_guarded_response_not_ready" + ]); + return { + schema_version: exports.ASSISTANT_MCP_DISCOVERY_EXECUTION_HANDOFF_SCHEMA_VERSION, + policy_owner: "assistantMcpDiscoveryExecutionHandoff", + handoff_status: handoffStatus, + selected_chain_id: input.routeCandidate.selected_chain_id, + route_candidate_status: input.routeCandidate.candidate_status, + evidence_answer_mode: input.routeCandidate.evidence_answer_mode, + evidence_expected_coverage: input.routeCandidate.evidence_expected_coverage, + pilot_status: input.pilot.pilot_status, + answer_mode: input.answerDraft.answer_mode, + mcp_execution_performed: input.pilot.mcp_execution_performed, + allowed_hot_chain: allowedHotChain, + can_use_guarded_response: readinessChecksPassed, + must_keep_internal_mechanics_hidden: true, + reason_codes: reasonCodes + }; +} diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryRuntimeBridge.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryRuntimeBridge.js index 77be4d9..d4cd882 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryRuntimeBridge.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryRuntimeBridge.js @@ -4,6 +4,7 @@ exports.ASSISTANT_MCP_ROUTE_CANDIDATE_SCHEMA_VERSION = exports.ASSISTANT_MCP_DIS exports.runAssistantMcpDiscoveryRuntimeBridge = runAssistantMcpDiscoveryRuntimeBridge; const assistantMcpDiscoveryAnswerAdapter_1 = require("./assistantMcpDiscoveryAnswerAdapter"); const assistantMcpDiscoveryPilotExecutor_1 = require("./assistantMcpDiscoveryPilotExecutor"); +const assistantMcpDiscoveryExecutionHandoff_1 = require("./assistantMcpDiscoveryExecutionHandoff"); const assistantMcpDiscoveryPlanner_1 = require("./assistantMcpDiscoveryPlanner"); exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION = "assistant_mcp_discovery_runtime_bridge_v1"; exports.ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION = "assistant_mcp_discovery_loop_state_v1"; @@ -258,12 +259,26 @@ async function runAssistantMcpDiscoveryRuntimeBridge(input) { const bridgeStatus = bridgeStatusFor(pilot, answerDraft); const loopState = buildLoopState(planner, pilot, bridgeStatus); const routeCandidate = buildRouteCandidate(planner, pilot, bridgeStatus); + const userFacingResponseAllowed = bridgeStatus !== "blocked"; + const businessFactAllowed = businessFactAnswerAllowed(answerDraft); + const executionHandoff = (0, assistantMcpDiscoveryExecutionHandoff_1.buildAssistantMcpDiscoveryExecutionHandoff)({ + bridgeStatus, + routeCandidate, + pilot, + answerDraft, + businessFactAnswerAllowed: businessFactAllowed, + userFacingResponseAllowed + }); const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]); pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`); pushReason(reasonCodes, "runtime_bridge_not_wired_to_hot_assistant_answer"); pushReason(reasonCodes, `runtime_bridge_loop_state_${loopState.loop_status}`); pushReason(reasonCodes, "runtime_bridge_route_candidate_built"); pushReason(reasonCodes, `runtime_bridge_route_candidate_${routeCandidate.candidate_status}`); + pushReason(reasonCodes, `runtime_bridge_execution_handoff_${executionHandoff.handoff_status}`); + for (const reasonCode of executionHandoff.reason_codes) { + pushReason(reasonCodes, reasonCode); + } return { schema_version: exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION, policy_owner: "assistantMcpDiscoveryRuntimeBridge", @@ -274,8 +289,9 @@ async function runAssistantMcpDiscoveryRuntimeBridge(input) { answer_draft: answerDraft, loop_state: loopState, route_candidate: routeCandidate, - user_facing_response_allowed: bridgeStatus !== "blocked", - business_fact_answer_allowed: businessFactAnswerAllowed(answerDraft), + execution_handoff: executionHandoff, + user_facing_response_allowed: userFacingResponseAllowed, + business_fact_answer_allowed: businessFactAllowed, requires_user_clarification: bridgeStatus === "needs_clarification", reason_codes: reasonCodes }; diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryDebugAttachment.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryDebugAttachment.ts index b9e19ef..3d7b0a8 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryDebugAttachment.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryDebugAttachment.ts @@ -1,5 +1,6 @@ import type { AssistantMcpDiscoveryRuntimeEntryPointContract } from "./assistantMcpDiscoveryRuntimeEntryPoint"; import type { AssistantMcpRouteCandidateContract } from "./assistantMcpDiscoveryRuntimeBridge"; +import type { AssistantMcpDiscoveryExecutionHandoffContract } from "./assistantMcpDiscoveryExecutionHandoff"; import type { AssistantEvidencePlannerContract } from "./assistantEvidencePlanner"; export interface AssistantMcpDiscoveryDebugAttachmentFields { @@ -31,6 +32,10 @@ export interface AssistantMcpDiscoveryDebugAttachmentFields { mcp_discovery_route_candidate_executable_now: boolean; mcp_discovery_route_candidate_enablement_reason: string | null; mcp_discovery_route_candidate_next_action: string | null; + mcp_discovery_execution_handoff_v1: AssistantMcpDiscoveryExecutionHandoffContract | null; + mcp_discovery_execution_handoff_status: string | null; + mcp_discovery_execution_handoff_allowed_hot_chain: boolean; + mcp_discovery_execution_handoff_can_use_guarded_response: boolean; mcp_discovery_answer_mode: string | null; mcp_discovery_business_fact_answer_allowed: boolean; mcp_discovery_user_facing_response_allowed: boolean; @@ -92,6 +97,14 @@ function isEvidencePlannerContract(value: unknown): value is AssistantEvidencePl return record?.schema_version === "assistant_evidence_planner_v1" && record?.policy_owner === "assistantEvidencePlanner"; } +function isExecutionHandoffContract(value: unknown): value is AssistantMcpDiscoveryExecutionHandoffContract { + const record = toRecordObject(value); + return ( + record?.schema_version === "assistant_mcp_discovery_execution_handoff_v1" && + record?.policy_owner === "assistantMcpDiscoveryExecutionHandoff" + ); +} + function resolveEntryPoint(input: AttachAssistantMcpDiscoveryDebugInput): AssistantMcpDiscoveryRuntimeEntryPointContract | null { if (isMcpDiscoveryEntryPointContract(input.entryPoint)) { return input.entryPoint; @@ -115,6 +128,7 @@ export function buildAssistantMcpDiscoveryDebugAttachmentFields( const evidencePlanAnswerContract = toRecordObject(evidencePlan?.answer_contract); const chainAlignment = toRecordObject(planner?.catalog_chain_template_alignment); const routeCandidate = isRouteCandidateContract(bridge?.route_candidate) ? bridge.route_candidate : null; + const executionHandoff = isExecutionHandoffContract(bridge?.execution_handoff) ? bridge.execution_handoff : null; const answerDraft = toRecordObject(bridge?.answer_draft); return { @@ -146,6 +160,10 @@ export function buildAssistantMcpDiscoveryDebugAttachmentFields( mcp_discovery_route_candidate_executable_now: routeCandidate?.executable_now === true, mcp_discovery_route_candidate_enablement_reason: toNonEmptyString(routeCandidate?.enablement_reason), mcp_discovery_route_candidate_next_action: toNonEmptyString(routeCandidate?.recommended_next_action), + mcp_discovery_execution_handoff_v1: executionHandoff, + mcp_discovery_execution_handoff_status: toNonEmptyString(executionHandoff?.handoff_status), + mcp_discovery_execution_handoff_allowed_hot_chain: executionHandoff?.allowed_hot_chain === true, + mcp_discovery_execution_handoff_can_use_guarded_response: executionHandoff?.can_use_guarded_response === true, mcp_discovery_answer_mode: toNonEmptyString(answerDraft?.answer_mode), mcp_discovery_business_fact_answer_allowed: bridge?.business_fact_answer_allowed === true, mcp_discovery_user_facing_response_allowed: bridge?.user_facing_response_allowed === true, diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryExecutionHandoff.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryExecutionHandoff.ts new file mode 100644 index 0000000..cdf8534 --- /dev/null +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryExecutionHandoff.ts @@ -0,0 +1,134 @@ +import type { AssistantMcpDiscoveryAnswerDraftContract } from "./assistantMcpDiscoveryAnswerAdapter"; +import type { AssistantMcpDiscoveryPilotExecutionContract } from "./assistantMcpDiscoveryPilotExecutor"; +import type { AssistantMcpDiscoveryChainId } from "./assistantMcpDiscoveryPlanner"; +import type { + AssistantMcpDiscoveryRuntimeBridgeStatus, + AssistantMcpRouteCandidateContract +} from "./assistantMcpDiscoveryRuntimeBridge"; + +export const ASSISTANT_MCP_DISCOVERY_EXECUTION_HANDOFF_SCHEMA_VERSION = + "assistant_mcp_discovery_execution_handoff_v1" as const; + +export type AssistantMcpDiscoveryExecutionHandoffStatus = + | "ready_for_guarded_response" + | "awaiting_user_scope" + | "checked_sources_only" + | "not_enabled_for_chain" + | "blocked"; + +export interface AssistantMcpDiscoveryExecutionHandoffInput { + bridgeStatus: AssistantMcpDiscoveryRuntimeBridgeStatus; + routeCandidate: AssistantMcpRouteCandidateContract; + pilot: AssistantMcpDiscoveryPilotExecutionContract; + answerDraft: AssistantMcpDiscoveryAnswerDraftContract; + businessFactAnswerAllowed: boolean; + userFacingResponseAllowed: boolean; +} + +export interface AssistantMcpDiscoveryExecutionHandoffContract { + schema_version: typeof ASSISTANT_MCP_DISCOVERY_EXECUTION_HANDOFF_SCHEMA_VERSION; + policy_owner: "assistantMcpDiscoveryExecutionHandoff"; + handoff_status: AssistantMcpDiscoveryExecutionHandoffStatus; + selected_chain_id: AssistantMcpDiscoveryChainId; + route_candidate_status: AssistantMcpRouteCandidateContract["candidate_status"]; + evidence_answer_mode: string | null; + evidence_expected_coverage: string | null; + pilot_status: AssistantMcpDiscoveryPilotExecutionContract["pilot_status"]; + answer_mode: AssistantMcpDiscoveryAnswerDraftContract["answer_mode"]; + mcp_execution_performed: boolean; + allowed_hot_chain: boolean; + can_use_guarded_response: boolean; + must_keep_internal_mechanics_hidden: true; + reason_codes: string[]; +} + +const HOT_HANDOFF_CHAIN_ALLOWLIST: AssistantMcpDiscoveryChainId[] = ["value_flow"]; + +function uniqueStrings(values: string[]): string[] { + const result: string[] = []; + for (const value of values) { + const text = String(value ?? "").trim(); + if (text && !result.includes(text)) { + result.push(text); + } + } + return result; +} + +function handoffStatusFor( + input: AssistantMcpDiscoveryExecutionHandoffInput, + allowedHotChain: boolean +): AssistantMcpDiscoveryExecutionHandoffStatus { + if (input.bridgeStatus === "needs_clarification" || input.routeCandidate.candidate_status === "needs_user_scope") { + return "awaiting_user_scope"; + } + if (input.bridgeStatus === "checked_sources_only") { + return "checked_sources_only"; + } + if ( + input.bridgeStatus === "blocked" || + input.bridgeStatus === "unsupported" || + input.routeCandidate.candidate_status === "blocked" || + input.routeCandidate.candidate_status === "needs_route_enablement" + ) { + return "blocked"; + } + if (!allowedHotChain) { + return "not_enabled_for_chain"; + } + return "ready_for_guarded_response"; +} + +export function buildAssistantMcpDiscoveryExecutionHandoff( + input: AssistantMcpDiscoveryExecutionHandoffInput +): AssistantMcpDiscoveryExecutionHandoffContract { + const allowedHotChain = HOT_HANDOFF_CHAIN_ALLOWLIST.includes(input.routeCandidate.selected_chain_id); + const baseStatus = handoffStatusFor(input, allowedHotChain); + const readinessChecksPassed = + baseStatus === "ready_for_guarded_response" && + input.bridgeStatus === "answer_draft_ready" && + input.routeCandidate.candidate_status === "ready_for_reviewed_execution" && + input.routeCandidate.executable_now === true && + input.pilot.pilot_status === "executed" && + input.pilot.mcp_execution_performed === true && + input.businessFactAnswerAllowed === true && + input.userFacingResponseAllowed === true && + input.answerDraft.internal_mechanics_allowed === false; + const handoffStatus = readinessChecksPassed ? "ready_for_guarded_response" : baseStatus; + const reasonCodes = uniqueStrings([ + `execution_handoff_status_${handoffStatus}`, + allowedHotChain ? "execution_handoff_chain_allowlisted" : "execution_handoff_chain_not_allowlisted", + input.routeCandidate.executable_now + ? "execution_handoff_route_candidate_executable" + : "execution_handoff_route_candidate_not_executable", + input.pilot.mcp_execution_performed + ? "execution_handoff_mcp_execution_performed" + : "execution_handoff_mcp_execution_not_performed", + input.businessFactAnswerAllowed + ? "execution_handoff_business_fact_allowed" + : "execution_handoff_business_fact_not_allowed", + input.userFacingResponseAllowed + ? "execution_handoff_user_facing_allowed" + : "execution_handoff_user_facing_not_allowed", + readinessChecksPassed + ? "execution_handoff_guarded_response_ready" + : "execution_handoff_guarded_response_not_ready" + ]); + + return { + schema_version: ASSISTANT_MCP_DISCOVERY_EXECUTION_HANDOFF_SCHEMA_VERSION, + policy_owner: "assistantMcpDiscoveryExecutionHandoff", + handoff_status: handoffStatus, + selected_chain_id: input.routeCandidate.selected_chain_id, + route_candidate_status: input.routeCandidate.candidate_status, + evidence_answer_mode: input.routeCandidate.evidence_answer_mode, + evidence_expected_coverage: input.routeCandidate.evidence_expected_coverage, + pilot_status: input.pilot.pilot_status, + answer_mode: input.answerDraft.answer_mode, + mcp_execution_performed: input.pilot.mcp_execution_performed, + allowed_hot_chain: allowedHotChain, + can_use_guarded_response: readinessChecksPassed, + must_keep_internal_mechanics_hidden: true, + reason_codes: reasonCodes + }; +} diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryRuntimeBridge.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryRuntimeBridge.ts index 9761c11..01eefbe 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryRuntimeBridge.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryRuntimeBridge.ts @@ -7,6 +7,10 @@ import { type AssistantMcpDiscoveryPilotExecutionContract, type AssistantMcpDiscoveryPilotExecutorDeps } from "./assistantMcpDiscoveryPilotExecutor"; +import { + buildAssistantMcpDiscoveryExecutionHandoff, + type AssistantMcpDiscoveryExecutionHandoffContract +} from "./assistantMcpDiscoveryExecutionHandoff"; import { planAssistantMcpDiscovery, type AssistantMcpDiscoveryChainId, @@ -101,6 +105,7 @@ export interface AssistantMcpDiscoveryRuntimeBridgeContract { answer_draft: AssistantMcpDiscoveryAnswerDraftContract; loop_state: AssistantMcpDiscoveryLoopStateContract; route_candidate: AssistantMcpRouteCandidateContract; + execution_handoff: AssistantMcpDiscoveryExecutionHandoffContract; user_facing_response_allowed: boolean; business_fact_answer_allowed: boolean; requires_user_clarification: boolean; @@ -414,6 +419,16 @@ export async function runAssistantMcpDiscoveryRuntimeBridge( const bridgeStatus = bridgeStatusFor(pilot, answerDraft); const loopState = buildLoopState(planner, pilot, bridgeStatus); const routeCandidate = buildRouteCandidate(planner, pilot, bridgeStatus); + const userFacingResponseAllowed = bridgeStatus !== "blocked"; + const businessFactAllowed = businessFactAnswerAllowed(answerDraft); + const executionHandoff = buildAssistantMcpDiscoveryExecutionHandoff({ + bridgeStatus, + routeCandidate, + pilot, + answerDraft, + businessFactAnswerAllowed: businessFactAllowed, + userFacingResponseAllowed + }); const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]); pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`); @@ -421,6 +436,10 @@ export async function runAssistantMcpDiscoveryRuntimeBridge( pushReason(reasonCodes, `runtime_bridge_loop_state_${loopState.loop_status}`); pushReason(reasonCodes, "runtime_bridge_route_candidate_built"); pushReason(reasonCodes, `runtime_bridge_route_candidate_${routeCandidate.candidate_status}`); + pushReason(reasonCodes, `runtime_bridge_execution_handoff_${executionHandoff.handoff_status}`); + for (const reasonCode of executionHandoff.reason_codes) { + pushReason(reasonCodes, reasonCode); + } return { schema_version: ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION, @@ -432,8 +451,9 @@ export async function runAssistantMcpDiscoveryRuntimeBridge( answer_draft: answerDraft, loop_state: loopState, route_candidate: routeCandidate, - user_facing_response_allowed: bridgeStatus !== "blocked", - business_fact_answer_allowed: businessFactAnswerAllowed(answerDraft), + execution_handoff: executionHandoff, + user_facing_response_allowed: userFacingResponseAllowed, + business_fact_answer_allowed: businessFactAllowed, requires_user_clarification: bridgeStatus === "needs_clarification", reason_codes: reasonCodes }; diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryDebugAttachment.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryDebugAttachment.test.ts index 48cff29..781c8b9 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryDebugAttachment.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryDebugAttachment.test.ts @@ -15,12 +15,12 @@ function entryPointContract(overrides: Record = {}) { business_fact_answer_allowed: true, requires_user_clarification: false, planner: { - selected_chain_id: "value_flow_ranking", + selected_chain_id: "value_flow", evidence_plan: { schema_version: "assistant_evidence_planner_v1", policy_owner: "assistantEvidencePlanner", planner_status: "ready_for_execution", - selected_chain_id: "value_flow_ranking", + selected_chain_id: "value_flow", evidence_axes: { missing_axes: [] }, @@ -31,10 +31,10 @@ function entryPointContract(overrides: Record = {}) { answer_mode: "confirmed_business_answer" } }, - catalog_chain_template_matches: ["value_flow_ranking", "value_flow"], + catalog_chain_template_matches: ["value_flow"], catalog_chain_template_alignment: { alignment_status: "selected_matches_top", - top_chain_template_match: "value_flow_ranking", + top_chain_template_match: "value_flow", selected_chain_template_rank: 1, selected_chain_is_catalog_template: true, selected_chain_in_catalog_matches: true, @@ -45,9 +45,9 @@ function entryPointContract(overrides: Record = {}) { schema_version: "assistant_mcp_route_candidate_v1", policy_owner: "assistantMcpDiscoveryRuntimeBridge", candidate_status: "ready_for_reviewed_execution", - selected_chain_id: "value_flow_ranking", - selected_chain_summary: "Rank value flow", - nearest_catalog_chain_template: "value_flow_ranking", + selected_chain_id: "value_flow", + selected_chain_summary: "Measure value flow", + nearest_catalog_chain_template: "value_flow", catalog_alignment_status: "selected_matches_top", business_fact_family: "value_flow", action_family: "turnover", @@ -63,6 +63,22 @@ function entryPointContract(overrides: Record = {}) { recommended_next_action: "Execute through the reviewed runtime bridge and truth gate.", forbidden_overclaim_flags: ["no_unchecked_fact_totals"] }, + 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" } @@ -86,14 +102,14 @@ describe("assistant MCP discovery debug attachment", () => { expect(debug.mcp_discovery_attempted).toBe(true); expect(debug.mcp_discovery_hot_runtime_wired).toBe(false); expect(debug.mcp_discovery_bridge_status).toBe("answer_draft_ready"); - expect(debug.mcp_discovery_selected_chain_id).toBe("value_flow_ranking"); + expect(debug.mcp_discovery_selected_chain_id).toBe("value_flow"); expect(debug.mcp_discovery_evidence_plan_status).toBe("ready_for_execution"); expect(debug.mcp_discovery_evidence_plan_answer_mode).toBe("confirmed_business_answer"); expect(debug.mcp_discovery_evidence_plan_expected_coverage).toBe("confirmed_coverage"); expect(debug.mcp_discovery_evidence_plan_missing_axes).toEqual([]); - expect(debug.mcp_discovery_catalog_chain_template_matches).toEqual(["value_flow_ranking", "value_flow"]); + expect(debug.mcp_discovery_catalog_chain_template_matches).toEqual(["value_flow"]); expect(debug.mcp_discovery_catalog_chain_alignment_status).toBe("selected_matches_top"); - expect(debug.mcp_discovery_catalog_chain_top_match).toBe("value_flow_ranking"); + expect(debug.mcp_discovery_catalog_chain_top_match).toBe("value_flow"); expect(debug.mcp_discovery_catalog_chain_selected_matches_top).toBe(true); expect(debug.mcp_discovery_route_candidate_status).toBe("ready_for_reviewed_execution"); expect(debug.mcp_discovery_route_candidate_fact_family).toBe("value_flow"); @@ -107,6 +123,9 @@ describe("assistant MCP discovery debug attachment", () => { expect(debug.mcp_discovery_route_candidate_next_action).toBe( "Execute through the reviewed runtime bridge and truth gate." ); + expect(debug.mcp_discovery_execution_handoff_status).toBe("ready_for_guarded_response"); + expect(debug.mcp_discovery_execution_handoff_allowed_hot_chain).toBe(true); + expect(debug.mcp_discovery_execution_handoff_can_use_guarded_response).toBe(true); expect(debug.mcp_discovery_answer_mode).toBe("confirmed_with_bounded_inference"); expect(debug.mcp_discovery_business_fact_answer_allowed).toBe(true); expect(debug.mcp_discovery_user_facing_response_allowed).toBe(true); @@ -142,6 +161,10 @@ describe("assistant MCP discovery debug attachment", () => { expect(debug.mcp_discovery_route_candidate_missing_axes).toEqual([]); expect(debug.mcp_discovery_route_candidate_provided_axes).toEqual([]); expect(debug.mcp_discovery_route_candidate_executable_now).toBe(false); + expect(debug.mcp_discovery_execution_handoff_v1).toBeNull(); + expect(debug.mcp_discovery_execution_handoff_status).toBeNull(); + expect(debug.mcp_discovery_execution_handoff_allowed_hot_chain).toBe(false); + expect(debug.mcp_discovery_execution_handoff_can_use_guarded_response).toBe(false); expect(debug.mcp_discovery_answer_mode).toBeNull(); expect(debug.mcp_discovery_business_fact_answer_allowed).toBe(false); expect(debug.mcp_discovery_user_facing_response_allowed).toBe(false); diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeBridge.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeBridge.test.ts index d495595..aa246b6 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeBridge.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeBridge.test.ts @@ -71,6 +71,11 @@ describe("assistant MCP discovery runtime bridge", () => { expect(result.answer_draft.answer_mode).toBe("confirmed_with_bounded_inference"); expect(result.business_fact_answer_allowed).toBe(true); expect(result.user_facing_response_allowed).toBe(true); + expect(result.execution_handoff).toMatchObject({ + handoff_status: "not_enabled_for_chain", + allowed_hot_chain: false, + can_use_guarded_response: false + }); expect(result.reason_codes).toContain("runtime_bridge_not_wired_to_hot_assistant_answer"); }); @@ -88,6 +93,10 @@ describe("assistant MCP discovery runtime bridge", () => { expect(result.requires_user_clarification).toBe(true); expect(result.pilot.mcp_execution_performed).toBe(false); expect(result.business_fact_answer_allowed).toBe(false); + expect(result.execution_handoff).toMatchObject({ + handoff_status: "awaiting_user_scope", + can_use_guarded_response: false + }); expect(result.answer_draft.next_step_line).toContain("Уточните контрагента"); }); @@ -242,6 +251,11 @@ describe("assistant MCP discovery runtime bridge", () => { executable_now: true, enablement_reason: null }); + expect(result.execution_handoff).toMatchObject({ + handoff_status: "not_enabled_for_chain", + allowed_hot_chain: false, + can_use_guarded_response: false + }); expect(result.answer_draft.confirmed_lines.join("\n")).toContain("СВК-А"); }); @@ -1065,6 +1079,19 @@ describe("assistant MCP discovery runtime bridge", () => { period_scope: "2020", total_amount: 5000 }); + expect(result.execution_handoff).toMatchObject({ + schema_version: "assistant_mcp_discovery_execution_handoff_v1", + handoff_status: "ready_for_guarded_response", + selected_chain_id: "value_flow", + route_candidate_status: "ready_for_reviewed_execution", + pilot_status: "executed", + mcp_execution_performed: true, + allowed_hot_chain: true, + can_use_guarded_response: true, + must_keep_internal_mechanics_hidden: true + }); + expect(result.reason_codes).toContain("runtime_bridge_execution_handoff_ready_for_guarded_response"); + expect(result.reason_codes).toContain("execution_handoff_guarded_response_ready"); expect(result.answer_draft.confirmed_lines.join("\n")).toContain("5 000"); expect(result.answer_draft.confirmed_lines.join("\n")).not.toContain("контрагенту"); });