ARCH: подключить MCP discovery к runtime meta

This commit is contained in:
dctouch 2026-04-20 11:54:04 +03:00
parent de4e064885
commit f0d7e81ec0
4 changed files with 171 additions and 4 deletions

View File

@ -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:

View File

@ -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,

View File

@ -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<string, unknown>
) => unknown;
runMcpDiscoveryRuntimeEntryPoint?: (
input: RunAssistantMcpDiscoveryRuntimeEntryPointInput
) => Promise<Record<string, unknown>>;
}
export interface AssistantAddressCarryoverLike {
@ -62,6 +69,13 @@ export interface BuildAssistantAddressOrchestrationRuntimeOutput {
};
}
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 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<string, unknown> | 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<string, unknown>;
} 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 {

View File

@ -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,