ARCH: подключить MCP discovery к runtime meta
This commit is contained in:
parent
de4e064885
commit
f0d7e81ec0
|
|
@ -921,6 +921,30 @@ Validation:
|
||||||
- `npm test -- assistantMcpDiscoveryDebugAttachment.test.ts assistantDebugPayloadAssembler.test.ts assistantAddressLaneResponseRuntimeAdapter.test.ts assistantMcpDiscoveryRuntimeEntryPoint.test.ts` passed 13/13;
|
- `npm test -- assistantMcpDiscoveryDebugAttachment.test.ts assistantDebugPayloadAssembler.test.ts assistantAddressLaneResponseRuntimeAdapter.test.ts assistantMcpDiscoveryRuntimeEntryPoint.test.ts` passed 13/13;
|
||||||
- `npm run build` passed.
|
- `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
|
## Execution Rule
|
||||||
|
|
||||||
Do not implement this plan as:
|
Do not implement this plan as:
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,14 @@
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.buildAssistantAddressOrchestrationRuntime = buildAssistantAddressOrchestrationRuntime;
|
exports.buildAssistantAddressOrchestrationRuntime = buildAssistantAddressOrchestrationRuntime;
|
||||||
const assistantRoutePolicyRuntimeAdapter_1 = require("./assistantRoutePolicyRuntimeAdapter");
|
const assistantRoutePolicyRuntimeAdapter_1 = require("./assistantRoutePolicyRuntimeAdapter");
|
||||||
|
const assistantMcpDiscoveryRuntimeEntryPoint_1 = require("./assistantMcpDiscoveryRuntimeEntryPoint");
|
||||||
const inventoryLifecycleCueHelpers_1 = require("./inventoryLifecycleCueHelpers");
|
const inventoryLifecycleCueHelpers_1 = require("./inventoryLifecycleCueHelpers");
|
||||||
|
function toRecordObject(value) {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
function hasSelectedObjectInventorySignal(text) {
|
function hasSelectedObjectInventorySignal(text) {
|
||||||
return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ним|selected\s+object)/iu.test(String(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
|
resolveAssistantOrchestrationDecision: input.resolveAssistantOrchestrationDecision
|
||||||
});
|
});
|
||||||
const orchestrationDecision = routePolicyRuntime.orchestrationDecision;
|
const orchestrationDecision = routePolicyRuntime.orchestrationDecision;
|
||||||
|
const orchestrationContract = toRecordObject(orchestrationDecision.orchestrationContract);
|
||||||
|
const predecomposeContract = toRecordObject(addressPreDecompose.predecomposeContract);
|
||||||
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(input.userMessage, addressInputMessage, carryover, addressPreDecompose);
|
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 = {
|
const addressRuntimeMeta = {
|
||||||
...addressPreDecompose,
|
...addressPreDecompose,
|
||||||
toolGateDecision: orchestrationDecision.toolGateDecision ?? null,
|
toolGateDecision: orchestrationDecision.toolGateDecision ?? null,
|
||||||
toolGateReason: orchestrationDecision.toolGateReason ?? null,
|
toolGateReason: orchestrationDecision.toolGateReason ?? null,
|
||||||
dialogContinuationContract,
|
dialogContinuationContract,
|
||||||
orchestrationContract: orchestrationDecision.orchestrationContract ?? null,
|
orchestrationContract: orchestrationContract ?? null,
|
||||||
routePolicyContract: routePolicyRuntime.routePolicyContract
|
routePolicyContract: routePolicyRuntime.routePolicyContract,
|
||||||
|
mcpDiscoveryRuntimeEntryPoint,
|
||||||
|
mcpDiscoveryRuntimeEntryPointError
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
addressPreDecompose,
|
addressPreDecompose,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
import { runAssistantRoutePolicyRuntime } from "./assistantRoutePolicyRuntimeAdapter";
|
import { runAssistantRoutePolicyRuntime } from "./assistantRoutePolicyRuntimeAdapter";
|
||||||
|
import {
|
||||||
|
runAssistantMcpDiscoveryRuntimeEntryPoint,
|
||||||
|
type RunAssistantMcpDiscoveryRuntimeEntryPointInput
|
||||||
|
} from "./assistantMcpDiscoveryRuntimeEntryPoint";
|
||||||
|
|
||||||
import { hasInventoryProfitabilityCue } from "./inventoryLifecycleCueHelpers";
|
import { hasInventoryProfitabilityCue } from "./inventoryLifecycleCueHelpers";
|
||||||
|
|
||||||
|
|
@ -43,6 +47,9 @@ export interface BuildAssistantAddressOrchestrationRuntimeInput {
|
||||||
carryover: AssistantAddressCarryoverLike | null,
|
carryover: AssistantAddressCarryoverLike | null,
|
||||||
addressPreDecompose: Record<string, unknown>
|
addressPreDecompose: Record<string, unknown>
|
||||||
) => unknown;
|
) => unknown;
|
||||||
|
runMcpDiscoveryRuntimeEntryPoint?: (
|
||||||
|
input: RunAssistantMcpDiscoveryRuntimeEntryPointInput
|
||||||
|
) => Promise<Record<string, unknown>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AssistantAddressCarryoverLike {
|
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 {
|
function hasSelectedObjectInventorySignal(text: string | null): boolean {
|
||||||
return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ним|selected\s+object)/iu.test(
|
return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ним|selected\s+object)/iu.test(
|
||||||
String(text ?? "")
|
String(text ?? "")
|
||||||
|
|
@ -283,19 +297,36 @@ export async function buildAssistantAddressOrchestrationRuntime(
|
||||||
resolveAssistantOrchestrationDecision: input.resolveAssistantOrchestrationDecision
|
resolveAssistantOrchestrationDecision: input.resolveAssistantOrchestrationDecision
|
||||||
});
|
});
|
||||||
const orchestrationDecision = routePolicyRuntime.orchestrationDecision;
|
const orchestrationDecision = routePolicyRuntime.orchestrationDecision;
|
||||||
|
const orchestrationContract = toRecordObject(orchestrationDecision.orchestrationContract);
|
||||||
|
const predecomposeContract = toRecordObject(addressPreDecompose.predecomposeContract);
|
||||||
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(
|
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(
|
||||||
input.userMessage,
|
input.userMessage,
|
||||||
addressInputMessage,
|
addressInputMessage,
|
||||||
carryover,
|
carryover,
|
||||||
addressPreDecompose
|
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 = {
|
const addressRuntimeMeta = {
|
||||||
...addressPreDecompose,
|
...addressPreDecompose,
|
||||||
toolGateDecision: orchestrationDecision.toolGateDecision ?? null,
|
toolGateDecision: orchestrationDecision.toolGateDecision ?? null,
|
||||||
toolGateReason: orchestrationDecision.toolGateReason ?? null,
|
toolGateReason: orchestrationDecision.toolGateReason ?? null,
|
||||||
dialogContinuationContract,
|
dialogContinuationContract,
|
||||||
orchestrationContract: orchestrationDecision.orchestrationContract ?? null,
|
orchestrationContract: orchestrationContract ?? null,
|
||||||
routePolicyContract: routePolicyRuntime.routePolicyContract
|
routePolicyContract: routePolicyRuntime.routePolicyContract,
|
||||||
|
mcpDiscoveryRuntimeEntryPoint,
|
||||||
|
mcpDiscoveryRuntimeEntryPointError
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -76,11 +76,98 @@ describe("assistant address orchestration runtime adapter", () => {
|
||||||
expect(output.addressRuntimeMeta.dialogContinuationContract).toEqual({
|
expect(output.addressRuntimeMeta.dialogContinuationContract).toEqual({
|
||||||
schema_version: "address_dialog_continuation_contract_v2"
|
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.runAddressLlmPreDecompose).toHaveBeenCalledTimes(1);
|
||||||
expect(input.__spies.resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(1);
|
expect(input.__spies.resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(1);
|
||||||
expect(input.__spies.resolveAssistantOrchestrationDecision).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 () => {
|
it("builds deterministic fallback predecompose payload when feature is disabled", async () => {
|
||||||
const input = buildInput({
|
const input = buildInput({
|
||||||
featureAddressLlmPredecomposeV1: false,
|
featureAddressLlmPredecomposeV1: false,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue