fix(rectification): continue after structured choices
Independent Staging Quality Gate / validate (push) Successful in 12m35s
Independent Staging Quality Gate / publish (push) Successful in 15m16s

This commit is contained in:
Jesse_Chen
2026-08-27 09:23:32 +08:00
parent 1c0ee8db13
commit 518119acbd
23 changed files with 649 additions and 35 deletions
@@ -50,6 +50,7 @@ import { isNearBottom, shouldShowJumpToLatest } from "@/lib/rectification-sticky
import {
CHOICE_ACTION,
STOP_ACTION,
shouldContinueAfterStructuredChoice,
stableChoiceActionKey,
type ChoiceOptionId,
} from "@/lib/rectification-agentic/v9/choice-action";
@@ -285,6 +286,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
const [copiedMessageKey, setCopiedMessageKey] = useState<string | null>(null);
const [regeneratingMessageKey, setRegeneratingMessageKey] = useState<string | null>(null);
const [choiceNonce, setChoiceNonce] = useState(0);
const choiceContinuationPending = useRef(false);
const conversation = useRef<HTMLElement>(null);
const workspace = useRef<HTMLDivElement>(null);
const composer = useRef<HTMLTextAreaElement>(null);
@@ -428,7 +430,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
return () => controller.abort();
}, [applyCaseSnapshot, caseId, sessionId]);
const send = useCallback(async (action: "opening" | "message", messageText: string) => {
const send = useCallback(async (action: "opening" | "message" | "read_only", messageText: string) => {
const trimmed = action === "message" ? messageText.trim() : "";
if ((action === "message" && !trimmed) || busy || readonly) return;
setError("");
@@ -472,7 +474,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
action,
modelId: selectedModelId,
...(action === "message" ? { message: trimmed } : {}),
origin: "typed",
origin: action === "read_only" ? "choice_click" : "typed",
clientActionId: requestId,
}),
});
@@ -805,7 +807,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
}
const narration = typeof payload?.narration === "string" && payload.narration.trim()
? payload.narration.trim()
: "已记录你的选择。正在准备下一步。";
: "已记录你的选择。";
setMessages((current) => current.map((message) => message.renderKey === assistantRenderKey
? {
...message,
@@ -820,6 +822,9 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
]);
onCompleted?.();
await loadCaseSnapshot();
if (shouldContinueAfterStructuredChoice(payload?.nextAction)) {
choiceContinuationPending.current = true;
}
} catch {
setMessages((current) => current.filter((message) => (
message.renderKey !== userRenderKey && message.renderKey !== assistantRenderKey
@@ -843,6 +848,12 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
setPending,
]);
useEffect(() => {
if (!choiceContinuationPending.current || busy || readonly) return;
choiceContinuationPending.current = false;
void send("read_only", "");
}, [busy, readonly, send]);
useEffect(() => {
if (initialTurns.length > 0) {
if (shouldStartOpening) onOpeningConsumed?.();
@@ -89,4 +89,4 @@ export function evidenceWritesAllowed(
export const MAX_RESUMABLE_CASES_PER_USER = 1;
export const RECTIFICATION_SKILL_NAME = "jyotish-birth-time-rectification";
export const RECTIFICATION_SKILL_VERSION = "10.0.11";
export const RECTIFICATION_SKILL_VERSION = "10.0.12";
@@ -85,12 +85,20 @@ export function composeChoiceNarration(input: {
return "已按你的选择先看到当前范围。独立核对尚未完成,这不是最终校正结果。";
}
if (!input.scoring) {
return "已记录你的选择。这是盘外核对,不会改候选分数。正在准备下一步。";
return "已记录你的选择。这是盘外核对,不会改候选分数。";
}
if (input.appliedInference) {
return "已记录你的选择,并更新了候选比较。正在准备下一步。";
return "已记录你的选择,并更新了候选比较。";
}
return "已记录你的选择。正在准备下一步。";
return "已记录你的选择。";
}
export function shouldContinueAfterStructuredChoice(nextAction: unknown): boolean {
if (!nextAction || typeof nextAction !== "object") return false;
const type = (nextAction as { type?: unknown }).type;
return type === "ask_fact_collection"
|| type === "ask_candidate_discriminator"
|| type === "ask_holdout_validation";
}
export function stableChoiceActionKey(focusId: string, optionId: ChoiceOptionId): string {