ARCH: довести planner-selected entity chains и ambiguity follow-up

This commit is contained in:
2026-04-22 17:32:24 +03:00
parent bc54cd9628
commit bd58ab490f
19 changed files with 1193 additions and 65 deletions
@@ -40,6 +40,8 @@ export interface AddressFollowupContext {
| "unknown"
| null;
previous_anchor_value?: string | null;
previous_discovery_entity_resolution_status?: "resolved" | "ambiguous" | "not_found" | null;
previous_discovery_entity_ambiguity_candidates?: string[];
resolved_counterparty_from_display?: boolean;
root_intent?: AddressIntent;
root_filters?: AddressFilterSet;
@@ -182,6 +182,24 @@ function readAssistantMcpDiscoveryDerivedEntityResolution(
return toRecordObject(pilot?.derived_entity_resolution);
}
export function readAssistantMcpDiscoveryEntityResolutionStatus(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string | null {
return toNonEmptyString(readAssistantMcpDiscoveryDerivedEntityResolution(debug)?.resolution_status);
}
export function readAssistantMcpDiscoveryEntityAmbiguityCandidates(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string[] {
const values = readAssistantMcpDiscoveryDerivedEntityResolution(debug)?.ambiguity_candidates;
if (!Array.isArray(values)) {
return [];
}
return values.map((item) => toNonEmptyString(item)).filter((item): item is string => Boolean(item));
}
function collectAssistantMcpDiscoveryEntityCandidates(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
@@ -52,6 +52,13 @@ function uniqueStrings(values: string[]): string[] {
return result;
}
function formatNamedChoiceList(values: string[]): string {
return uniqueStrings(values)
.slice(0, 6)
.map((value, index) => `${index + 1}. ${value}`)
.join("; ");
}
function isInternalMechanicsLine(value: string): boolean {
const text = value.toLowerCase();
return (
@@ -344,6 +351,12 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
if (isEntityResolutionPilot(pilot) && mode === "needs_clarification") {
const ambiguityCandidates = pilot.derived_entity_resolution?.ambiguity_candidates ?? [];
if (ambiguityCandidates.length > 0) {
return `Уточните, какой именно контрагент нужен: ${formatNamedChoiceList(
ambiguityCandidates
)}. Можно ответить названием или номером варианта.`;
}
return "Уточните точное название контрагента или добавьте ИНН, и я продолжу уже по нужной сущности в 1С.";
}
if (isEntityResolutionPilot(pilot) && mode === "confirmed_with_bounded_inference") {
@@ -536,7 +549,9 @@ function derivedEntityResolutionInferenceLine(pilot: AssistantMcpDiscoveryPilotE
return "Сейчас подтверждено только заземление сущности по каталогу 1С; документы, движения и денежные показатели по ней еще не проверялись.";
}
if (resolution.resolution_status === "ambiguous" && resolution.ambiguity_candidates.length > 0) {
return `В checked catalog slice есть несколько близких кандидатов: ${resolution.ambiguity_candidates.join(", ")}. Без уточнения нельзя честно выбрать одного контрагента для следующего шага.`;
return `В каталоге 1С нашлось несколько близких кандидатов: ${formatNamedChoiceList(
resolution.ambiguity_candidates
)}. Без уточнения нельзя честно выбрать одного контрагента для следующего шага.`;
}
return null;
}
@@ -974,6 +974,54 @@ function summarizeEntityResolutionRows(result: AddressMcpQueryExecutorResult): s
return `${result.fetched_rows} MCP catalog rows fetched for entity search`;
}
function entityResolutionFollowupStepLimitation(): string {
return "Entity-resolution could not continue because the checked catalog search step did not return a confirmed slice";
}
function buildEntityResolutionResolveProbeResult(input: {
queryResult: AddressMcpQueryExecutorResult;
resolution: AssistantMcpDiscoveryDerivedEntityResolution | null;
}): AssistantMcpDiscoveryProbeResult {
if (!input.resolution) {
return {
primitive_id: "resolve_entity_reference",
status: "ok",
rows_received: input.queryResult.fetched_rows,
rows_matched: 0,
limitation: null
};
}
if (input.resolution.resolution_status === "resolved") {
return {
primitive_id: "resolve_entity_reference",
status: "ok",
rows_received: input.queryResult.fetched_rows,
rows_matched: 1,
limitation: null
};
}
return {
primitive_id: "resolve_entity_reference",
status: "ok",
rows_received: input.queryResult.fetched_rows,
rows_matched: 0,
limitation: null
};
}
function buildEntityResolutionCoverageProbeResult(input: {
resolution: AssistantMcpDiscoveryDerivedEntityResolution | null;
}): AssistantMcpDiscoveryProbeResult {
const resolved = input.resolution?.resolution_status === "resolved";
return {
primitive_id: "probe_coverage",
status: "ok",
rows_received: 1,
rows_matched: resolved ? 1 : 0,
limitation: null
};
}
function metadataRowText(row: Record<string, unknown>, keys: string[]): string | null {
for (const key of keys) {
const text = toNonEmptyString(row[key]);
@@ -2153,31 +2201,88 @@ export async function executeAssistantMcpDiscoveryPilot(
};
}
let derivedEntityResolution: AssistantMcpDiscoveryDerivedEntityResolution | null = null;
for (const step of dryRun.execution_steps) {
if (step.primitive_id !== "search_business_entity") {
skippedPrimitives.push(step.primitive_id);
probeResults.push(skippedProbeResult(step, "pilot_only_executes_search_business_entity"));
if (step.primitive_id === "search_business_entity") {
queryResult = await runtimeDeps.executeAddressMcpQuery({
query: ENTITY_RESOLUTION_COUNTERPARTY_QUERY_TEMPLATE.replaceAll(
"__LIMIT__",
String(ENTITY_RESOLUTION_COUNTERPARTY_LOOKUP_LIMIT)
),
limit: ENTITY_RESOLUTION_COUNTERPARTY_LOOKUP_LIMIT
});
pushUnique(executedPrimitives, step.primitive_id);
probeResults.push(queryResultToProbeResult(step.primitive_id, queryResult));
if (queryResult.error) {
pushUnique(queryLimitations, queryResult.error);
pushReason(reasonCodes, "pilot_search_business_entity_mcp_error");
} else {
pushReason(reasonCodes, "pilot_search_business_entity_mcp_executed");
derivedEntityResolution = deriveEntityResolution(queryResult, requestedEntity);
}
continue;
}
queryResult = await runtimeDeps.executeAddressMcpQuery({
query: ENTITY_RESOLUTION_COUNTERPARTY_QUERY_TEMPLATE.replaceAll(
"__LIMIT__",
String(ENTITY_RESOLUTION_COUNTERPARTY_LOOKUP_LIMIT)
),
limit: ENTITY_RESOLUTION_COUNTERPARTY_LOOKUP_LIMIT
});
pushUnique(executedPrimitives, step.primitive_id);
probeResults.push(queryResultToProbeResult(step.primitive_id, queryResult));
if (queryResult.error) {
pushUnique(queryLimitations, queryResult.error);
pushReason(reasonCodes, "pilot_search_business_entity_mcp_error");
} else {
pushReason(reasonCodes, "pilot_search_business_entity_mcp_executed");
if (step.primitive_id === "resolve_entity_reference") {
if (!queryResult || queryResult.error) {
skippedPrimitives.push(step.primitive_id);
probeResults.push(skippedProbeResult(step, entityResolutionFollowupStepLimitation()));
continue;
}
if (!derivedEntityResolution) {
derivedEntityResolution = deriveEntityResolution(queryResult, requestedEntity);
}
pushUnique(executedPrimitives, step.primitive_id);
probeResults.push(
buildEntityResolutionResolveProbeResult({
queryResult,
resolution: derivedEntityResolution
})
);
if (derivedEntityResolution?.resolution_status === "resolved") {
pushReason(reasonCodes, "pilot_resolve_entity_reference_from_catalog_rows");
} else if (derivedEntityResolution?.resolution_status === "ambiguous") {
pushReason(reasonCodes, "pilot_resolve_entity_reference_requires_clarification");
} else {
pushReason(reasonCodes, "pilot_resolve_entity_reference_not_confirmed");
}
continue;
}
if (step.primitive_id === "probe_coverage") {
if (!queryResult || queryResult.error) {
skippedPrimitives.push(step.primitive_id);
probeResults.push(skippedProbeResult(step, entityResolutionFollowupStepLimitation()));
continue;
}
if (!derivedEntityResolution) {
derivedEntityResolution = deriveEntityResolution(queryResult, requestedEntity);
}
pushUnique(executedPrimitives, step.primitive_id);
probeResults.push(
buildEntityResolutionCoverageProbeResult({
resolution: derivedEntityResolution
})
);
pushReason(reasonCodes, "pilot_probe_coverage_executed_for_entity_resolution");
if (derivedEntityResolution?.resolution_status === "resolved") {
pushReason(reasonCodes, "pilot_entity_resolution_grounding_stable_for_downstream_probe");
} else if (derivedEntityResolution?.resolution_status === "ambiguous") {
pushReason(reasonCodes, "pilot_entity_resolution_coverage_requires_clarification");
} else {
pushReason(reasonCodes, "pilot_entity_resolution_coverage_not_confirmed");
}
continue;
}
skippedPrimitives.push(step.primitive_id);
probeResults.push(skippedProbeResult(step, "pilot_entity_resolution_step_not_implemented"));
}
const sourceRowsSummary = queryResult ? summarizeEntityResolutionRows(queryResult) : null;
const derivedEntityResolution = deriveEntityResolution(queryResult, requestedEntity);
if (!derivedEntityResolution && queryResult && !queryResult.error) {
derivedEntityResolution = deriveEntityResolution(queryResult, requestedEntity);
}
if (derivedEntityResolution?.resolution_status === "resolved") {
pushReason(reasonCodes, "pilot_derived_entity_resolution_from_catalog_rows");
}
@@ -2195,7 +2300,7 @@ export async function executeAssistantMcpDiscoveryPilot(
unknownFacts: buildEntityResolutionUnknownFacts(derivedEntityResolution, requestedEntity),
sourceRowsSummary,
queryLimitations,
recommendedNextProbe: "resolve_entity_reference"
recommendedNextProbe: null
});
return {
@@ -214,6 +214,68 @@ function readDiscoveryTurnMeaning(
return toRecordObject(turnInput?.turn_meaning_ref);
}
function readTruthAnswerShape(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): Record<string, unknown> | null {
const directShape = toRecordObject(input.addressRuntimeMeta?.answer_shape_contract);
if (directShape) {
return directShape;
}
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
return toRecordObject(truthAnswerPolicy?.answer_shape);
}
function hasEffectivelyFactualAddressReply(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): boolean {
if (toNonEmptyString(input.currentReplyType) === "factual") {
return true;
}
const truthAnswerShape = readTruthAnswerShape(input);
return toNonEmptyString(truthAnswerShape?.reply_type) === "factual";
}
function readStateTransitionReasonCodes(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): string[] {
const directTransition = toRecordObject(input.addressRuntimeMeta?.assistant_state_transition_v1);
const fallbackTransition = toRecordObject(input.addressRuntimeMeta?.state_transition_contract);
const stateTransition = directTransition ?? fallbackTransition;
if (!stateTransition || !Array.isArray(stateTransition.reason_codes)) {
return [];
}
return stateTransition.reason_codes
.map((item) => toNonEmptyString(item))
.filter((item): item is string => Boolean(item));
}
function hasRuntimeAdjustedExactReply(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
const truthGateStatus = toNonEmptyString(input.addressRuntimeMeta?.truth_gate_contract_status);
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
const truthGate = toRecordObject(truthAnswerPolicy?.truth_gate);
const sourceTruthGateStatus = toNonEmptyString(truthGate?.source_truth_gate_status);
const coverageStatus = toNonEmptyString(truthGate?.coverage_status);
const groundingStatus = toNonEmptyString(truthGate?.grounding_status);
const hasFullConfirmedTruth =
truthGateStatus === "full_confirmed" ||
sourceTruthGateStatus === "full_confirmed" ||
(coverageStatus === "full" && groundingStatus === "grounded");
if (!hasFullConfirmedTruth) {
return false;
}
const truthAnswerShape = readTruthAnswerShape(input);
const capabilityContractId = toNonEmptyString(truthAnswerShape?.capability_contract_id);
if (!capabilityContractId) {
return false;
}
return readStateTransitionReasonCodes(input).some(
(reason) => /^intent_adjusted_to_.+_followup_context$/i.test(reason)
);
}
function hasAlignedFactualAddressReply(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
@@ -221,7 +283,7 @@ function hasAlignedFactualAddressReply(
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (toNonEmptyString(input.currentReplyType) !== "factual") {
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
@@ -235,7 +297,10 @@ function hasSemanticConflictWithDiscoveryTurnMeaning(
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (toNonEmptyString(input.currentReplyType) !== "factual") {
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
if (hasRuntimeAdjustedExactReply(input, entryPoint)) {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
@@ -253,7 +318,7 @@ function hasMatchedFactualAddressContinuationTarget(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
if (toNonEmptyString(input.currentReplyType) !== "factual") {
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
@@ -274,7 +339,7 @@ function hasFullConfirmedFactualAddressReply(
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (toNonEmptyString(input.currentReplyType) !== "factual") {
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
@@ -310,6 +375,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
const semanticConflictWithDiscoveryTurnMeaning = hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint);
const matchedFactualAddressContinuationTarget = hasMatchedFactualAddressContinuationTarget(input, entryPoint);
const fullConfirmedFactualAddressReply = hasFullConfirmedFactualAddressReply(input, entryPoint);
const runtimeAdjustedExactReply = hasRuntimeAdjustedExactReply(input, entryPoint);
if (!entryPoint) {
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
@@ -338,6 +404,12 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
if (fullConfirmedFactualAddressReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
}
if (runtimeAdjustedExactReply) {
pushReason(
reasonCodes,
"mcp_discovery_response_policy_keep_runtime_adjusted_exact_reply_over_stale_discovery_turn_meaning"
);
}
if (deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") {
pushReason(
reasonCodes,
@@ -363,6 +435,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
!alignedFactualAddressReply &&
!matchedFactualAddressContinuationTarget &&
!fullConfirmedFactualAddressReply &&
!runtimeAdjustedExactReply &&
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
candidate.eligible_for_future_hot_runtime &&
@@ -306,6 +306,8 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
unsupported: string | null;
counterparty: string | null;
discoveryEntity: string | null;
entityResolutionStatus: string | null;
entityResolutionAmbiguityCandidates: string[];
organization: string | null;
dateScope: string | null;
metadataRouteFamily: string | null;
@@ -323,13 +325,19 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
? mapPilotScopeToFollowupMeaning(pilotScope)
: mapAddressIntentToFollowupMeaning(previousIntent);
const discoveryEntities = collectEntityCandidates(followupContext?.previous_discovery_entity_candidates);
const entityResolutionStatus = toNonEmptyString(followupContext?.previous_discovery_entity_resolution_status);
const entityResolutionAmbiguityCandidates = collectEntityCandidates(
followupContext?.previous_discovery_entity_ambiguity_candidates
);
const ambiguityBlocksImplicitGrounding =
pilotScope === "entity_resolution_search_v1" && entityResolutionStatus === "ambiguous";
const counterparty =
toNonEmptyString(previousFilters?.counterparty) ??
toNonEmptyString(rootFilters?.counterparty) ??
(toNonEmptyString(followupContext?.previous_anchor_type) === "counterparty"
? toNonEmptyString(followupContext?.previous_anchor_value)
: null) ??
(discoveryEntities[0] ?? null);
(ambiguityBlocksImplicitGrounding ? null : discoveryEntities[0] ?? null);
const organization =
toNonEmptyString(previousFilters?.organization) ??
toNonEmptyString(rootFilters?.organization) ??
@@ -345,7 +353,9 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
action: mapped.action,
unsupported: mapped.unsupported,
counterparty,
discoveryEntity: discoveryEntities[0] ?? null,
discoveryEntity: ambiguityBlocksImplicitGrounding ? null : discoveryEntities[0] ?? null,
entityResolutionStatus,
entityResolutionAmbiguityCandidates,
organization,
dateScope,
metadataRouteFamily: toNonEmptyString(followupContext?.previous_discovery_metadata_route_family),
@@ -492,6 +502,105 @@ function rawEntityResolutionCandidate(text: string): string | null {
return null;
}
function resolveEntityResolutionAmbiguityChoice(text: string, candidates: string[]): string | null {
const normalizedText = canonicalizeEntityResolutionCandidate(text);
if (!normalizedText || candidates.length <= 0) {
return null;
}
const exactMatch = candidates.find(
(candidate) => canonicalizeEntityResolutionCandidate(candidate) === normalizedText
);
if (exactMatch) {
return exactMatch;
}
const includedMatches = candidates.filter((candidate) => {
const normalizedCandidate = canonicalizeEntityResolutionCandidate(candidate);
return normalizedCandidate.length > 0 && normalizedText.includes(normalizedCandidate);
});
if (includedMatches.length === 1) {
return includedMatches[0];
}
const narrowedMatches = candidates.filter((candidate) => {
const normalizedCandidate = canonicalizeEntityResolutionCandidate(candidate);
return normalizedText.length >= 4 && normalizedCandidate.includes(normalizedText);
});
if (narrowedMatches.length === 1) {
return narrowedMatches[0];
}
const normalizedLowerText = compactLower(text);
const ordinalMatchers: Array<{ index: number; keywords: string[]; numericPatterns: RegExp[] }> = [
{
index: 0,
keywords: ["первый"],
numericPatterns: [
/(?:^|[^\p{L}\p{N}])(?:вариант|номер|)?\s*1(?:$|[^\p{L}\p{N}])/iu,
/^\s*1\s*$/u
]
},
{
index: 1,
keywords: ["второй"],
numericPatterns: [
/(?:^|[^\p{L}\p{N}])(?:вариант|номер|)?\s*2(?:$|[^\p{L}\p{N}])/iu,
/^\s*2\s*$/u
]
},
{
index: 2,
keywords: ["третий"],
numericPatterns: [
/(?:^|[^\p{L}\p{N}])(?:вариант|номер|)?\s*3(?:$|[^\p{L}\p{N}])/iu,
/^\s*3\s*$/u
]
},
{
index: 3,
keywords: ["четвертый", "четвёртый"],
numericPatterns: [
/(?:^|[^\p{L}\p{N}])(?:вариант|номер|)?\s*4(?:$|[^\p{L}\p{N}])/iu,
/^\s*4\s*$/u
]
},
{
index: 4,
keywords: ["пятый"],
numericPatterns: [
/(?:^|[^\p{L}\p{N}])(?:вариант|номер|)?\s*5(?:$|[^\p{L}\p{N}])/iu,
/^\s*5\s*$/u
]
},
{
index: 5,
keywords: ["шестой"],
numericPatterns: [
/(?:^|[^\p{L}\p{N}])(?:вариант|номер|)?\s*6(?:$|[^\p{L}\p{N}])/iu,
/^\s*6\s*$/u
]
}
];
for (const matcher of ordinalMatchers) {
if (
matcher.index < candidates.length &&
(matcher.keywords.some((keyword) => normalizedLowerText.includes(keyword)) ||
matcher.numericPatterns.some((pattern) => pattern.test(normalizedLowerText)))
) {
return candidates[matcher.index];
}
}
if (
candidates.length > 0 &&
(normalizedLowerText.includes("последний") || normalizedLowerText.includes("крайний"))
) {
return candidates[candidates.length - 1] ?? null;
}
return null;
}
function metadataActionFromRawText(text: string): string {
if (/(?:\u043e\u0431\u044a\u0435\u043a\u0442(?:\u044b|\u0430|\u043e\u0432)?|objects?)/iu.test(text)) {
return "inspect_surface";
@@ -642,7 +751,16 @@ export function buildAssistantMcpDiscoveryTurnInput(
const rawDateScope = collectDateScopeFromRawText(rawText);
const rawMetadataScopeHint = rawMetadataSignal ? metadataScopeHintFromRawText(rawText) : null;
const rawEntityCandidate = rawEntityResolutionSignal ? rawEntityResolutionCandidate(rawEntitySourceText) : null;
const entityResolutionSignal = rawEntityResolutionSignal || Boolean(rawEntityCandidate);
const entityResolutionClarificationCandidate =
followupSeed.pilotScope === "entity_resolution_search_v1" &&
followupSeed.entityResolutionStatus === "ambiguous"
? resolveEntityResolutionAmbiguityChoice(
rawEntitySourceText,
followupSeed.entityResolutionAmbiguityCandidates
)
: null;
const entityResolutionSignal =
rawEntityResolutionSignal || Boolean(rawEntityCandidate) || Boolean(entityResolutionClarificationCandidate);
const metadataDocumentHintSignal =
hasDocumentEvidenceFollowupSignal(rawText) || hasPronounDocumentEvidenceFollowupSignal(rawText);
const metadataMovementHintSignal =
@@ -712,6 +830,15 @@ export function buildAssistantMcpDiscoveryTurnInput(
!rawMetadataSignal &&
metadataDocumentHintSignal
);
const entityResolutionClarifiedDocumentFollowupApplicable = Boolean(
followupSeed.pilotScope === "entity_resolution_search_v1" &&
followupSeed.entityResolutionStatus === "ambiguous" &&
entityResolutionClarificationCandidate &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawMetadataSignal &&
metadataDocumentHintSignal
);
const entityResolutionGroundedMovementFollowupApplicable = Boolean(
followupSeed.pilotScope === "entity_resolution_search_v1" &&
(followupSeed.counterparty || followupSeed.discoveryEntity) &&
@@ -720,6 +847,15 @@ export function buildAssistantMcpDiscoveryTurnInput(
!rawMetadataSignal &&
metadataMovementHintSignal
);
const entityResolutionClarifiedMovementFollowupApplicable = Boolean(
followupSeed.pilotScope === "entity_resolution_search_v1" &&
followupSeed.entityResolutionStatus === "ambiguous" &&
entityResolutionClarificationCandidate &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawMetadataSignal &&
metadataMovementHintSignal
);
const groundedValueFlowFollowupApplicable = Boolean(
rawValueFlowSignal &&
!rawLifecycleSignal &&
@@ -821,6 +957,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
metadataGroundedDocumentFollowupApplicable ||
metadataAmbiguityResolvedDocumentFollowupApplicable ||
entityResolutionGroundedDocumentFollowupApplicable ||
entityResolutionClarifiedDocumentFollowupApplicable ||
valueFlowGroundedDocumentFollowupApplicable ||
movementEvidenceGroundedDocumentFollowupApplicable ||
(metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "document_evidence") ||
@@ -829,6 +966,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
metadataGroundedMovementFollowupApplicable ||
metadataAmbiguityResolvedMovementFollowupApplicable ||
entityResolutionGroundedMovementFollowupApplicable ||
entityResolutionClarifiedMovementFollowupApplicable ||
valueFlowGroundedMovementFollowupApplicable ||
documentEvidenceGroundedMovementFollowupApplicable ||
(metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "movement_evidence") ||
@@ -887,11 +1025,15 @@ export function buildAssistantMcpDiscoveryTurnInput(
lifecycleSignal,
valueFlowSignal,
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
entityResolutionSignal
entityResolutionSignal:
entityResolutionSignal &&
!metadataGroundedDocumentLaneApplicable &&
!metadataGroundedMovementLaneApplicable
});
const groundedFollowupEntity = followupSeed.counterparty ?? followupSeed.discoveryEntity;
const entityCandidates = entityResolutionSignal ? [] : [];
if (entityResolutionSignal) {
pushNormalizedEntityResolutionCandidate(entityCandidates, entityResolutionClarificationCandidate);
pushNormalizedEntityResolutionCandidate(entityCandidates, rawEntityCandidate);
for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) {
pushNormalizedEntityResolutionCandidate(entityCandidates, candidate);
@@ -1043,12 +1185,14 @@ export function buildAssistantMcpDiscoveryTurnInput(
explicitIntentCandidate,
followupDiscoverySeedApplicable:
followupDiscoverySeedApplicable ||
Boolean(entityResolutionClarificationCandidate) ||
effectiveMetadataFollowupSeedApplicable ||
metadataAmbiguityLaneClarificationApplicable ||
metadataGroundedMovementLaneApplicable ||
metadataGroundedDocumentLaneApplicable ||
groundedValueFlowFollowupApplicable,
forceDiscoveryOverExplicitIntent:
Boolean(entityResolutionClarificationCandidate) ||
metadataAmbiguityLaneClarificationApplicable ||
metadataGroundedMovementLaneApplicable ||
metadataGroundedDocumentLaneApplicable ||
@@ -1057,7 +1201,10 @@ export function buildAssistantMcpDiscoveryTurnInput(
const hasTurnMeaning = Object.keys(cleanTurnMeaning).length > 0;
const sourceSignal: AssistantMcpDiscoveryTurnInputSource = assistantTurnMeaning
? "assistant_turn_meaning"
: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable || metadataAmbiguityLaneClarificationApplicable
: followupDiscoverySeedApplicable ||
Boolean(entityResolutionClarificationCandidate) ||
effectiveMetadataFollowupSeedApplicable ||
metadataAmbiguityLaneClarificationApplicable
? "followup_context"
: metadataGroundedMovementLaneApplicable
? "followup_context"
@@ -1093,6 +1240,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (rawEntityCandidate) {
pushReason(reasonCodes, "mcp_discovery_entity_scope_from_raw_entity_search");
}
if (entityResolutionClarificationCandidate) {
pushReason(reasonCodes, "mcp_discovery_entity_resolution_clarification_candidate_selected");
}
if (payoutSignal) {
pushReason(reasonCodes, "mcp_discovery_payout_signal_detected");
}
@@ -1123,9 +1273,15 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (entityResolutionGroundedDocumentFollowupApplicable) {
pushReason(reasonCodes, "mcp_discovery_entity_resolution_grounded_document_followup");
}
if (entityResolutionClarifiedDocumentFollowupApplicable) {
pushReason(reasonCodes, "mcp_discovery_entity_resolution_clarified_document_followup");
}
if (entityResolutionGroundedMovementFollowupApplicable) {
pushReason(reasonCodes, "mcp_discovery_entity_resolution_grounded_movement_followup");
}
if (entityResolutionClarifiedMovementFollowupApplicable) {
pushReason(reasonCodes, "mcp_discovery_entity_resolution_clarified_movement_followup");
}
if (valueFlowGroundedDocumentFollowupApplicable) {
pushReason(reasonCodes, "mcp_discovery_value_flow_grounded_document_followup");
}
@@ -13,6 +13,8 @@ import {
readAssistantMcpDiscoveryMetadataAmbiguityDetected,
readAssistantMcpDiscoveryMetadataAmbiguityEntitySets,
readAssistantMcpDiscoveryEntityCandidates,
readAssistantMcpDiscoveryEntityAmbiguityCandidates,
readAssistantMcpDiscoveryEntityResolutionStatus,
readAssistantMcpDiscoveryMetadataRouteFamily,
readAssistantMcpDiscoveryMetadataSelectedEntitySet,
readAddressDebugTemporalScope,
@@ -673,10 +675,18 @@ export function createAssistantTransitionPolicy(deps) {
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryEntityResolutionStatus = readAssistantMcpDiscoveryEntityResolutionStatus(
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryEntityCandidates = readAssistantMcpDiscoveryEntityCandidates(
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryEntityAmbiguityCandidates = readAssistantMcpDiscoveryEntityAmbiguityCandidates(
carryoverSourceDebug,
deps.toNonEmptyString
);
const llmExplicitIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const llmSelectedObjectScopeDetected =
llmPreDecomposeMeta?.predecomposeContract?.semantics?.selected_object_scope_detected === true;
@@ -1013,8 +1023,13 @@ export function createAssistantTransitionPolicy(deps) {
previous_anchor_type: previousAnchorType ?? undefined,
previous_anchor_value: previousAnchor,
previous_discovery_pilot_scope: sourceDiscoveryPilotScope ?? undefined,
previous_discovery_entity_resolution_status: sourceDiscoveryEntityResolutionStatus ?? undefined,
previous_discovery_entity_candidates:
sourceDiscoveryEntityCandidates.length > 0 ? sourceDiscoveryEntityCandidates : undefined,
previous_discovery_entity_ambiguity_candidates:
sourceDiscoveryEntityAmbiguityCandidates.length > 0
? sourceDiscoveryEntityAmbiguityCandidates
: undefined,
previous_discovery_metadata_route_family: sourceDiscoveryMetadataRouteFamily ?? undefined,
previous_discovery_metadata_selected_entity_set: sourceDiscoveryMetadataSelectedEntitySet ?? undefined,
previous_discovery_metadata_ambiguity_detected: sourceDiscoveryMetadataAmbiguityDetected || undefined,