fix(rectification): expose agentic route and runtime trace

This commit is contained in:
Jesse_Chen
2026-08-01 18:18:43 +08:00
parent 24ab4284b7
commit 7abc053278
13 changed files with 212 additions and 28 deletions
@@ -1,7 +1,8 @@
"use client";
import { useEffect, useState } from "react";
import { loadActiveRectificationV4 } from "../lib/rectification-v4/client.ts";
import { loadActiveRectificationV4, transitionRectificationV4 } from "../lib/rectification-v4/client.ts";
import type { RectificationV4ApiResponse } from "../lib/rectification-v4/contracts.ts";
import type { PublicLanguageModel } from "../lib/public-models.ts";
import { AgenticRectificationChat } from "./rectification-agentic-chat.tsx";
import { ChatMessageRow } from "./chat-message-row.tsx";
@@ -24,19 +25,24 @@ export type ConversationalBirthTimeRectificationProps = Readonly<{
/**
* Birth-time rectification surface.
*
* Resumes an existing v4 evidence case when one is still in progress (so users
* never lose a saved candidate range), and otherwise opens the agentic chat
* where the LLM drives the full Jyotish rectification methodology with the
* engine as its computation layer.
* Lets the user explicitly continue or end an existing v4 evidence case, and
* otherwise opens the agentic chat where the LLM drives the full Jyotish
* rectification methodology with the engine as its computation layer.
*/
export function ConversationalBirthTimeRectification(props: ConversationalBirthTimeRectificationProps) {
const [mode, setMode] = useState<"loading" | "v4" | "agentic">("loading");
const [mode, setMode] = useState<"loading" | "choice" | "v4" | "agentic">("loading");
const [existing, setExisting] = useState<RectificationV4ApiResponse | null>(null);
const [switching, setSwitching] = useState(false);
const [switchError, setSwitchError] = useState("");
useEffect(() => {
let mounted = true;
void (async () => {
const existing = await loadActiveRectificationV4().catch(() => null);
if (mounted) setMode(existing ? "v4" : "agentic");
if (mounted) {
setExisting(existing);
setMode(existing ? "choice" : "agentic");
}
})();
return () => { mounted = false; };
}, []);
@@ -52,8 +58,48 @@ export function ConversationalBirthTimeRectification(props: ConversationalBirthT
);
}
if (mode === "choice" && existing) {
const startAgentic = async () => {
setSwitching(true);
setSwitchError("");
try {
await transitionRectificationV4(existing.case.id, existing.case.version, "abandon");
setMode("agentic");
} catch {
setSwitchError("无法结束旧版校正,请稍后再试。");
} finally {
setSwitching(false);
}
};
return (
<>
<section className="conversation" aria-label="生时校正版本选择" aria-busy={switching}>
<div className="message-list" aria-live="polite">
<ChatMessageRow
message={{
role: "assistant",
text: "检测到一段尚未结束的旧版生时校正。你可以继续保留进度,或结束旧版并使用新版 Agent 重新开始。",
renderKey: "rectification-version-choice",
state: "settled",
}}
/>
{switchError && <p className="error-message" role="alert">{switchError}</p>}
</div>
</section>
<div className="composer-wrap">
<div className="composer-suggestions" aria-label="选择生时校正版本">
<button type="button" disabled={switching} onClick={() => setMode("v4")}>继续旧版校正</button>
<button type="button" disabled={switching} onClick={() => void startAgentic()}>
{switching ? "正在结束旧版校正…" : "结束旧版并使用新版 Agent"}
</button>
</div>
</div>
</>
);
}
if (mode === "v4") {
return <RectificationV4Panel {...props} />;
return <RectificationV4Panel {...props} onUseAgentic={() => setMode("agentic")} />;
}
return <AgenticRectificationChat {...props} />;
@@ -28,6 +28,7 @@ type RectificationV4PanelProps = Readonly<{
continuationPending?: boolean;
onPendingChange?: (pending: boolean) => void;
onContinueOriginalQuestion?: (continuation: RectificationV4Continuation) => void;
onUseAgentic?: () => void;
}>;
type RectificationChatMessageView = ChatMessageView & Readonly<{
@@ -358,8 +359,9 @@ export function RectificationV4Panel(props: RectificationV4PanelProps) {
&& handoff?.status === "pending"
&& props.onContinueOriginalQuestion,
);
const canUseAgentic = Boolean(caseValue && caseValue.status !== "abandoned" && props.onUseAgentic);
const showControls = Boolean(caseValue && caseValue.status !== "abandoned" && (
canAnswer || canAcceptRange || canContinue || caseValue.status === "paused"
canAnswer || canAcceptRange || canContinue || caseValue.status === "paused" || canUseAgentic
));
useEffect(() => {
@@ -410,6 +412,11 @@ export function RectificationV4Panel(props: RectificationV4PanelProps) {
});
}
async function startAgentic() {
const result = await controller.abandon();
if (result) props.onUseAgentic?.();
}
return (
<>
<section className="conversation" aria-label="生时校正对话" aria-busy={processing || controller.pending}>
@@ -482,6 +489,7 @@ export function RectificationV4Panel(props: RectificationV4PanelProps) {
</div>
);
})}
{controller.data?.runtimeTrace && <RectificationRuntimeDetails trace={controller.data.runtimeTrace} />}
{controller.error && <p className="error-message" role="alert">{controller.error}</p>}
<div ref={conversationEnd} />
</div>
@@ -489,7 +497,7 @@ export function RectificationV4Panel(props: RectificationV4PanelProps) {
{showControls && (
<div className="composer-wrap">
{(canAcceptRange || canContinue || caseValue?.status === "paused") && (
{(canAcceptRange || canContinue || caseValue?.status === "paused" || canUseAgentic) && (
<div className="composer-suggestions" aria-label="生时校正操作">
{canAcceptRange && (
<button type="button" disabled={controller.pending} onClick={() => void controller.acceptRange()}>
@@ -506,6 +514,11 @@ export function RectificationV4Panel(props: RectificationV4PanelProps) {
继续校正
</button>
)}
{canUseAgentic && (
<button type="button" disabled={controller.pending} onClick={() => void startAgentic()}>
结束旧版并使用新版 Agent
</button>
)}
</div>
)}
@@ -544,3 +557,28 @@ export function RectificationV4Panel(props: RectificationV4PanelProps) {
</>
);
}
function RectificationRuntimeDetails({ trace }: Readonly<{ trace: RectificationV4ApiResponse["runtimeTrace"] }>) {
const rows = [
["Runtime", trace.deploymentMode, "当前校正路由"],
["Execution", trace.executionMode, trace.fallbackCode ?? "无 fallback code"],
["Model", trace.modelId ?? "default / unavailable", "本轮实际选择"],
["Skill", trace.skillVersion, "已注册版本"],
["Deployment SHA", trace.deploymentSha ?? "unknown", "当前部署"],
] as const;
return (
<details className="evidence-audit-panel" open={trace.executionMode === "deterministic_fallback"}>
<summary>运行信息 · {trace.executionMode}</summary>
<div className="evidence-audit-table" role="table" aria-label="生时校正运行信息">
{rows.map(([label, value, note]) => (
<div className="evidence-audit-row" role="row" key={label}>
<span role="cell">{label}</span>
<b role="cell">{value}</b>
<small role="cell">{note}</small>
</div>
))}
</div>
</details>
);
}