fix(rectification): unify discriminator question contract and rank by information gain
Python and TypeScript now share a four-option probe contract, persist Focus before asking, and pick the highest-value renderable probe instead of preferring low-gain career events over D24. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,6 +5,11 @@
|
||||
|
||||
import type { AnswerClass, ConflictProbe } from "./types.ts";
|
||||
import { d9StyleLabel, d10StyleLabel } from "../v9/varga-type-tables.ts";
|
||||
import {
|
||||
completeStyleOptions,
|
||||
isRenderableProbe,
|
||||
rankDiscriminatorScore,
|
||||
} from "../v9/probe-question-contract.ts";
|
||||
|
||||
export type ContrastChoiceKind = "existence" | "varga_style" | "event_quality";
|
||||
|
||||
@@ -342,15 +347,134 @@ function vargaDifferencesForPacket(input: {
|
||||
|
||||
export function selectDiscriminatorProbe(
|
||||
packet: CandidateContrastPacket | null | undefined,
|
||||
options?: { askedKeys?: readonly string[]; topCandidateTimes?: readonly string[] },
|
||||
): CandidateDiscriminatorProbe | null {
|
||||
const ranked = (packet?.probes ?? []).filter((probe) => {
|
||||
const ids = new Set(probe.expectedOutcomes.flatMap((row) => [
|
||||
const asked = new Set(options?.askedKeys ?? []);
|
||||
const ranked = (packet?.probes ?? []).flatMap((probe) => {
|
||||
const completed = withCompletedContrastOptions(probe);
|
||||
if (!completed) return [];
|
||||
const ids = [...new Set(completed.expectedOutcomes.flatMap((row) => [
|
||||
...row.supportsCandidateIds,
|
||||
...row.conflictsCandidateIds,
|
||||
]));
|
||||
return probe.expectedOutcomes.length >= 2 && probe.informationGain > 0 && ids.size >= 2;
|
||||
]))];
|
||||
if (!isRenderableProbe({
|
||||
informationGain: completed.informationGain,
|
||||
candidateIds: ids,
|
||||
expectedOutcomeCount: completed.expectedOutcomes.length,
|
||||
choiceKind: completed.choiceKind,
|
||||
styleOptions: completed.styleOptions,
|
||||
})) return [];
|
||||
const askedAlready = asked.has(completed.semanticKey)
|
||||
|| asked.has(completed.candidateSplitHash)
|
||||
|| asked.has(completed.probeId);
|
||||
return [{
|
||||
probe: completed,
|
||||
score: rankDiscriminatorScore({
|
||||
informationGain: completed.informationGain,
|
||||
asked: askedAlready,
|
||||
candidateIds: ids,
|
||||
topCandidateTimes: options?.topCandidateTimes,
|
||||
}),
|
||||
}];
|
||||
}).sort((left, right) => right.score - left.score || right.probe.informationGain - left.probe.informationGain);
|
||||
return ranked[0]?.probe ?? null;
|
||||
}
|
||||
|
||||
function withCompletedContrastOptions(
|
||||
probe: CandidateDiscriminatorProbe,
|
||||
): CandidateDiscriminatorProbe | null {
|
||||
const mapped = probe.styleOptions?.map((item) => ({
|
||||
label: item.label,
|
||||
answer_class: item.answerClass,
|
||||
...(item.sign ? { sign: item.sign } : {}),
|
||||
})) ?? [];
|
||||
const incoming = mapped.length >= 2 ? mapped : [...mapped, ...inferredVargaStyleIncoming(probe)];
|
||||
const choiceKind = effectiveContrastChoiceKind({
|
||||
...probe,
|
||||
styleOptions: incoming.map((item) => ({
|
||||
label: item.label,
|
||||
answerClass: item.answer_class as AnswerClass,
|
||||
...(item.sign ? { sign: item.sign } : {}),
|
||||
})),
|
||||
});
|
||||
return ranked[0] ?? null;
|
||||
const styleOptions = completeStyleOptions({
|
||||
choiceKind,
|
||||
styleOptions: incoming,
|
||||
});
|
||||
if (!styleOptions) return null;
|
||||
const outcomes = withUnsureOutcome(probe.expectedOutcomes);
|
||||
return {
|
||||
...probe,
|
||||
choiceKind,
|
||||
expectedOutcomes: outcomes,
|
||||
styleOptions: styleOptions.map((item) => ({
|
||||
label: item.label,
|
||||
answerClass: item.answer_class,
|
||||
...(item.sign ? { sign: item.sign } : {}),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function inferredVargaStyleIncoming(
|
||||
probe: CandidateDiscriminatorProbe,
|
||||
): Array<{ label: string; answer_class: AnswerClass; sign?: string }> {
|
||||
const parsed = signsFromVargaProbe(probe);
|
||||
if (!parsed) return [];
|
||||
const labelFor = parsed.layer === "d9" ? d9StyleLabel : d10StyleLabel;
|
||||
const classes = ["yes", "weak_yes", "no"] as const;
|
||||
return parsed.signs.slice(0, 3).flatMap((sign, index) => {
|
||||
const label = labelFor(sign);
|
||||
const answerClass = classes[index];
|
||||
if (!label || !answerClass) return [];
|
||||
return [{ label, answer_class: answerClass, sign }];
|
||||
});
|
||||
}
|
||||
|
||||
function signsFromVargaProbe(
|
||||
probe: CandidateDiscriminatorProbe,
|
||||
): { layer: "d9" | "d10"; signs: string[] } | null {
|
||||
const match = probe.semanticKey.match(/^varga\.(d9|d10)\.(.+)$/);
|
||||
const layer = match?.[1] === "d9" || match?.[1] === "d10" ? match[1] : null;
|
||||
const fromKey = match?.[2]
|
||||
?.split(/[|/]/)
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item && !/^\d{1,2}:\d{2}$/.test(item))
|
||||
?? [];
|
||||
const fromOutcomes = probe.expectedOutcomes.flatMap((row) => {
|
||||
const token = row.outcomeId.replace(/^supports_/, "").trim();
|
||||
return token && !/^\d{1,2}:\d{2}$/.test(token) ? [token] : [];
|
||||
});
|
||||
const signs = (fromKey.length >= 2 ? fromKey : fromOutcomes).slice(0, 3);
|
||||
if (!layer || signs.length < 2) return null;
|
||||
return { layer, signs };
|
||||
}
|
||||
|
||||
function effectiveContrastChoiceKind(probe: CandidateDiscriminatorProbe): ContrastChoiceKind {
|
||||
const key = probe.semanticKey;
|
||||
if (probe.choiceKind === "varga_style" && (probe.styleOptions?.length ?? 0) < 2) {
|
||||
if (key.startsWith("varga.d24") || key.startsWith("varga.d5")) return "event_quality";
|
||||
if (key.startsWith("varga.d9") || key.startsWith("varga.d10")) return "varga_style";
|
||||
return "existence";
|
||||
}
|
||||
if (probe.choiceKind === "varga_style" || probe.choiceKind === "event_quality" || probe.choiceKind === "existence") {
|
||||
return probe.choiceKind;
|
||||
}
|
||||
if (key.startsWith("varga.d24") || key.startsWith("varga.d5")) return "event_quality";
|
||||
if (key.startsWith("varga.d9") || key.startsWith("varga.d10")) return "varga_style";
|
||||
return "existence";
|
||||
}
|
||||
|
||||
function withUnsureOutcome(
|
||||
outcomes: readonly ContrastExpectedOutcome[],
|
||||
): readonly ContrastExpectedOutcome[] {
|
||||
const rows = [...outcomes];
|
||||
if (!rows.some((row) => row.outcomeId === "unsure")) {
|
||||
rows.push({ outcomeId: "unsure", supportsCandidateIds: [], conflictsCandidateIds: [] });
|
||||
}
|
||||
if (!rows.some((row) => row.outcomeId === "no") && rows.some((row) => row.outcomeId === "weak_yes")) {
|
||||
rows.push({ outcomeId: "no", supportsCandidateIds: [], conflictsCandidateIds: [] });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
export function conflictProbesFromContrast(
|
||||
@@ -513,15 +637,22 @@ function remainingStyleOptions(
|
||||
split: RemainingVargaSplit,
|
||||
kind: ContrastChoiceKind,
|
||||
): readonly ContrastStyleOption[] | undefined {
|
||||
if (kind !== "varga_style") return undefined;
|
||||
const classes = ["yes", "weak_yes", "no"] as const;
|
||||
const options = split.groups.slice(0, 3).flatMap((group, index) => {
|
||||
const sign = split.signs[index];
|
||||
if (!sign) return [];
|
||||
const label = split.layer === "d10" ? d10StyleLabel(sign) : d9StyleLabel(sign);
|
||||
return [{ label, answerClass: classes[index] ?? "unsure", sign }];
|
||||
});
|
||||
return options.length >= 2 ? uniquifyStyleLabels(options) : undefined;
|
||||
const incoming = kind === "varga_style"
|
||||
? split.groups.slice(0, 3).flatMap((group, index) => {
|
||||
const sign = split.signs[index];
|
||||
if (!sign) return [];
|
||||
const label = split.layer === "d10" ? d10StyleLabel(sign) : d9StyleLabel(sign);
|
||||
const classes = ["yes", "weak_yes", "no"] as const;
|
||||
return [{ label, answer_class: classes[index] ?? "unsure", sign }];
|
||||
})
|
||||
: [];
|
||||
const completed = completeStyleOptions({ choiceKind: kind, styleOptions: incoming });
|
||||
if (!completed) return undefined;
|
||||
return uniquifyStyleLabels(completed.map((item) => ({
|
||||
label: item.label,
|
||||
answerClass: item.answer_class,
|
||||
...(item.sign ? { sign: item.sign } : {}),
|
||||
})));
|
||||
}
|
||||
|
||||
function uniquifyStyleLabels(
|
||||
@@ -558,24 +689,31 @@ function remainingOutcomes(
|
||||
allMinutes: readonly string[],
|
||||
kind: ContrastChoiceKind,
|
||||
): ContrastExpectedOutcome[] {
|
||||
let rows: ContrastExpectedOutcome[];
|
||||
if (kind === "varga_style" && groups.length === 2) {
|
||||
return [
|
||||
rows = [
|
||||
{ outcomeId: "yes", supportsCandidateIds: groups[0], conflictsCandidateIds: groups[1] },
|
||||
{ outcomeId: "weak_yes", supportsCandidateIds: groups[1], conflictsCandidateIds: groups[0] },
|
||||
{ outcomeId: "no", supportsCandidateIds: [], conflictsCandidateIds: [] },
|
||||
{ outcomeId: "unsure", supportsCandidateIds: [], conflictsCandidateIds: [] },
|
||||
];
|
||||
}
|
||||
if (groups.length === 2) {
|
||||
return [
|
||||
} else if (groups.length === 2) {
|
||||
rows = [
|
||||
{ outcomeId: "yes", supportsCandidateIds: groups[0], conflictsCandidateIds: groups[1] },
|
||||
{ outcomeId: "weak_yes", supportsCandidateIds: groups[0], conflictsCandidateIds: groups[1] },
|
||||
{ outcomeId: "no", supportsCandidateIds: groups[1], conflictsCandidateIds: groups[0] },
|
||||
{ outcomeId: "unsure", supportsCandidateIds: [], conflictsCandidateIds: [] },
|
||||
];
|
||||
} else {
|
||||
const classes = ["yes", "weak_yes", "no"] as const;
|
||||
rows = groups.slice(0, 3).map((group, index) => ({
|
||||
outcomeId: classes[index] ?? `group_${index}`,
|
||||
supportsCandidateIds: group,
|
||||
conflictsCandidateIds: allMinutes.filter((time) => !group.includes(time)),
|
||||
}));
|
||||
if (!rows.some((row) => row.outcomeId === "unsure")) {
|
||||
rows.push({ outcomeId: "unsure", supportsCandidateIds: [], conflictsCandidateIds: [] });
|
||||
}
|
||||
}
|
||||
const classes = ["yes", "weak_yes", "no"] as const;
|
||||
return groups.slice(0, 3).map((group, index) => ({
|
||||
outcomeId: classes[index] ?? `group_${index}`,
|
||||
supportsCandidateIds: group,
|
||||
conflictsCandidateIds: allMinutes.filter((time) => !group.includes(time)),
|
||||
}));
|
||||
return rows;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ import { RECTIFICATION_SKILL_NAME, RECTIFICATION_SKILL_VERSION } from "./case-st
|
||||
import { RECTIFICATION_AGENT_TOOLS } from "./public-receipt";
|
||||
import { agentGenerationSettings } from "../../agent-generation-settings.ts";
|
||||
import { toAgentModelFinishReason } from "../../agent-observability.ts";
|
||||
import { decideFromDossier } from "./decision-from-dossier";
|
||||
import { parseAgentChoiceCopy, isPersistedFocusId } from "./choice-card";
|
||||
import {
|
||||
resolveExactSkillPackage,
|
||||
type ResolvedSkillPackageIdentity,
|
||||
@@ -785,6 +787,25 @@ export async function runV9AgentTurn(options: V9AgentRunOptions): Promise<V9Agen
|
||||
return true;
|
||||
};
|
||||
|
||||
const discriminatorInvariant = async (): Promise<{ ok: true } | { ok: false; errorCode: string }> => {
|
||||
try {
|
||||
const latest = await loadV9CaseDossier(accounting, userId, caseId);
|
||||
const decision = decideFromDossier(latest);
|
||||
if (decision.nextAction !== "ask_candidate_discriminator") return { ok: true };
|
||||
const focus = latest.conversationSummary.activeFocus;
|
||||
if (
|
||||
focus
|
||||
&& isPersistedFocusId(focus.id)
|
||||
&& parseAgentChoiceCopy(focus.expectedAnswerSchema)
|
||||
) {
|
||||
return { ok: true };
|
||||
}
|
||||
return { ok: false, errorCode: "state_invariant_failed" };
|
||||
} catch {
|
||||
return { ok: false, errorCode: "state_invariant_failed" };
|
||||
}
|
||||
};
|
||||
|
||||
const completeAttempt = async (): Promise<AttemptOutcome> => {
|
||||
let inputTokens = 0;
|
||||
let outputTokens = 0;
|
||||
@@ -848,7 +869,11 @@ export async function runV9AgentTurn(options: V9AgentRunOptions): Promise<V9Agen
|
||||
if (mapped === "max_steps" || mapped === "provider_error") {
|
||||
return failedAttempt(attemptId, mapped);
|
||||
}
|
||||
if (await flushPersistedPrompt()) return completeAttempt();
|
||||
if (await flushPersistedPrompt()) {
|
||||
const invariant = await discriminatorInvariant();
|
||||
if (!invariant.ok) return failedAttempt(attemptId, invariant.errorCode);
|
||||
return completeAttempt();
|
||||
}
|
||||
if (!answerText.trim()) {
|
||||
try {
|
||||
const latest = await loadV9CaseDossier(accounting, userId, caseId);
|
||||
@@ -863,6 +888,8 @@ export async function runV9AgentTurn(options: V9AgentRunOptions): Promise<V9Agen
|
||||
}
|
||||
}
|
||||
if (!answerText.trim()) return failedAttempt(attemptId, "empty_stream");
|
||||
const invariant = await discriminatorInvariant();
|
||||
if (!invariant.ok) return failedAttempt(attemptId, invariant.errorCode);
|
||||
return completeAttempt();
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
|
||||
import type { AnswerClass } from "../core/types";
|
||||
import { completeStyleOptions, clippedProbeLabel } from "./probe-question-contract";
|
||||
import type { DiscriminatingEventProbe, EventProbeChoiceKind, EventProbeStyleOption } from "./refinement-packet";
|
||||
import type { InternalVargaObservation } from "./varga-observations";
|
||||
|
||||
@@ -235,8 +236,12 @@ function hypothesisFor(
|
||||
): Hypothesis | null {
|
||||
const domain = followupDomain(followup);
|
||||
const probe = pickProbe(probes, domain, followup);
|
||||
const styleOptions = followup.style_options ?? probe?.style_options ?? [];
|
||||
if (!probe?.event_family?.trim()) return null;
|
||||
const styleOptions = completeStyleOptions({
|
||||
choiceKind: followup.choice_kind ?? probe.choice_kind,
|
||||
styleOptions: followup.style_options ?? probe.style_options ?? [],
|
||||
});
|
||||
if (!styleOptions) return null;
|
||||
const period = periodFor(evidence, domain, probes, birthDate, followup);
|
||||
const prompt = eventLockPrompt(period, probe.event_family);
|
||||
const why = probe.user_meaning?.trim() || followup.user_prompt_hint.trim();
|
||||
@@ -297,11 +302,7 @@ function hypothesisKind(
|
||||
}
|
||||
|
||||
function clippedCopy(value: unknown, min: number, max: number): string | null {
|
||||
if (typeof value !== "string") return null;
|
||||
const text = value.trim().replace(/\s+/g, " ");
|
||||
if (text.length < min || text.length > max) return null;
|
||||
if (FORBIDDEN_CHOICE_COPY.test(text)) return null;
|
||||
return text;
|
||||
return clippedProbeLabel(value, min, max);
|
||||
}
|
||||
|
||||
function isAnswerClass(value: unknown): value is AnswerClass {
|
||||
|
||||
@@ -55,14 +55,22 @@ import {
|
||||
type CandidateDiscriminatorProbe,
|
||||
} from "../core/candidate-contrast-packet.ts";
|
||||
import { candidateIdsFromProbe, isValidDistinguishProbe } from "../core/distinguish-contract.ts";
|
||||
import {
|
||||
completeStyleOptions,
|
||||
isRenderableProbe,
|
||||
rankDiscriminatorScore,
|
||||
} from "./probe-question-contract.ts";
|
||||
import type { SessionOutcomeKind } from "./confirmation-gate.ts";
|
||||
import { meetsAcceptanceEventQuality, trainingScoreableGate } from "./evidence-model";
|
||||
import type {
|
||||
DiscriminatingEventProbe,
|
||||
EventProbeDomain,
|
||||
EventProbeStyleOption,
|
||||
NakshatraBoundary,
|
||||
OosBlindPrompt,
|
||||
PrecisionStageId,
|
||||
} from "./refinement-packet";
|
||||
import { EVENT_PROBE_DOMAINS } from "./refinement-packet";
|
||||
import type { InternalVargaObservation } from "./varga-observations";
|
||||
|
||||
|
||||
@@ -108,6 +116,8 @@ export type MethodFollowup = Readonly<{
|
||||
answer_class: string;
|
||||
sign?: string;
|
||||
}>[];
|
||||
selection_score?: number;
|
||||
probe_id?: string;
|
||||
}>;
|
||||
|
||||
export type MethodFollowupPlan = Readonly<{
|
||||
@@ -361,6 +371,159 @@ function remainingConflictProbes(
|
||||
.slice(0, MAX_REVERSE_VERIFY);
|
||||
}
|
||||
|
||||
type RankedDiscriminator = Readonly<{
|
||||
kind: "event" | "contrast";
|
||||
score: number;
|
||||
eventProbe?: DiscriminatingEventProbe;
|
||||
contrastProbe?: CandidateDiscriminatorProbe;
|
||||
styleOptions: NonNullable<ReturnType<typeof completeStyleOptions>>;
|
||||
}>;
|
||||
|
||||
function renderableEventProbe(
|
||||
probe: DiscriminatingEventProbe,
|
||||
askedKeys: ReadonlySet<string>,
|
||||
topCandidateTimes: readonly string[],
|
||||
): RankedDiscriminator | null {
|
||||
const candidateIds = probe.candidate_ids ?? candidateIdsFromProbe(probe);
|
||||
const styleOptions = completeStyleOptions({
|
||||
choiceKind: probe.choice_kind,
|
||||
styleOptions: probe.style_options,
|
||||
});
|
||||
if (!styleOptions || !isValidDistinguishProbe({ ...probe, role: "distinguish" })) return null;
|
||||
if (!isRenderableProbe({
|
||||
informationGain: probe.information_gain,
|
||||
candidateIds,
|
||||
expectedOutcomeCount: probe.expected_outcomes?.length,
|
||||
choiceKind: probe.choice_kind,
|
||||
styleOptions,
|
||||
})) return null;
|
||||
const key = probe.semantic_key ?? `${probe.domain}.${probe.year}`;
|
||||
const asked = askedKeys.has(key) || Boolean(probe.candidate_split_hash && askedKeys.has(probe.candidate_split_hash));
|
||||
return {
|
||||
kind: "event",
|
||||
eventProbe: probe,
|
||||
styleOptions,
|
||||
score: rankDiscriminatorScore({
|
||||
informationGain: probe.information_gain ?? 0,
|
||||
asked,
|
||||
candidateIds,
|
||||
topCandidateTimes,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function renderableContrastProbe(
|
||||
probe: CandidateDiscriminatorProbe,
|
||||
askedKeys: ReadonlySet<string>,
|
||||
topCandidateTimes: readonly string[],
|
||||
): RankedDiscriminator | null {
|
||||
const candidateIds = [...new Set(probe.expectedOutcomes.flatMap((row) => [
|
||||
...row.supportsCandidateIds,
|
||||
...row.conflictsCandidateIds,
|
||||
]))];
|
||||
const styleOptions = completeStyleOptions({
|
||||
choiceKind: probe.choiceKind,
|
||||
styleOptions: probe.styleOptions?.map((item) => ({
|
||||
label: item.label,
|
||||
answer_class: item.answerClass,
|
||||
...(item.sign ? { sign: item.sign } : {}),
|
||||
})),
|
||||
});
|
||||
if (!styleOptions || !isRenderableProbe({
|
||||
informationGain: probe.informationGain,
|
||||
candidateIds,
|
||||
expectedOutcomeCount: probe.expectedOutcomes.length,
|
||||
choiceKind: probe.choiceKind,
|
||||
styleOptions,
|
||||
})) return null;
|
||||
const asked = askedKeys.has(probe.semanticKey)
|
||||
|| askedKeys.has(probe.candidateSplitHash)
|
||||
|| askedKeys.has(probe.probeId);
|
||||
return {
|
||||
kind: "contrast",
|
||||
contrastProbe: probe,
|
||||
styleOptions,
|
||||
score: rankDiscriminatorScore({
|
||||
informationGain: probe.informationGain,
|
||||
asked,
|
||||
candidateIds,
|
||||
topCandidateTimes,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function followupEventFamily(domain: string, kind: string): string {
|
||||
if (kind === "event_quality") return "学业、考试发挥或学习压力出现明显变化";
|
||||
if (kind === "varga_style") {
|
||||
return domain === "relationship" ? "相处方式更接近其中一种" : "做事风格更接近其中一种";
|
||||
}
|
||||
return "这段经历是否发生过";
|
||||
}
|
||||
|
||||
function followupOwnedProbe(
|
||||
item: Omit<MethodFollowup, "must_not_label" | "choice_frame">,
|
||||
): DiscriminatingEventProbe | null {
|
||||
if (!item.style_options?.length) return null;
|
||||
if (!item.domain || !EVENT_PROBE_DOMAINS.includes(item.domain as EventProbeDomain)) return null;
|
||||
const kind = item.choice_kind ?? "existence";
|
||||
const styleOptions: EventProbeStyleOption[] = [];
|
||||
for (const row of item.style_options) {
|
||||
const answer = row.answer_class;
|
||||
if (answer !== "yes" && answer !== "weak_yes" && answer !== "no" && answer !== "unsure") return null;
|
||||
styleOptions.push({
|
||||
label: row.label,
|
||||
answer_class: answer,
|
||||
...(row.sign ? { sign: row.sign } : {}),
|
||||
});
|
||||
}
|
||||
if (styleOptions.length !== 4) return null;
|
||||
return {
|
||||
year: item.probe_year ?? 0,
|
||||
year_label: item.probe_year ? `${item.probe_year} 年前后` : "当前这几个候选",
|
||||
domain: item.domain as EventProbeDomain,
|
||||
event_family: followupEventFamily(item.domain, kind),
|
||||
source: "dasha_activation",
|
||||
tracks: ["vimshottari", "narayana"],
|
||||
tracks_agree: true,
|
||||
unique_minute_claim: false,
|
||||
user_meaning: item.user_prompt_hint,
|
||||
role: "distinguish",
|
||||
phase: "candidate_discriminator",
|
||||
information_gain: item.information_gain,
|
||||
semantic_key: item.semantic_key,
|
||||
candidate_split_hash: item.candidate_split_hash,
|
||||
candidate_ids: item.candidate_ids,
|
||||
expected_outcomes: item.expected_outcomes,
|
||||
choice_kind: kind,
|
||||
style_options: styleOptions,
|
||||
};
|
||||
}
|
||||
|
||||
function rankRenderableDiscriminators(input: {
|
||||
eventProbes: readonly DiscriminatingEventProbe[];
|
||||
contrastProbe: CandidateDiscriminatorProbe | null;
|
||||
askedKeys: ReadonlySet<string>;
|
||||
topCandidateTimes?: readonly string[];
|
||||
}): RankedDiscriminator[] {
|
||||
const top = input.topCandidateTimes ?? [];
|
||||
const rows: RankedDiscriminator[] = [];
|
||||
const seen = new Set<string>();
|
||||
const push = (row: RankedDiscriminator | null) => {
|
||||
if (!row) return;
|
||||
const key = row.eventProbe?.semantic_key
|
||||
?? row.contrastProbe?.semanticKey
|
||||
?? "";
|
||||
if (!key || seen.has(key)) return;
|
||||
seen.add(key);
|
||||
rows.push(row);
|
||||
};
|
||||
for (const probe of input.eventProbes) {
|
||||
push(renderableEventProbe(probe, input.askedKeys, top));
|
||||
}
|
||||
push(input.contrastProbe ? renderableContrastProbe(input.contrastProbe, input.askedKeys, top) : null);
|
||||
return rows.sort((left, right) => right.score - left.score || (right.eventProbe?.information_gain ?? right.contrastProbe?.informationGain ?? 0) - (left.eventProbe?.information_gain ?? left.contrastProbe?.informationGain ?? 0));
|
||||
}
|
||||
|
||||
function coverage(
|
||||
methodId: MethodFollowupId,
|
||||
status: MethodCoverageStatus,
|
||||
@@ -658,6 +821,12 @@ export function buildMethodFollowupPlan(input: {
|
||||
): MethodFollowup => {
|
||||
const base = { ...item, must_not_label: false as const };
|
||||
const attach = forceChoice ?? shouldAttachChoiceFrame(base, input.evidence);
|
||||
const keyed = Boolean(base.semantic_key) && [
|
||||
...(input.eventProbes ?? []),
|
||||
...(input.eventClarificationProbes ?? []),
|
||||
...(input.evidenceCollectionProbes ?? []),
|
||||
].some((probe) => probe.semantic_key === base.semantic_key);
|
||||
const ownedProbe = keyed ? null : followupOwnedProbe(base);
|
||||
return {
|
||||
...base,
|
||||
choice_frame: attach
|
||||
@@ -665,6 +834,7 @@ export function buildMethodFollowupPlan(input: {
|
||||
observations: input.observations,
|
||||
evidence: input.evidence,
|
||||
probes: [
|
||||
...(ownedProbe ? [ownedProbe] : []),
|
||||
...(input.eventProbes ?? []),
|
||||
...(input.eventClarificationProbes ?? []),
|
||||
...(input.evidenceCollectionProbes ?? []),
|
||||
@@ -852,9 +1022,74 @@ export function buildMethodFollowupPlan(input: {
|
||||
...(input.askedProbeKeys ?? []),
|
||||
...askedKeysFromLedgerEvidence(input.evidence),
|
||||
]);
|
||||
const conflictProbe = dashaCovered && meetsAcceptanceEventQuality(input.evidence)
|
||||
? remainingConflictProbes(input.eventProbes, input.evidence, declined, askedKeys)[0] ?? null
|
||||
: null;
|
||||
const rankedDiscriminators = dashaCovered && meetsAcceptanceEventQuality(input.evidence)
|
||||
? rankRenderableDiscriminators({
|
||||
eventProbes: remainingConflictProbes(input.eventProbes, input.evidence, declined, askedKeys),
|
||||
contrastProbe: !candidatesSeparated ? contrastProbe : null,
|
||||
askedKeys,
|
||||
})
|
||||
: [];
|
||||
const bestDiscriminator = rankedDiscriminators[0] ?? null;
|
||||
const followupFromRanked = (ranked: RankedDiscriminator): MethodFollowup => {
|
||||
if (ranked.kind === "event" && ranked.eventProbe) {
|
||||
const conflictProbe = ranked.eventProbe;
|
||||
return makeFollowup({
|
||||
method_id: PROBE_METHOD_ID[conflictProbe.domain],
|
||||
intent: "distinguish_candidates",
|
||||
ask_theme: REVERSE_VERIFY_THEME[conflictProbe.domain],
|
||||
domain: conflictProbe.domain,
|
||||
kind_hint: REVERSE_VERIFY_KIND[conflictProbe.domain],
|
||||
user_prompt_hint: ask(
|
||||
`当前候选时间还分不开。按冲突分钟反推:${conflictProbe.year_label} 是否有${conflictProbe.event_family}。对得上写入账本并重算以筛窗;对不上关闭该问。不要问两套盘哪个更像。不确认唯一分钟。`,
|
||||
REVERSE_VERIFY_VARGA[conflictProbe.domain],
|
||||
),
|
||||
source: "event_probe",
|
||||
information_gain: conflictProbe.information_gain ?? 0,
|
||||
semantic_key: conflictProbe.semantic_key ?? `${conflictProbe.domain}.${conflictProbe.year}`,
|
||||
candidate_split_hash: conflictProbe.candidate_split_hash,
|
||||
probe_year: conflictProbe.year,
|
||||
choice_kind: conflictProbe.choice_kind ?? "existence",
|
||||
candidate_ids: conflictProbe.candidate_ids ?? candidateIdsFromProbe(conflictProbe),
|
||||
expected_outcomes: conflictProbe.expected_outcomes,
|
||||
style_options: ranked.styleOptions,
|
||||
selection_score: ranked.score,
|
||||
probe_id: conflictProbe.semantic_key,
|
||||
}, true, true);
|
||||
}
|
||||
const contrast = ranked.contrastProbe!;
|
||||
const domain = contrastFollowupDomain(contrast.domain);
|
||||
const expectedOutcomes = contrast.expectedOutcomes.map((row) => ({
|
||||
answer_class: row.outcomeId,
|
||||
supports: row.supportsCandidateIds,
|
||||
conflicts: row.conflictsCandidateIds,
|
||||
}));
|
||||
return makeFollowup({
|
||||
method_id: PROBE_METHOD_ID[domain],
|
||||
intent: "distinguish_candidates",
|
||||
ask_theme: REVERSE_VERIFY_THEME[domain],
|
||||
domain,
|
||||
kind_hint: REVERSE_VERIFY_KIND[domain],
|
||||
user_prompt_hint: ask(
|
||||
contrast.question,
|
||||
REVERSE_VERIFY_VARGA[domain],
|
||||
"按候选盘面差异核对前事,不要问两套盘哪个更像。",
|
||||
),
|
||||
source: "event_probe",
|
||||
information_gain: contrast.informationGain,
|
||||
semantic_key: contrast.semanticKey,
|
||||
candidate_split_hash: contrast.candidateSplitHash,
|
||||
probe_year: contrast.year ?? undefined,
|
||||
choice_kind: contrast.choiceKind ?? "existence",
|
||||
candidate_ids: [...new Set(contrast.expectedOutcomes.flatMap((row) => [
|
||||
...row.supportsCandidateIds,
|
||||
...row.conflictsCandidateIds,
|
||||
]))],
|
||||
expected_outcomes: expectedOutcomes,
|
||||
style_options: ranked.styleOptions,
|
||||
selection_score: ranked.score,
|
||||
probe_id: contrast.probeId,
|
||||
}, true, true);
|
||||
};
|
||||
if (!dashaCovered) {
|
||||
next = makeFollowup({
|
||||
method_id: "dasha_events",
|
||||
@@ -869,27 +1104,8 @@ export function buildMethodFollowupPlan(input: {
|
||||
),
|
||||
source: "method_coverage",
|
||||
});
|
||||
} else if (conflictProbe && (!coverageComplete || !candidatesSeparated || (conflictProbe.information_gain ?? 0) >= 0.08)) {
|
||||
next = makeFollowup({
|
||||
method_id: PROBE_METHOD_ID[conflictProbe.domain],
|
||||
intent: "distinguish_candidates",
|
||||
ask_theme: REVERSE_VERIFY_THEME[conflictProbe.domain],
|
||||
domain: conflictProbe.domain,
|
||||
kind_hint: REVERSE_VERIFY_KIND[conflictProbe.domain],
|
||||
user_prompt_hint: ask(
|
||||
`当前候选时间还分不开。按冲突分钟反推:${conflictProbe.year_label} 是否有${conflictProbe.event_family}。对得上写入账本并重算以筛窗;对不上关闭该问。不要问两套盘哪个更像。不确认唯一分钟。`,
|
||||
REVERSE_VERIFY_VARGA[conflictProbe.domain],
|
||||
),
|
||||
source: "event_probe",
|
||||
information_gain: conflictProbe.information_gain ?? 0,
|
||||
semantic_key: conflictProbe.semantic_key ?? `${conflictProbe.domain}.${conflictProbe.year}`,
|
||||
candidate_split_hash: conflictProbe.candidate_split_hash,
|
||||
probe_year: conflictProbe.year,
|
||||
choice_kind: conflictProbe.choice_kind ?? "existence",
|
||||
candidate_ids: conflictProbe.candidate_ids ?? candidateIdsFromProbe(conflictProbe),
|
||||
expected_outcomes: conflictProbe.expected_outcomes,
|
||||
style_options: conflictProbe.style_options,
|
||||
}, true, true);
|
||||
} else if (bestDiscriminator && (!coverageComplete || !candidatesSeparated || bestDiscriminator.score >= 0.08)) {
|
||||
next = followupFromRanked(bestDiscriminator);
|
||||
} else if (!relationshipCovered && !declined.has("relationship")) {
|
||||
next = makeFollowup({
|
||||
method_id: "d9_relationship",
|
||||
@@ -968,31 +1184,6 @@ export function buildMethodFollowupPlan(input: {
|
||||
),
|
||||
source: "method_coverage",
|
||||
});
|
||||
} else if (contrastProbe && !candidatesSeparated) {
|
||||
const domain = contrastFollowupDomain(contrastProbe.domain);
|
||||
next = makeFollowup({
|
||||
method_id: PROBE_METHOD_ID[domain],
|
||||
intent: "distinguish_candidates",
|
||||
ask_theme: REVERSE_VERIFY_THEME[domain],
|
||||
domain,
|
||||
kind_hint: REVERSE_VERIFY_KIND[domain],
|
||||
user_prompt_hint: ask(
|
||||
contrastProbe.question,
|
||||
REVERSE_VERIFY_VARGA[domain],
|
||||
"按候选盘面差异核对前事,不要问两套盘哪个更像。",
|
||||
),
|
||||
source: "event_probe",
|
||||
information_gain: contrastProbe.informationGain,
|
||||
semantic_key: contrastProbe.semanticKey,
|
||||
candidate_split_hash: contrastProbe.candidateSplitHash,
|
||||
probe_year: contrastProbe.year ?? undefined,
|
||||
choice_kind: contrastProbe.choiceKind ?? "existence",
|
||||
style_options: contrastProbe.styleOptions?.map((item) => ({
|
||||
label: item.label,
|
||||
answer_class: item.answerClass,
|
||||
...(item.sign ? { sign: item.sign } : {}),
|
||||
})),
|
||||
}, true, true);
|
||||
} else if (stage === "lagna_frame") {
|
||||
next = makeFollowup({
|
||||
method_id: "dasha_events",
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* Shared probe → choice-card contract.
|
||||
*
|
||||
* Python event probes and TypeScript cards must agree on four options that
|
||||
* cover yes / weak_yes / no / unsure. Existence questions may be completed
|
||||
* by the server; varga-style labels stay dynamic from candidate features.
|
||||
*/
|
||||
|
||||
import type { AnswerClass } from "../core/types";
|
||||
|
||||
export const QUESTION_CONTRACT_VERSION = "probe-question-v1";
|
||||
|
||||
export const ANSWER_CLASSES = ["yes", "weak_yes", "no", "unsure"] as const;
|
||||
|
||||
export type ProbeQuestionKind = "existence" | "event_quality" | "varga_style";
|
||||
|
||||
export type ProbeStyleOption = Readonly<{
|
||||
label: string;
|
||||
answer_class: AnswerClass;
|
||||
sign?: string;
|
||||
}>;
|
||||
|
||||
export const EXISTENCE_STYLE_OPTIONS: readonly ProbeStyleOption[] = [
|
||||
{ label: "明确发生且时间吻合", answer_class: "yes" },
|
||||
{ label: "发生过但程度较弱", answer_class: "weak_yes" },
|
||||
{ label: "明确没有发生", answer_class: "no" },
|
||||
{ label: "这段记不清楚", answer_class: "unsure" },
|
||||
];
|
||||
|
||||
export const QUALITY_STYLE_OPTIONS: readonly ProbeStyleOption[] = [
|
||||
{ label: "发挥明显失常或压力很大", answer_class: "yes" },
|
||||
{ label: "有压力但不算明显失常", answer_class: "weak_yes" },
|
||||
{ label: "发挥正常、没有明显失常", answer_class: "no" },
|
||||
{ label: "这段记不清楚", answer_class: "unsure" },
|
||||
];
|
||||
|
||||
export const VARGA_NONE_STYLE_OPTION: ProbeStyleOption = {
|
||||
label: "都不是这些特质",
|
||||
answer_class: "no",
|
||||
};
|
||||
|
||||
export const UNSURE_STYLE_OPTION: ProbeStyleOption = {
|
||||
label: "这段记不清楚",
|
||||
answer_class: "unsure",
|
||||
};
|
||||
|
||||
const FORBIDDEN_COPY = /外貌|体质|胎记|疤痕|伤疤|身高|体型|(?:[01]?\d|2[0-3]):[0-5]\d/;
|
||||
|
||||
function isAnswerClass(value: unknown): value is AnswerClass {
|
||||
return value === "yes" || value === "weak_yes" || value === "no" || value === "unsure";
|
||||
}
|
||||
|
||||
export function probeQuestionKind(value: unknown): ProbeQuestionKind {
|
||||
if (value === "varga_style" || value === "event_quality") return value;
|
||||
return "existence";
|
||||
}
|
||||
|
||||
export function clippedProbeLabel(value: unknown, min = 4, max = 80): string | null {
|
||||
if (typeof value !== "string") return null;
|
||||
const text = value.trim().replace(/\s+/g, " ");
|
||||
if (text.length < min || text.length > max) return null;
|
||||
if (FORBIDDEN_COPY.test(text)) return null;
|
||||
return text;
|
||||
}
|
||||
|
||||
function catalogFor(kind: ProbeQuestionKind): readonly ProbeStyleOption[] {
|
||||
return kind === "event_quality" ? QUALITY_STYLE_OPTIONS : EXISTENCE_STYLE_OPTIONS;
|
||||
}
|
||||
|
||||
function incomingOption(row: unknown): ProbeStyleOption | null {
|
||||
if (!row || typeof row !== "object" || Array.isArray(row)) return null;
|
||||
const record = row as Record<string, unknown>;
|
||||
const answerClass = record.answer_class ?? record.answerClass;
|
||||
const label = clippedProbeLabel(record.label);
|
||||
if (!label || !isAnswerClass(answerClass)) return null;
|
||||
const sign = typeof record.sign === "string" && record.sign.trim() ? record.sign.trim() : undefined;
|
||||
return sign ? { label, answer_class: answerClass, sign } : { label, answer_class: answerClass };
|
||||
}
|
||||
|
||||
function uniquify(options: readonly ProbeStyleOption[]): ProbeStyleOption[] {
|
||||
const seen = new Set<string>();
|
||||
return options.map((option) => {
|
||||
let label = option.label;
|
||||
if (seen.has(label) && option.sign) label = `${label}(${option.sign})`;
|
||||
if (seen.has(label)) label = `${label}·${option.answer_class}`;
|
||||
seen.add(label);
|
||||
return label === option.label ? option : { ...option, label };
|
||||
});
|
||||
}
|
||||
|
||||
export function completeStyleOptions(input: {
|
||||
choiceKind?: string | null;
|
||||
styleOptions?: readonly unknown[] | null;
|
||||
}): ProbeStyleOption[] | null {
|
||||
const kind = probeQuestionKind(input.choiceKind);
|
||||
const incoming = (input.styleOptions ?? []).flatMap((row) => {
|
||||
const option = incomingOption(row);
|
||||
return option ? [option] : [];
|
||||
});
|
||||
const byClass = new Map<AnswerClass, ProbeStyleOption>();
|
||||
if (kind === "varga_style") {
|
||||
for (const option of incoming) byClass.set(option.answer_class, option);
|
||||
if (!byClass.has("unsure")) byClass.set("unsure", UNSURE_STYLE_OPTION);
|
||||
const scoring = ANSWER_CLASSES.filter((item) => item !== "unsure" && byClass.has(item));
|
||||
if (scoring.length < 2) return null;
|
||||
if (!byClass.has("no")) byClass.set("no", VARGA_NONE_STYLE_OPTION);
|
||||
if (!byClass.has("weak_yes") || !byClass.has("yes")) return null;
|
||||
} else {
|
||||
for (const option of catalogFor(kind)) byClass.set(option.answer_class, option);
|
||||
for (const option of incoming) byClass.set(option.answer_class, option);
|
||||
}
|
||||
const ordered = uniquify(ANSWER_CLASSES.map((answerClass) => byClass.get(answerClass)).filter((item): item is ProbeStyleOption => Boolean(item)));
|
||||
if (!isRenderableStyleOptions(ordered)) return null;
|
||||
return ordered;
|
||||
}
|
||||
|
||||
export function isRenderableStyleOptions(options: readonly ProbeStyleOption[] | null | undefined): boolean {
|
||||
if (!options || options.length !== 4) return false;
|
||||
const classes = new Set(options.map((item) => item.answer_class));
|
||||
const labels = new Set(options.map((item) => item.label));
|
||||
return ANSWER_CLASSES.every((item) => classes.has(item)) && labels.size === 4
|
||||
&& options.every((item) => clippedProbeLabel(item.label) === item.label);
|
||||
}
|
||||
|
||||
export function isRenderableProbe(input: {
|
||||
informationGain?: number | null;
|
||||
candidateIds?: readonly string[] | null;
|
||||
expectedOutcomeCount?: number | null;
|
||||
choiceKind?: string | null;
|
||||
styleOptions?: readonly unknown[] | null;
|
||||
}): boolean {
|
||||
const gain = typeof input.informationGain === "number" && Number.isFinite(input.informationGain)
|
||||
? input.informationGain
|
||||
: 0;
|
||||
if (gain <= 0) return false;
|
||||
if ((input.candidateIds?.length ?? 0) < 2) return false;
|
||||
if ((input.expectedOutcomeCount ?? 0) < 2) return false;
|
||||
return completeStyleOptions({
|
||||
choiceKind: input.choiceKind,
|
||||
styleOptions: input.styleOptions,
|
||||
}) !== null;
|
||||
}
|
||||
|
||||
export function discriminatorPriority(input: {
|
||||
informationGain: number;
|
||||
semanticNovelty?: number;
|
||||
topCandidateCoverage?: number;
|
||||
repetitionPenalty?: number;
|
||||
}): number {
|
||||
const novelty = input.semanticNovelty ?? 1;
|
||||
const coverage = input.topCandidateCoverage ?? 1;
|
||||
const penalty = input.repetitionPenalty ?? 0;
|
||||
return input.informationGain * novelty * coverage - penalty;
|
||||
}
|
||||
|
||||
export function rankDiscriminatorScore(input: {
|
||||
informationGain: number;
|
||||
asked?: boolean;
|
||||
candidateIds?: readonly string[];
|
||||
topCandidateTimes?: readonly string[];
|
||||
}): number {
|
||||
const asked = input.asked === true;
|
||||
const top = input.topCandidateTimes ?? [];
|
||||
const ids = input.candidateIds ?? [];
|
||||
const coverage = top.length === 0
|
||||
? 1
|
||||
: ids.filter((item) => top.includes(item)).length / top.length;
|
||||
return discriminatorPriority({
|
||||
informationGain: input.informationGain,
|
||||
semanticNovelty: asked ? 0.35 : 1,
|
||||
topCandidateCoverage: coverage > 0 ? coverage : 0.25,
|
||||
repetitionPenalty: asked ? 0.45 : 0,
|
||||
});
|
||||
}
|
||||
@@ -9,6 +9,9 @@ import { compactInferenceProjection, previousInferenceFromReceipt } from "./infe
|
||||
import { decideFromDossier } from "./decision-from-dossier";
|
||||
import { evidenceLedgerFingerprint } from "./tool-service";
|
||||
import type { V9CaseDossier } from "./tool-service";
|
||||
import { QUESTION_CONTRACT_VERSION } from "./probe-question-contract";
|
||||
import { RECTIFICATION_SKILL_VERSION } from "./case-status";
|
||||
import { parseAgentChoiceCopy } from "./choice-card";
|
||||
|
||||
export const TURN_DECISION_MAX_BYTES = 6 * 1024;
|
||||
export const TURN_DECISION_RECENT_TURNS = 6;
|
||||
@@ -43,6 +46,7 @@ export function projectTurnDecision(
|
||||
nextAction?: Readonly<Record<string, unknown>> | null;
|
||||
currentQuestion?: Readonly<Record<string, unknown>> | null;
|
||||
followupHint?: string | null;
|
||||
questionContract?: Readonly<Record<string, unknown>> | null;
|
||||
} = {},
|
||||
): Record<string, unknown> {
|
||||
const inference = previousInferenceFromReceipt(dossier.latestResult?.decisionReceipt ?? null);
|
||||
@@ -70,24 +74,26 @@ export function projectTurnDecision(
|
||||
summary: clipText(item.summary, 160),
|
||||
}));
|
||||
const focus = dossier.conversationSummary.activeFocus;
|
||||
const currentQuestion = extras.currentQuestion ?? (focus && parseAgentChoiceCopy(focus.expectedAnswerSchema)
|
||||
? {
|
||||
question_id: focus.questionId,
|
||||
focus_id: focus.id,
|
||||
probe_id: typeof focus.expectedAnswerSchema.probe_id === "string"
|
||||
? focus.expectedAnswerSchema.probe_id
|
||||
: null,
|
||||
prompt: choicePromptFromSchema(focus.expectedAnswerSchema),
|
||||
intent: focus.intent,
|
||||
domain: focus.targetDomain,
|
||||
}
|
||||
: null);
|
||||
const inferenceProjection = compactInferenceProjection(inference);
|
||||
const payload: Record<string, unknown> = {
|
||||
projection: "turn_decision",
|
||||
case_id: dossier.case.caseId,
|
||||
case_revision: inference?.revision ?? 0,
|
||||
status: dossier.case.status,
|
||||
current_question: extras.currentQuestion ?? (focus
|
||||
? {
|
||||
question_id: focus.questionId,
|
||||
focus_id: focus.id,
|
||||
probe_id: typeof focus.expectedAnswerSchema.probe_id === "string"
|
||||
? focus.expectedAnswerSchema.probe_id
|
||||
: null,
|
||||
prompt: choicePromptFromSchema(focus.expectedAnswerSchema),
|
||||
intent: focus.intent,
|
||||
domain: focus.targetDomain,
|
||||
}
|
||||
: null),
|
||||
current_probe: compactInferenceProjection(inference)?.next_probe ?? null,
|
||||
current_question: currentQuestion,
|
||||
current_probe: currentQuestion ? inferenceProjection?.next_probe ?? null : null,
|
||||
candidate_summary: {
|
||||
representative_time: dossier.latestResult?.representativeTime ?? null,
|
||||
selection_allowed: decision.selectionAllowed,
|
||||
@@ -96,7 +102,9 @@ export function projectTurnDecision(
|
||||
candidates,
|
||||
entropy: inference?.entropy ?? null,
|
||||
},
|
||||
inference: compactInferenceProjection(inference),
|
||||
inference: currentQuestion || !inferenceProjection
|
||||
? inferenceProjection
|
||||
: { ...inferenceProjection, next_probe: null },
|
||||
next_action: extras.nextAction ?? {
|
||||
type: decision.nextAction,
|
||||
session_outcome: decision.sessionOutcome,
|
||||
@@ -113,6 +121,14 @@ export function projectTurnDecision(
|
||||
item.status === "draft" || item.status === "pending_confirmation"
|
||||
)).length,
|
||||
},
|
||||
question_contract: extras.questionContract ?? {
|
||||
version: QUESTION_CONTRACT_VERSION,
|
||||
git_sha: process.env.GITHUB_SHA
|
||||
?? process.env.VERCEL_GIT_COMMIT_SHA
|
||||
?? process.env.NEXT_PUBLIC_GIT_COMMIT
|
||||
?? null,
|
||||
skill_version: RECTIFICATION_SKILL_VERSION,
|
||||
},
|
||||
};
|
||||
return enforceTurnDecisionBudget(payload);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user