diff --git a/docs/ARCH/11 - architecture_turnaround/14 - semantic_dialog_authority_recovery_plan_2026-04-19.md b/docs/ARCH/11 - architecture_turnaround/14 - semantic_dialog_authority_recovery_plan_2026-04-19.md index a9e5c60..fb451b8 100644 --- a/docs/ARCH/11 - architecture_turnaround/14 - semantic_dialog_authority_recovery_plan_2026-04-19.md +++ b/docs/ARCH/11 - architecture_turnaround/14 - semantic_dialog_authority_recovery_plan_2026-04-19.md @@ -921,6 +921,30 @@ Validation: - `npm test -- assistantMcpDiscoveryDebugAttachment.test.ts assistantDebugPayloadAssembler.test.ts assistantAddressLaneResponseRuntimeAdapter.test.ts assistantMcpDiscoveryRuntimeEntryPoint.test.ts` passed 13/13; - `npm run build` passed. +## Progress Update - 2026-04-20 MCP Discovery Runtime Meta Hook + +The eleventh implementation slice of Big Block 5 wires the discovery entry point into real orchestration runtime meta: + +- `assistantAddressOrchestrationRuntimeAdapter.ts` +- `assistantAddressOrchestrationRuntimeAdapter.test.ts` + +This is the first live hook from the address orchestration runtime into MCP discovery. + +It still does not change the user-facing answer. + +Runtime behavior: + +- builds MCP discovery input from raw user message, effective address message, `orchestrationContract.assistant_turn_meaning`, and the predecompose contract; +- stores the result under `addressRuntimeMeta.mcpDiscoveryRuntimeEntryPoint`; +- keeps `hot_runtime_wired=false`; +- keeps route/living-mode decisions unchanged; +- records `mcpDiscoveryRuntimeEntryPointError` instead of breaking address orchestration if discovery fails. + +Validation: + +- `npm test -- assistantMcpDiscoveryRuntimeEntryPoint.test.ts assistantMcpDiscoveryTurnInputAdapter.test.ts assistantMcpDiscoveryPolicy.test.ts assistantMcpCatalogIndex.test.ts assistantMcpDiscoveryPlanner.test.ts assistantMcpDiscoveryRuntimeAdapter.test.ts assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts assistantMcpDiscoveryRuntimeBridge.test.ts assistantMcpDiscoveryDebugAttachment.test.ts assistantAddressOrchestrationRuntimeAdapter.test.ts assistantAddressLaneResponseRuntimeAdapter.test.ts assistantDebugPayloadAssembler.test.ts` passed 61/61; +- `npm run build` passed. + ## Execution Rule Do not implement this plan as: diff --git a/llm_normalizer/backend/dist/services/assistantAddressOrchestrationRuntimeAdapter.js b/llm_normalizer/backend/dist/services/assistantAddressOrchestrationRuntimeAdapter.js index 6d000bc..3d4c0d0 100644 --- a/llm_normalizer/backend/dist/services/assistantAddressOrchestrationRuntimeAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantAddressOrchestrationRuntimeAdapter.js @@ -2,7 +2,14 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.buildAssistantAddressOrchestrationRuntime = buildAssistantAddressOrchestrationRuntime; const assistantRoutePolicyRuntimeAdapter_1 = require("./assistantRoutePolicyRuntimeAdapter"); +const assistantMcpDiscoveryRuntimeEntryPoint_1 = require("./assistantMcpDiscoveryRuntimeEntryPoint"); const inventoryLifecycleCueHelpers_1 = require("./inventoryLifecycleCueHelpers"); +function toRecordObject(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return null; + } + return value; +} function hasSelectedObjectInventorySignal(text) { return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ним|selected\s+object)/iu.test(String(text ?? "")); } @@ -141,14 +148,32 @@ async function buildAssistantAddressOrchestrationRuntime(input) { resolveAssistantOrchestrationDecision: input.resolveAssistantOrchestrationDecision }); const orchestrationDecision = routePolicyRuntime.orchestrationDecision; + const orchestrationContract = toRecordObject(orchestrationDecision.orchestrationContract); + const predecomposeContract = toRecordObject(addressPreDecompose.predecomposeContract); const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(input.userMessage, addressInputMessage, carryover, addressPreDecompose); + const runDiscoveryEntryPoint = input.runMcpDiscoveryRuntimeEntryPoint ?? assistantMcpDiscoveryRuntimeEntryPoint_1.runAssistantMcpDiscoveryRuntimeEntryPoint; + let mcpDiscoveryRuntimeEntryPoint = null; + let mcpDiscoveryRuntimeEntryPointError = null; + try { + mcpDiscoveryRuntimeEntryPoint = (await runDiscoveryEntryPoint({ + userMessage: input.userMessage, + effectiveMessage: addressInputMessage, + assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning), + predecomposeContract + })); + } + catch (error) { + mcpDiscoveryRuntimeEntryPointError = String(error instanceof Error ? error.message : error ?? "unknown_error").slice(0, 280); + } const addressRuntimeMeta = { ...addressPreDecompose, toolGateDecision: orchestrationDecision.toolGateDecision ?? null, toolGateReason: orchestrationDecision.toolGateReason ?? null, dialogContinuationContract, - orchestrationContract: orchestrationDecision.orchestrationContract ?? null, - routePolicyContract: routePolicyRuntime.routePolicyContract + orchestrationContract: orchestrationContract ?? null, + routePolicyContract: routePolicyRuntime.routePolicyContract, + mcpDiscoveryRuntimeEntryPoint, + mcpDiscoveryRuntimeEntryPointError }; return { addressPreDecompose, diff --git a/llm_normalizer/backend/src/services/assistantAddressOrchestrationRuntimeAdapter.ts b/llm_normalizer/backend/src/services/assistantAddressOrchestrationRuntimeAdapter.ts index e58244f..444c304 100644 --- a/llm_normalizer/backend/src/services/assistantAddressOrchestrationRuntimeAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantAddressOrchestrationRuntimeAdapter.ts @@ -1,4 +1,8 @@ import { runAssistantRoutePolicyRuntime } from "./assistantRoutePolicyRuntimeAdapter"; +import { + runAssistantMcpDiscoveryRuntimeEntryPoint, + type RunAssistantMcpDiscoveryRuntimeEntryPointInput +} from "./assistantMcpDiscoveryRuntimeEntryPoint"; import { hasInventoryProfitabilityCue } from "./inventoryLifecycleCueHelpers"; @@ -43,6 +47,9 @@ export interface BuildAssistantAddressOrchestrationRuntimeInput { carryover: AssistantAddressCarryoverLike | null, addressPreDecompose: Record ) => unknown; + runMcpDiscoveryRuntimeEntryPoint?: ( + input: RunAssistantMcpDiscoveryRuntimeEntryPointInput + ) => Promise>; } export interface AssistantAddressCarryoverLike { @@ -62,6 +69,13 @@ export interface BuildAssistantAddressOrchestrationRuntimeOutput { }; } +function toRecordObject(value: unknown): Record | null { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return null; + } + return value as Record; +} + function hasSelectedObjectInventorySignal(text: string | null): boolean { return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ним|selected\s+object)/iu.test( String(text ?? "") @@ -283,19 +297,36 @@ export async function buildAssistantAddressOrchestrationRuntime( resolveAssistantOrchestrationDecision: input.resolveAssistantOrchestrationDecision }); const orchestrationDecision = routePolicyRuntime.orchestrationDecision; + const orchestrationContract = toRecordObject(orchestrationDecision.orchestrationContract); + const predecomposeContract = toRecordObject(addressPreDecompose.predecomposeContract); const dialogContinuationContract = input.buildAddressDialogContinuationContractV2( input.userMessage, addressInputMessage, carryover, addressPreDecompose ); + const runDiscoveryEntryPoint = input.runMcpDiscoveryRuntimeEntryPoint ?? runAssistantMcpDiscoveryRuntimeEntryPoint; + let mcpDiscoveryRuntimeEntryPoint: Record | null = null; + let mcpDiscoveryRuntimeEntryPointError: string | null = null; + try { + mcpDiscoveryRuntimeEntryPoint = (await runDiscoveryEntryPoint({ + userMessage: input.userMessage, + effectiveMessage: addressInputMessage, + assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning), + predecomposeContract + })) as Record; + } catch (error) { + mcpDiscoveryRuntimeEntryPointError = String(error instanceof Error ? error.message : error ?? "unknown_error").slice(0, 280); + } const addressRuntimeMeta = { ...addressPreDecompose, toolGateDecision: orchestrationDecision.toolGateDecision ?? null, toolGateReason: orchestrationDecision.toolGateReason ?? null, dialogContinuationContract, - orchestrationContract: orchestrationDecision.orchestrationContract ?? null, - routePolicyContract: routePolicyRuntime.routePolicyContract + orchestrationContract: orchestrationContract ?? null, + routePolicyContract: routePolicyRuntime.routePolicyContract, + mcpDiscoveryRuntimeEntryPoint, + mcpDiscoveryRuntimeEntryPointError }; return { diff --git a/llm_normalizer/backend/tests/assistantAddressOrchestrationRuntimeAdapter.test.ts b/llm_normalizer/backend/tests/assistantAddressOrchestrationRuntimeAdapter.test.ts index 160d0ea..a99f530 100644 --- a/llm_normalizer/backend/tests/assistantAddressOrchestrationRuntimeAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantAddressOrchestrationRuntimeAdapter.test.ts @@ -76,11 +76,98 @@ describe("assistant address orchestration runtime adapter", () => { expect(output.addressRuntimeMeta.dialogContinuationContract).toEqual({ schema_version: "address_dialog_continuation_contract_v2" }); + expect(output.addressRuntimeMeta.mcpDiscoveryRuntimeEntryPoint).toEqual( + expect.objectContaining({ + schema_version: "assistant_mcp_discovery_runtime_entry_point_v1", + entry_status: "skipped_not_applicable", + hot_runtime_wired: false + }) + ); expect(input.__spies.runAddressLlmPreDecompose).toHaveBeenCalledTimes(1); expect(input.__spies.resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(1); expect(input.__spies.resolveAssistantOrchestrationDecision).toHaveBeenCalledTimes(1); }); + it("runs MCP discovery entry point from real orchestration context without changing the route decision", async () => { + const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({ + schema_version: "assistant_mcp_discovery_runtime_entry_point_v1", + policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint", + entry_status: "bridge_executed", + hot_runtime_wired: false, + discovery_attempted: true + })); + const input = buildInput({ + userMessage: "Сколько лет мы работаем с Группа СВК?", + runAddressLlmPreDecompose: vi.fn(async () => ({ + attempted: true, + applied: false, + effectiveMessage: "Сколько лет мы работаем с Группа СВК?", + reason: "raw_kept", + predecomposeContract: { + entities: { counterparty: "Группа СВК" }, + period: {} + } + })), + resolveAssistantOrchestrationDecision: vi.fn(() => ({ + runAddressLane: false, + livingMode: "chat", + livingReason: "unsupported_current_turn_meaning_boundary", + toolGateDecision: "skip_address_lane", + toolGateReason: "unsupported_current_turn_meaning_boundary", + orchestrationContract: { + schema_version: "assistant_orchestration_contract_v1", + assistant_turn_meaning: { + schema_version: "assistant_turn_meaning_v1", + asked_domain_family: "counterparty", + asked_action_family: "counterparty_lifecycle", + unsupported_but_understood_family: "counterparty_lifecycle" + } + } + })), + runMcpDiscoveryRuntimeEntryPoint + }); + + const output = await buildAssistantAddressOrchestrationRuntime(input); + + expect(output.livingModeDecision).toEqual({ + mode: "chat", + reason: "unsupported_current_turn_meaning_boundary" + }); + expect(output.addressRuntimeMeta.mcpDiscoveryRuntimeEntryPoint).toEqual( + expect.objectContaining({ + entry_status: "bridge_executed", + discovery_attempted: true, + hot_runtime_wired: false + }) + ); + expect(runMcpDiscoveryRuntimeEntryPoint).toHaveBeenCalledWith( + expect.objectContaining({ + userMessage: "Сколько лет мы работаем с Группа СВК?", + effectiveMessage: "Сколько лет мы работаем с Группа СВК?", + assistantTurnMeaning: expect.objectContaining({ + unsupported_but_understood_family: "counterparty_lifecycle" + }), + predecomposeContract: expect.objectContaining({ + entities: { counterparty: "Группа СВК" } + }) + }) + ); + }); + + it("keeps address orchestration alive when MCP discovery entry point fails", async () => { + const input = buildInput({ + runMcpDiscoveryRuntimeEntryPoint: vi.fn(async () => { + throw new Error("discovery transport failed"); + }) + }); + + const output = await buildAssistantAddressOrchestrationRuntime(input); + + expect(output.orchestrationDecision.runAddressLane).toBe(true); + expect(output.addressRuntimeMeta.mcpDiscoveryRuntimeEntryPoint).toBeNull(); + expect(output.addressRuntimeMeta.mcpDiscoveryRuntimeEntryPointError).toBe("discovery transport failed"); + }); + it("builds deterministic fallback predecompose payload when feature is disabled", async () => { const input = buildInput({ featureAddressLlmPredecomposeV1: false,