ARCH: ввести resumable discovery loop state для clarification follow-up

This commit is contained in:
2026-04-23 11:54:36 +03:00
parent 0d3b33578e
commit e4cab85dd9
11 changed files with 715 additions and 11 deletions
@@ -174,6 +174,12 @@ function readAssistantMcpDiscoveryBridge(
return toRecordObject(readAssistantMcpDiscoveryEntry(debug)?.bridge);
}
function readAssistantMcpDiscoveryLoopState(
debug: Record<string, unknown> | null
): Record<string, unknown> | null {
return toRecordObject(readAssistantMcpDiscoveryBridge(debug)?.loop_state);
}
function readAssistantMcpDiscoveryDerivedMetadataSurface(
debug: Record<string, unknown> | null
): Record<string, unknown> | null {
@@ -260,7 +266,67 @@ export function readAssistantMcpDiscoveryRankingNeed(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string | null {
return toNonEmptyString(readAssistantMcpDiscoveryDataNeedGraph(debug)?.ranking_need);
return (
toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.ranking_need) ??
toNonEmptyString(readAssistantMcpDiscoveryDataNeedGraph(debug)?.ranking_need)
);
}
export function readAssistantMcpDiscoveryLoopStatus(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string | null {
return toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.loop_status);
}
export function readAssistantMcpDiscoveryLoopSelectedChainId(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string | null {
return toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.selected_chain_id);
}
export function readAssistantMcpDiscoveryLoopPendingAxes(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string[] {
const values = readAssistantMcpDiscoveryLoopState(debug)?.pending_axes;
if (!Array.isArray(values)) {
return [];
}
return values.map((item) => toNonEmptyString(item)).filter((item): item is string => Boolean(item));
}
export function readAssistantMcpDiscoveryLoopProvidedAxes(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string[] {
const values = readAssistantMcpDiscoveryLoopState(debug)?.provided_axes;
if (!Array.isArray(values)) {
return [];
}
return values.map((item) => toNonEmptyString(item)).filter((item): item is string => Boolean(item));
}
export function readAssistantMcpDiscoveryLoopAskedDomainFamily(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string | null {
return toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.asked_domain_family);
}
export function readAssistantMcpDiscoveryLoopAskedActionFamily(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string | null {
return toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.asked_action_family);
}
export function readAssistantMcpDiscoveryLoopUnsupportedFamily(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string | null {
return toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.unsupported_but_understood_family);
}
export function readAssistantMcpDiscoveryMetadataRouteFamily(
@@ -9,6 +9,7 @@ import {
} from "./assistantMcpDiscoveryPilotExecutor";
import {
planAssistantMcpDiscovery,
type AssistantMcpDiscoveryChainId,
type AssistantMcpDiscoveryMetadataSurfaceRef,
type AssistantMcpDiscoveryPlannerContract
} from "./assistantMcpDiscoveryPlanner";
@@ -17,6 +18,8 @@ import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscover
export const ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION =
"assistant_mcp_discovery_runtime_bridge_v1" as const;
export const ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION =
"assistant_mcp_discovery_loop_state_v1" as const;
export type AssistantMcpDiscoveryRuntimeBridgeStatus =
| "answer_draft_ready"
@@ -24,6 +27,10 @@ export type AssistantMcpDiscoveryRuntimeBridgeStatus =
| "needs_clarification"
| "blocked"
| "unsupported";
export type AssistantMcpDiscoveryLoopStatus =
| "awaiting_clarification"
| "ready_for_next_hop"
| "blocked";
export interface AssistantMcpDiscoveryRuntimeBridgeInput {
semanticDataNeed?: string | null;
@@ -33,6 +40,23 @@ export interface AssistantMcpDiscoveryRuntimeBridgeInput {
deps?: AssistantMcpDiscoveryPilotExecutorDeps;
}
export interface AssistantMcpDiscoveryLoopStateContract {
schema_version: typeof ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION;
policy_owner: "assistantMcpDiscoveryRuntimeBridge";
loop_status: AssistantMcpDiscoveryLoopStatus;
selected_chain_id: AssistantMcpDiscoveryChainId;
pilot_scope: AssistantMcpDiscoveryPilotExecutionContract["pilot_scope"];
asked_domain_family: string | null;
asked_action_family: string | null;
unsupported_but_understood_family: string | null;
ranking_need: string | null;
pending_axes: string[];
provided_axes: string[];
explicit_entity_candidates: string[];
explicit_organization_scope: string | null;
explicit_date_scope: string | null;
}
export interface AssistantMcpDiscoveryRuntimeBridgeContract {
schema_version: typeof ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION;
policy_owner: "assistantMcpDiscoveryRuntimeBridge";
@@ -41,6 +65,7 @@ export interface AssistantMcpDiscoveryRuntimeBridgeContract {
planner: AssistantMcpDiscoveryPlannerContract;
pilot: AssistantMcpDiscoveryPilotExecutionContract;
answer_draft: AssistantMcpDiscoveryAnswerDraftContract;
loop_state: AssistantMcpDiscoveryLoopStateContract;
user_facing_response_allowed: boolean;
business_fact_answer_allowed: boolean;
requires_user_clarification: boolean;
@@ -97,6 +122,72 @@ function businessFactAnswerAllowed(draft: AssistantMcpDiscoveryAnswerDraftContra
return draft.answer_mode === "confirmed_with_bounded_inference" || draft.answer_mode === "bounded_inference_only";
}
function loopStatusFor(
bridgeStatus: AssistantMcpDiscoveryRuntimeBridgeStatus
): AssistantMcpDiscoveryLoopStatus {
if (bridgeStatus === "needs_clarification") {
return "awaiting_clarification";
}
if (bridgeStatus === "blocked" || bridgeStatus === "unsupported") {
return "blocked";
}
return "ready_for_next_hop";
}
function flattenAxes(
pilot: AssistantMcpDiscoveryPilotExecutionContract,
source: "provided_axes" | "missing_axis_options"
): string[] {
const result: string[] = [];
for (const step of pilot.dry_run.execution_steps) {
if (source === "provided_axes") {
for (const axis of step.provided_axes) {
if (axis && !result.includes(axis)) {
result.push(axis);
}
}
continue;
}
for (const option of step.missing_axis_options) {
for (const axis of option) {
if (axis && !result.includes(axis)) {
result.push(axis);
}
}
}
}
return result;
}
function entityCandidatesFromPlanner(planner: AssistantMcpDiscoveryPlannerContract): string[] {
const values = planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? [];
return uniqueStrings(values);
}
function buildLoopState(
planner: AssistantMcpDiscoveryPlannerContract,
pilot: AssistantMcpDiscoveryPilotExecutionContract,
bridgeStatus: AssistantMcpDiscoveryRuntimeBridgeStatus
): AssistantMcpDiscoveryLoopStateContract {
return {
schema_version: ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
loop_status: loopStatusFor(bridgeStatus),
selected_chain_id: planner.selected_chain_id,
pilot_scope: pilot.pilot_scope,
asked_domain_family: planner.discovery_plan.turn_meaning_ref?.asked_domain_family ?? null,
asked_action_family: planner.discovery_plan.turn_meaning_ref?.asked_action_family ?? null,
unsupported_but_understood_family:
planner.discovery_plan.turn_meaning_ref?.unsupported_but_understood_family ?? null,
ranking_need: planner.data_need_graph?.ranking_need ?? planner.discovery_plan.turn_meaning_ref?.seeded_ranking_need ?? null,
pending_axes: flattenAxes(pilot, "missing_axis_options"),
provided_axes: flattenAxes(pilot, "provided_axes"),
explicit_entity_candidates: entityCandidatesFromPlanner(planner),
explicit_organization_scope: planner.discovery_plan.turn_meaning_ref?.explicit_organization_scope ?? null,
explicit_date_scope: planner.discovery_plan.turn_meaning_ref?.explicit_date_scope ?? null
};
}
export async function runAssistantMcpDiscoveryRuntimeBridge(
input: AssistantMcpDiscoveryRuntimeBridgeInput
): Promise<AssistantMcpDiscoveryRuntimeBridgeContract> {
@@ -109,10 +200,12 @@ export async function runAssistantMcpDiscoveryRuntimeBridge(
const pilot = await executeAssistantMcpDiscoveryPilot(planner, input.deps);
const answerDraft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
const bridgeStatus = bridgeStatusFor(pilot, answerDraft);
const loopState = buildLoopState(planner, pilot, bridgeStatus);
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}`);
return {
schema_version: ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION,
@@ -122,6 +215,7 @@ export async function runAssistantMcpDiscoveryRuntimeBridge(
planner,
pilot,
answer_draft: answerDraft,
loop_state: loopState,
user_facing_response_allowed: bridgeStatus !== "blocked",
business_fact_answer_allowed: businessFactAnswerAllowed(answerDraft),
requires_user_clarification: bridgeStatus === "needs_clarification",
@@ -339,11 +339,87 @@ function mapAddressIntentToFollowupMeaning(
};
}
function mapLoopClarificationSeedToFollowupMeaning(input: {
selectedChainId: string | null;
domain: string | null;
action: string | null;
unsupported: string | null;
}): {
domain: string | null;
action: string | null;
unsupported: string | null;
} {
if (input.domain || input.action || input.unsupported) {
return {
domain: input.domain,
action: input.action,
unsupported: input.unsupported
};
}
if (input.selectedChainId === "metadata_lane_clarification") {
return {
domain: "metadata",
action: "resolve_next_lane",
unsupported: "metadata_lane_choice_clarification"
};
}
return {
domain: null,
action: null,
unsupported: null
};
}
function pilotScopeFromLoopClarificationSeed(
selectedChainId: string | null,
action: string | null
): string | null {
if (!selectedChainId) {
return null;
}
if (
selectedChainId === "metadata_inspection" ||
selectedChainId === "metadata_lane_clarification" ||
selectedChainId === "catalog_drilldown"
) {
return "metadata_inspection_v1";
}
if (selectedChainId === "movement_evidence") {
return "counterparty_movement_evidence_query_movements_v1";
}
if (selectedChainId === "document_evidence") {
return "counterparty_document_evidence_query_documents_v1";
}
if (selectedChainId === "lifecycle") {
return "counterparty_lifecycle_query_documents_v1";
}
if (selectedChainId === "entity_resolution") {
return "entity_resolution_search_v1";
}
if (selectedChainId === "value_flow_comparison") {
return "counterparty_bidirectional_value_flow_query_movements_v1";
}
if (selectedChainId === "value_flow_ranking" || selectedChainId === "value_flow") {
if (action === "payout") {
return "counterparty_supplier_payout_query_movements_v1";
}
if (action === "net_value_flow") {
return "counterparty_bidirectional_value_flow_query_movements_v1";
}
return "counterparty_value_flow_query_movements_v1";
}
return null;
}
function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> | null): {
pilotScope: string | null;
domain: string | null;
action: string | null;
unsupported: string | null;
loopStatus: string | null;
loopSelectedChainId: string | null;
loopPendingAxes: string[];
loopProvidedAxes: string[];
counterparty: string | null;
discoveryEntity: string | null;
entityResolutionStatus: string | null;
@@ -362,12 +438,36 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
const previousFilters = toRecordObject(followupContext?.previous_filters);
const rootFilters = toRecordObject(followupContext?.root_filters);
const pilotScope = toNonEmptyString(followupContext?.previous_discovery_pilot_scope);
const loopStatus = toNonEmptyString(followupContext?.previous_discovery_loop_status);
const loopSelectedChainId = toNonEmptyString(followupContext?.previous_discovery_loop_selected_chain_id);
const loopPendingAxes = collectEntityCandidates(followupContext?.previous_discovery_loop_pending_axes);
const loopProvidedAxes = collectEntityCandidates(followupContext?.previous_discovery_loop_provided_axes);
const loopAskedDomainFamily = toNonEmptyString(followupContext?.previous_discovery_loop_asked_domain_family);
const loopAskedActionFamily = toNonEmptyString(followupContext?.previous_discovery_loop_asked_action_family);
const loopUnsupportedFamily = toNonEmptyString(followupContext?.previous_discovery_loop_unsupported_family);
const previousIntent =
toNonEmptyString(followupContext?.target_intent) ?? toNonEmptyString(followupContext?.previous_intent);
const loopMapped =
loopStatus === "awaiting_clarification"
? mapLoopClarificationSeedToFollowupMeaning({
selectedChainId: loopSelectedChainId,
domain: loopAskedDomainFamily,
action: loopAskedActionFamily,
unsupported: loopUnsupportedFamily
})
: {
domain: null,
action: null,
unsupported: null
};
const effectivePilotScope =
pilotScope ?? pilotScopeFromLoopClarificationSeed(loopSelectedChainId, loopMapped.action);
const mapped =
mapPilotScopeToFollowupMeaning(pilotScope).domain !== null
? mapPilotScopeToFollowupMeaning(pilotScope)
: mapAddressIntentToFollowupMeaning(previousIntent);
loopMapped.domain !== null || loopMapped.action !== null || loopMapped.unsupported !== null
? loopMapped
: mapPilotScopeToFollowupMeaning(effectivePilotScope).domain !== null
? mapPilotScopeToFollowupMeaning(effectivePilotScope)
: mapAddressIntentToFollowupMeaning(previousIntent);
const discoveryEntities = collectEntityCandidates(followupContext?.previous_discovery_entity_candidates);
const entityResolutionStatus = toNonEmptyString(followupContext?.previous_discovery_entity_resolution_status);
const entityResolutionAmbiguityCandidates = collectEntityCandidates(
@@ -392,10 +492,14 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
collectDateScopeFromFilters(previousFilters) ??
collectDateScopeFromFilters(rootFilters);
return {
pilotScope,
pilotScope: effectivePilotScope,
domain: mapped.domain,
action: mapped.action,
unsupported: mapped.unsupported,
loopStatus,
loopSelectedChainId,
loopPendingAxes,
loopProvidedAxes,
counterparty,
discoveryEntity: ambiguityBlocksImplicitGrounding ? null : discoveryEntities[0] ?? null,
entityResolutionStatus,
@@ -1278,6 +1382,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
!rawDateScope &&
followupSeed.dateScope
);
const clarificationLoopSeedApplied = Boolean(
followupSeed.loopStatus === "awaiting_clarification" && followupSeed.loopSelectedChainId
);
const turnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {
asked_domain_family:
@@ -1478,6 +1585,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (followupDiscoverySeedApplicable) {
pushReason(reasonCodes, "mcp_discovery_seeded_from_followup_context");
}
if (clarificationLoopSeedApplied) {
pushReason(reasonCodes, "mcp_discovery_resumed_from_saved_loop_state");
}
if (effectiveMetadataFollowupSeedApplicable) {
pushReason(reasonCodes, "mcp_discovery_metadata_seeded_from_followup_context");
}
@@ -21,6 +21,13 @@ import {
readAssistantMcpDiscoveryMetadataSelectedSurfaceObjects,
readAssistantMcpDiscoveryMetadataRecommendedNextPrimitive,
readAssistantMcpDiscoveryRankingNeed,
readAssistantMcpDiscoveryLoopStatus,
readAssistantMcpDiscoveryLoopSelectedChainId,
readAssistantMcpDiscoveryLoopPendingAxes,
readAssistantMcpDiscoveryLoopProvidedAxes,
readAssistantMcpDiscoveryLoopAskedDomainFamily,
readAssistantMcpDiscoveryLoopAskedActionFamily,
readAssistantMcpDiscoveryLoopUnsupportedFamily,
readAddressDebugTemporalScope,
readAssistantMcpDiscoveryPilotScope,
resolveOrganizationClarificationContinuation,
@@ -695,6 +702,31 @@ export function createAssistantTransitionPolicy(deps) {
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryLoopStatus = readAssistantMcpDiscoveryLoopStatus(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryLoopSelectedChainId = readAssistantMcpDiscoveryLoopSelectedChainId(
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryLoopPendingAxes = readAssistantMcpDiscoveryLoopPendingAxes(
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryLoopProvidedAxes = readAssistantMcpDiscoveryLoopProvidedAxes(
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryLoopAskedDomainFamily = readAssistantMcpDiscoveryLoopAskedDomainFamily(
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryLoopAskedActionFamily = readAssistantMcpDiscoveryLoopAskedActionFamily(
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryLoopUnsupportedFamily = readAssistantMcpDiscoveryLoopUnsupportedFamily(
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryRankingNeed = readAssistantMcpDiscoveryRankingNeed(
carryoverSourceDebug,
deps.toNonEmptyString
@@ -1042,6 +1074,15 @@ export function createAssistantTransitionPolicy(deps) {
previous_discovery_entity_resolution_status: sourceDiscoveryEntityResolutionStatus ?? undefined,
previous_discovery_entity_candidates:
sourceDiscoveryEntityCandidates.length > 0 ? sourceDiscoveryEntityCandidates : undefined,
previous_discovery_loop_status: sourceDiscoveryLoopStatus ?? undefined,
previous_discovery_loop_selected_chain_id: sourceDiscoveryLoopSelectedChainId ?? undefined,
previous_discovery_loop_pending_axes:
sourceDiscoveryLoopPendingAxes.length > 0 ? sourceDiscoveryLoopPendingAxes : undefined,
previous_discovery_loop_provided_axes:
sourceDiscoveryLoopProvidedAxes.length > 0 ? sourceDiscoveryLoopProvidedAxes : undefined,
previous_discovery_loop_asked_domain_family: sourceDiscoveryLoopAskedDomainFamily ?? undefined,
previous_discovery_loop_asked_action_family: sourceDiscoveryLoopAskedActionFamily ?? undefined,
previous_discovery_loop_unsupported_family: sourceDiscoveryLoopUnsupportedFamily ?? undefined,
previous_discovery_ranking_need: sourceDiscoveryRankingNeed ?? undefined,
previous_discovery_entity_ambiguity_candidates:
sourceDiscoveryEntityAmbiguityCandidates.length > 0