ARCH: добавить runtime bridge MCP discovery
This commit is contained in:
parent
b4865f36b1
commit
9d6e7066e0
|
|
@ -818,6 +818,32 @@ Validation:
|
||||||
- `npm test -- assistantMcpDiscoveryPolicy.test.ts assistantMcpCatalogIndex.test.ts assistantMcpDiscoveryPlanner.test.ts assistantMcpDiscoveryRuntimeAdapter.test.ts assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts` passed 29/29;
|
- `npm test -- assistantMcpDiscoveryPolicy.test.ts assistantMcpCatalogIndex.test.ts assistantMcpDiscoveryPlanner.test.ts assistantMcpDiscoveryRuntimeAdapter.test.ts assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts` passed 29/29;
|
||||||
- `npm run build` passed.
|
- `npm run build` passed.
|
||||||
|
|
||||||
|
## Progress Update - 2026-04-20 MCP Discovery Runtime Bridge
|
||||||
|
|
||||||
|
The seventh implementation slice of Big Block 5 added a runtime bridge contract:
|
||||||
|
|
||||||
|
- `assistantMcpDiscoveryRuntimeBridge.ts`
|
||||||
|
- `assistantMcpDiscoveryRuntimeBridge.test.ts`
|
||||||
|
|
||||||
|
This bridge is still not wired into the hot assistant answer path.
|
||||||
|
|
||||||
|
It composes the already separated discovery layers into one orchestration-facing contract:
|
||||||
|
|
||||||
|
- planner;
|
||||||
|
- isolated pilot executor;
|
||||||
|
- human-safe answer draft;
|
||||||
|
- bridge status;
|
||||||
|
- explicit `hot_runtime_wired=false`;
|
||||||
|
- business-fact answer authorization flags;
|
||||||
|
- clarification and unsupported-state flags.
|
||||||
|
|
||||||
|
This gives future runtime integration a single object to attach to debug/evidence flow without letting discovery silently become a user-facing answer path.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- `npm test -- assistantMcpDiscoveryPolicy.test.ts assistantMcpCatalogIndex.test.ts assistantMcpDiscoveryPlanner.test.ts assistantMcpDiscoveryRuntimeAdapter.test.ts assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts assistantMcpDiscoveryRuntimeBridge.test.ts` passed 33/33;
|
||||||
|
- `npm run build` passed.
|
||||||
|
|
||||||
## Execution Rule
|
## Execution Rule
|
||||||
|
|
||||||
Do not implement this plan as:
|
Do not implement this plan as:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION = void 0;
|
||||||
|
exports.runAssistantMcpDiscoveryRuntimeBridge = runAssistantMcpDiscoveryRuntimeBridge;
|
||||||
|
const assistantMcpDiscoveryAnswerAdapter_1 = require("./assistantMcpDiscoveryAnswerAdapter");
|
||||||
|
const assistantMcpDiscoveryPilotExecutor_1 = require("./assistantMcpDiscoveryPilotExecutor");
|
||||||
|
const assistantMcpDiscoveryPlanner_1 = require("./assistantMcpDiscoveryPlanner");
|
||||||
|
exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION = "assistant_mcp_discovery_runtime_bridge_v1";
|
||||||
|
function normalizeReasonCode(value) {
|
||||||
|
const normalized = value
|
||||||
|
.trim()
|
||||||
|
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
|
||||||
|
.replace(/^_+|_+$/g, "")
|
||||||
|
.toLowerCase();
|
||||||
|
return normalized.length > 0 ? normalized.slice(0, 120) : null;
|
||||||
|
}
|
||||||
|
function pushReason(target, value) {
|
||||||
|
const normalized = normalizeReasonCode(value);
|
||||||
|
if (normalized && !target.includes(normalized)) {
|
||||||
|
target.push(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 bridgeStatusFor(pilot, draft) {
|
||||||
|
if (draft.answer_mode === "blocked" || pilot.pilot_status === "blocked") {
|
||||||
|
return "blocked";
|
||||||
|
}
|
||||||
|
if (draft.answer_mode === "needs_clarification" || pilot.pilot_status === "skipped_needs_clarification") {
|
||||||
|
return "needs_clarification";
|
||||||
|
}
|
||||||
|
if (pilot.pilot_status === "unsupported") {
|
||||||
|
return "unsupported";
|
||||||
|
}
|
||||||
|
if (draft.answer_mode === "checked_sources_only") {
|
||||||
|
return "checked_sources_only";
|
||||||
|
}
|
||||||
|
return "answer_draft_ready";
|
||||||
|
}
|
||||||
|
function businessFactAnswerAllowed(draft) {
|
||||||
|
return draft.answer_mode === "confirmed_with_bounded_inference" || draft.answer_mode === "bounded_inference_only";
|
||||||
|
}
|
||||||
|
async function runAssistantMcpDiscoveryRuntimeBridge(input) {
|
||||||
|
const planner = (0, assistantMcpDiscoveryPlanner_1.planAssistantMcpDiscovery)({
|
||||||
|
semanticDataNeed: input.semanticDataNeed,
|
||||||
|
turnMeaning: input.turnMeaning
|
||||||
|
});
|
||||||
|
const pilot = await (0, assistantMcpDiscoveryPilotExecutor_1.executeAssistantMcpDiscoveryPilot)(planner, input.deps);
|
||||||
|
const answerDraft = (0, assistantMcpDiscoveryAnswerAdapter_1.buildAssistantMcpDiscoveryAnswerDraft)(pilot);
|
||||||
|
const bridgeStatus = bridgeStatusFor(pilot, answerDraft);
|
||||||
|
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");
|
||||||
|
return {
|
||||||
|
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION,
|
||||||
|
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
|
||||||
|
bridge_status: bridgeStatus,
|
||||||
|
hot_runtime_wired: false,
|
||||||
|
planner,
|
||||||
|
pilot,
|
||||||
|
answer_draft: answerDraft,
|
||||||
|
user_facing_response_allowed: bridgeStatus !== "blocked",
|
||||||
|
business_fact_answer_allowed: businessFactAnswerAllowed(answerDraft),
|
||||||
|
requires_user_clarification: bridgeStatus === "needs_clarification",
|
||||||
|
reason_codes: reasonCodes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
import {
|
||||||
|
buildAssistantMcpDiscoveryAnswerDraft,
|
||||||
|
type AssistantMcpDiscoveryAnswerDraftContract
|
||||||
|
} from "./assistantMcpDiscoveryAnswerAdapter";
|
||||||
|
import {
|
||||||
|
executeAssistantMcpDiscoveryPilot,
|
||||||
|
type AssistantMcpDiscoveryPilotExecutionContract,
|
||||||
|
type AssistantMcpDiscoveryPilotExecutorDeps
|
||||||
|
} from "./assistantMcpDiscoveryPilotExecutor";
|
||||||
|
import {
|
||||||
|
planAssistantMcpDiscovery,
|
||||||
|
type AssistantMcpDiscoveryPlannerContract
|
||||||
|
} from "./assistantMcpDiscoveryPlanner";
|
||||||
|
import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscoveryPolicy";
|
||||||
|
|
||||||
|
export const ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION =
|
||||||
|
"assistant_mcp_discovery_runtime_bridge_v1" as const;
|
||||||
|
|
||||||
|
export type AssistantMcpDiscoveryRuntimeBridgeStatus =
|
||||||
|
| "answer_draft_ready"
|
||||||
|
| "checked_sources_only"
|
||||||
|
| "needs_clarification"
|
||||||
|
| "blocked"
|
||||||
|
| "unsupported";
|
||||||
|
|
||||||
|
export interface AssistantMcpDiscoveryRuntimeBridgeInput {
|
||||||
|
semanticDataNeed?: string | null;
|
||||||
|
turnMeaning?: AssistantMcpDiscoveryTurnMeaningRef | null;
|
||||||
|
deps?: AssistantMcpDiscoveryPilotExecutorDeps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssistantMcpDiscoveryRuntimeBridgeContract {
|
||||||
|
schema_version: typeof ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION;
|
||||||
|
policy_owner: "assistantMcpDiscoveryRuntimeBridge";
|
||||||
|
bridge_status: AssistantMcpDiscoveryRuntimeBridgeStatus;
|
||||||
|
hot_runtime_wired: false;
|
||||||
|
planner: AssistantMcpDiscoveryPlannerContract;
|
||||||
|
pilot: AssistantMcpDiscoveryPilotExecutionContract;
|
||||||
|
answer_draft: AssistantMcpDiscoveryAnswerDraftContract;
|
||||||
|
user_facing_response_allowed: boolean;
|
||||||
|
business_fact_answer_allowed: boolean;
|
||||||
|
requires_user_clarification: boolean;
|
||||||
|
reason_codes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeReasonCode(value: string): string | null {
|
||||||
|
const normalized = value
|
||||||
|
.trim()
|
||||||
|
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
|
||||||
|
.replace(/^_+|_+$/g, "")
|
||||||
|
.toLowerCase();
|
||||||
|
return normalized.length > 0 ? normalized.slice(0, 120) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushReason(target: string[], value: string): void {
|
||||||
|
const normalized = normalizeReasonCode(value);
|
||||||
|
if (normalized && !target.includes(normalized)) {
|
||||||
|
target.push(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 bridgeStatusFor(
|
||||||
|
pilot: AssistantMcpDiscoveryPilotExecutionContract,
|
||||||
|
draft: AssistantMcpDiscoveryAnswerDraftContract
|
||||||
|
): AssistantMcpDiscoveryRuntimeBridgeStatus {
|
||||||
|
if (draft.answer_mode === "blocked" || pilot.pilot_status === "blocked") {
|
||||||
|
return "blocked";
|
||||||
|
}
|
||||||
|
if (draft.answer_mode === "needs_clarification" || pilot.pilot_status === "skipped_needs_clarification") {
|
||||||
|
return "needs_clarification";
|
||||||
|
}
|
||||||
|
if (pilot.pilot_status === "unsupported") {
|
||||||
|
return "unsupported";
|
||||||
|
}
|
||||||
|
if (draft.answer_mode === "checked_sources_only") {
|
||||||
|
return "checked_sources_only";
|
||||||
|
}
|
||||||
|
return "answer_draft_ready";
|
||||||
|
}
|
||||||
|
|
||||||
|
function businessFactAnswerAllowed(draft: AssistantMcpDiscoveryAnswerDraftContract): boolean {
|
||||||
|
return draft.answer_mode === "confirmed_with_bounded_inference" || draft.answer_mode === "bounded_inference_only";
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runAssistantMcpDiscoveryRuntimeBridge(
|
||||||
|
input: AssistantMcpDiscoveryRuntimeBridgeInput
|
||||||
|
): Promise<AssistantMcpDiscoveryRuntimeBridgeContract> {
|
||||||
|
const planner = planAssistantMcpDiscovery({
|
||||||
|
semanticDataNeed: input.semanticDataNeed,
|
||||||
|
turnMeaning: input.turnMeaning
|
||||||
|
});
|
||||||
|
const pilot = await executeAssistantMcpDiscoveryPilot(planner, input.deps);
|
||||||
|
const answerDraft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||||
|
const bridgeStatus = bridgeStatusFor(pilot, answerDraft);
|
||||||
|
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");
|
||||||
|
|
||||||
|
return {
|
||||||
|
schema_version: ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION,
|
||||||
|
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
|
||||||
|
bridge_status: bridgeStatus,
|
||||||
|
hot_runtime_wired: false,
|
||||||
|
planner,
|
||||||
|
pilot,
|
||||||
|
answer_draft: answerDraft,
|
||||||
|
user_facing_response_allowed: bridgeStatus !== "blocked",
|
||||||
|
business_fact_answer_allowed: businessFactAnswerAllowed(answerDraft),
|
||||||
|
requires_user_clarification: bridgeStatus === "needs_clarification",
|
||||||
|
reason_codes: reasonCodes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { runAssistantMcpDiscoveryRuntimeBridge } from "../src/services/assistantMcpDiscoveryRuntimeBridge";
|
||||||
|
|
||||||
|
function buildDeps(rows: Array<Record<string, unknown>>, error: string | null = null) {
|
||||||
|
return {
|
||||||
|
executeAddressMcpQuery: vi.fn(async () => ({
|
||||||
|
fetched_rows: rows.length,
|
||||||
|
matched_rows: error ? 0 : rows.length,
|
||||||
|
raw_rows: rows,
|
||||||
|
rows: error ? [] : rows,
|
||||||
|
error
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("assistant MCP discovery runtime bridge", () => {
|
||||||
|
it("composes planner, pilot executor, and answer draft without wiring the hot runtime", async () => {
|
||||||
|
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||||
|
turnMeaning: {
|
||||||
|
asked_domain_family: "counterparty_lifecycle",
|
||||||
|
asked_action_family: "activity_duration",
|
||||||
|
explicit_entity_candidates: ["SVK"]
|
||||||
|
},
|
||||||
|
deps: buildDeps([{ Период: "2020-01-15T00:00:00", Регистратор: "CP_CUSTOMER_ACTIVITY_FIRST" }])
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.schema_version).toBe("assistant_mcp_discovery_runtime_bridge_v1");
|
||||||
|
expect(result.bridge_status).toBe("answer_draft_ready");
|
||||||
|
expect(result.hot_runtime_wired).toBe(false);
|
||||||
|
expect(result.pilot.mcp_execution_performed).toBe(true);
|
||||||
|
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.reason_codes).toContain("runtime_bridge_not_wired_to_hot_assistant_answer");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps missing scope as clarification and does not authorize a business fact answer", async () => {
|
||||||
|
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||||
|
turnMeaning: {
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "turnover",
|
||||||
|
explicit_entity_candidates: ["SVK"]
|
||||||
|
},
|
||||||
|
deps: buildDeps([])
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.bridge_status).toBe("needs_clarification");
|
||||||
|
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.answer_draft.next_step_line).toContain("Уточните контрагента");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps unsupported ready plans outside the hot answer path", async () => {
|
||||||
|
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||||
|
turnMeaning: {
|
||||||
|
asked_domain_family: "document",
|
||||||
|
asked_action_family: "documents",
|
||||||
|
explicit_entity_candidates: ["SVK"]
|
||||||
|
},
|
||||||
|
deps: buildDeps([])
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.bridge_status).toBe("unsupported");
|
||||||
|
expect(result.hot_runtime_wired).toBe(false);
|
||||||
|
expect(result.pilot.mcp_execution_performed).toBe(false);
|
||||||
|
expect(result.business_fact_answer_allowed).toBe(false);
|
||||||
|
expect(result.reason_codes).toContain("runtime_bridge_status_unsupported");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves the answer adapter boundary against internal mechanics leakage", async () => {
|
||||||
|
const result = await runAssistantMcpDiscoveryRuntimeBridge({
|
||||||
|
turnMeaning: {
|
||||||
|
asked_domain_family: "counterparty_lifecycle",
|
||||||
|
asked_action_family: "activity_duration",
|
||||||
|
explicit_entity_candidates: ["SVK"]
|
||||||
|
},
|
||||||
|
deps: buildDeps([{ Период: "2020-01-15T00:00:00", Регистратор: "CP_CUSTOMER_ACTIVITY_FIRST" }])
|
||||||
|
});
|
||||||
|
const userFacing = [
|
||||||
|
result.answer_draft.headline,
|
||||||
|
...result.answer_draft.confirmed_lines,
|
||||||
|
...result.answer_draft.inference_lines,
|
||||||
|
...result.answer_draft.unknown_lines,
|
||||||
|
...result.answer_draft.limitation_lines,
|
||||||
|
result.answer_draft.next_step_line ?? ""
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
expect(userFacing).not.toContain("query_documents");
|
||||||
|
expect(userFacing).not.toContain("runtime_bridge");
|
||||||
|
expect(userFacing).not.toContain("primitive");
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue