refactor(rectification): let director agent own interview flow
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
import { z } from "zod";
|
||||
import { clockTimeSchema, evidenceDomainSchema, rectificationAnalysisTraceSchema, rectificationDeploymentModeSchema } from "../rectification-v4/contracts.ts";
|
||||
import { clockTimeSchema, eventKindSchema, eventSubjectSchema, evidenceDomainSchema, relatedPersonSchema, rectificationAnalysisTraceSchema, rectificationDeploymentModeSchema } from "../rectification-v4/contracts.ts";
|
||||
|
||||
const uuid = z.string().uuid();
|
||||
const hash = z.string().regex(/^[a-f0-9]{64}$/);
|
||||
const nonblank = (max: number) => z.string().trim().min(1).max(max);
|
||||
|
||||
export const CURRENT_RECTIFICATION_SKILL_VERSION = "birth-time-rectification-v6" as const;
|
||||
export const CURRENT_RECTIFICATION_PROMPT_VERSION = "rectification-agent-v6-1" as const;
|
||||
export const CURRENT_RECTIFICATION_PROMPT_VERSION = "rectification-director-v1" as const;
|
||||
|
||||
export const rectificationDiagnosticSchema = z.enum([
|
||||
"leave_one_event_out",
|
||||
@@ -17,12 +17,144 @@ export const rectificationDiagnosticSchema = z.enum([
|
||||
]);
|
||||
export type RectificationDiagnostic = z.infer<typeof rectificationDiagnosticSchema>;
|
||||
|
||||
export const rectificationDecisionSchema = z.discriminatedUnion("action", [
|
||||
export const targetDispositionSchema = z.enum([
|
||||
"resolved",
|
||||
"unknown",
|
||||
"declined",
|
||||
"direction_change",
|
||||
"answered_other_event",
|
||||
"unresolved",
|
||||
"not_applicable",
|
||||
]);
|
||||
|
||||
export const evidenceProposalSchema = z.object({
|
||||
operation: z.enum(["create", "revise", "ignore"]),
|
||||
targetEventId: uuid.nullable(),
|
||||
sourceSpan: nonblank(4_000),
|
||||
dateText: nonblank(80).nullable(),
|
||||
proposedSummary: nonblank(1_000),
|
||||
proposedDomain: evidenceDomainSchema,
|
||||
proposedEventKind: eventKindSchema,
|
||||
proposedSubject: eventSubjectSchema,
|
||||
proposedRelatedPerson: relatedPersonSchema.nullable(),
|
||||
confidence: z.enum(["high", "medium", "low"]),
|
||||
}).strict();
|
||||
export type EvidenceProposal = z.infer<typeof evidenceProposalSchema>;
|
||||
|
||||
export const rectificationFocusSchema = z.object({
|
||||
mode: z.enum([
|
||||
"clarify_existing_event",
|
||||
"collect_independent_event",
|
||||
"pair_related_event",
|
||||
"resolve_conflict",
|
||||
"distinguish_candidate_clusters",
|
||||
]),
|
||||
targetEventId: uuid.nullable(),
|
||||
domain: evidenceDomainSchema.nullable(),
|
||||
requestedFacts: z.array(z.enum([
|
||||
"year",
|
||||
"month",
|
||||
"day_or_period",
|
||||
"subject",
|
||||
"event_type",
|
||||
"event_stage",
|
||||
"independent_event",
|
||||
"paired_event",
|
||||
])).max(3),
|
||||
rationaleCodes: z.array(nonblank(80)).max(8),
|
||||
}).strict();
|
||||
export type RectificationFocus = z.infer<typeof rectificationFocusSchema>;
|
||||
|
||||
const directorActionSchema = z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
type: z.literal("ask_question"),
|
||||
focus: rectificationFocusSchema,
|
||||
question: nonblank(240),
|
||||
optionalQuickReplies: z.array(z.object({ label: nonblank(40), value: nonblank(120) }).strict()).max(4),
|
||||
}).strict(),
|
||||
z.object({ type: z.literal("request_diagnostic"), diagnostic: rectificationDiagnosticSchema }).strict(),
|
||||
z.object({ type: z.literal("offer_candidate_range"), snapshotId: uuid }).strict(),
|
||||
z.object({ type: z.literal("stop_low_confidence"), reasonCodes: z.array(nonblank(80)).min(1).max(8) }).strict(),
|
||||
]);
|
||||
|
||||
export const rectificationTurnPlanSchema = z.object({
|
||||
contractVersion: z.literal("rectification-turn-plan-v1"),
|
||||
targetDisposition: targetDispositionSchema,
|
||||
evidenceProposals: z.array(evidenceProposalSchema).max(8),
|
||||
action: directorActionSchema,
|
||||
publicReply: z.object({
|
||||
acknowledgement: nonblank(1_000),
|
||||
candidateCommentary: nonblank(1_000).nullable(),
|
||||
limitation: nonblank(1_000).nullable(),
|
||||
}).strict(),
|
||||
}).strict();
|
||||
export type RectificationTurnPlan = z.infer<typeof rectificationTurnPlanSchema>;
|
||||
|
||||
export const rectificationCaseDossierSchema = z.object({
|
||||
case: z.object({
|
||||
candidateWindow: z.object({ start: clockTimeSchema, end: clockTimeSchema }).strict(),
|
||||
birthDate: nonblank(10),
|
||||
location: z.object({
|
||||
latitude: z.number().finite(),
|
||||
longitude: z.number().finite(),
|
||||
timezoneId: z.string().nullable(),
|
||||
timezoneOffsetHours: z.number().finite(),
|
||||
}).strict(),
|
||||
birthTimeSource: z.string().nullable(),
|
||||
algorithmVersion: nonblank(120),
|
||||
}).strict(),
|
||||
conversation: z.object({
|
||||
recentRawTurns: z.array(z.object({ question: z.string(), answer: z.string() }).strict()).max(12),
|
||||
earlierConversationSummary: z.string().nullable(),
|
||||
}).strict(),
|
||||
eventLedger: z.array(z.object({
|
||||
eventId: uuid,
|
||||
revision: z.number().int().positive(),
|
||||
summary: nonblank(1_000),
|
||||
rawText: nonblank(4_000),
|
||||
domain: evidenceDomainSchema,
|
||||
eventKind: eventKindSchema,
|
||||
subject: eventSubjectSchema,
|
||||
relatedPerson: relatedPersonSchema.nullable(),
|
||||
dateRange: z.object({ start: nonblank(10), end: nonblank(10), precision: nonblank(20), label: nonblank(80) }).strict(),
|
||||
scoreability: nonblank(40),
|
||||
status: z.enum(["active", "superseded", "pending"]),
|
||||
}).strict()),
|
||||
interviewState: z.object({
|
||||
currentTargetEventId: uuid.nullable(),
|
||||
declinedDomains: z.array(evidenceDomainSchema),
|
||||
unresolvedTargets: z.array(uuid),
|
||||
askedTopics: z.array(z.string()).max(50),
|
||||
turnCount: z.number().int().nonnegative(),
|
||||
targetDisposition: targetDispositionSchema,
|
||||
}).strict(),
|
||||
candidateState: z.object({
|
||||
hasSnapshot: z.boolean(),
|
||||
publicRangeAllowed: z.boolean(),
|
||||
rangeChanged: z.boolean(),
|
||||
topClusters: z.array(z.object({ rank: z.number().int(), widthMinutes: z.number().int(), stability: z.enum(["stable", "unstable"]) }).strict()).max(4),
|
||||
contrasts: z.array(z.object({ techniqueLayers: z.array(z.string()), relevantEventIds: z.array(uuid) }).strict()).max(8),
|
||||
eventDiagnostics: z.array(z.object({ eventId: uuid, winnerRetentionRate: z.number(), scoreVariance: z.number() }).strict()).max(100),
|
||||
gateReasons: z.array(z.string()).max(20),
|
||||
currentSnapshotId: uuid.nullable(),
|
||||
}).strict(),
|
||||
capabilities: z.object({
|
||||
supportedDomains: z.array(evidenceDomainSchema),
|
||||
supportedEventKinds: z.array(eventKindSchema),
|
||||
maxQuestionsPerTurn: z.literal(1),
|
||||
maxDiagnosticsPerRun: z.number().int().min(0).max(2),
|
||||
forbiddenPublicClaims: z.array(z.string()),
|
||||
}).strict(),
|
||||
}).strict();
|
||||
export type RectificationCaseDossier = z.infer<typeof rectificationCaseDossierSchema>;
|
||||
|
||||
export const rectificationDecisionSchema = z.union([
|
||||
z.object({
|
||||
action: z.literal("ask_question"),
|
||||
opportunityId: uuid,
|
||||
narrativeFocus: z.array(z.enum(["latest_event", "candidate_change", "date_precision", "uncertainty"])).max(3),
|
||||
}).strict(),
|
||||
z.object({ action: z.literal("ask_question"), focus: rectificationFocusSchema, question: nonblank(240) }).strict(),
|
||||
z.object({ action: z.literal("run_diagnostic"), diagnostic: rectificationDiagnosticSchema }).strict(),
|
||||
z.object({ action: z.literal("offer_candidate_range"), snapshotId: uuid }).strict(),
|
||||
z.object({ action: z.literal("stop_low_confidence"), reasonCodes: z.array(nonblank(80)).min(1).max(8) }).strict(),
|
||||
@@ -336,7 +468,7 @@ export function validateRectificationDecision(input: Readonly<{
|
||||
const issues: string[] = [];
|
||||
if (input.caseId && input.diagnostics.caseId !== input.caseId) issues.push("diagnostics_case_mismatch");
|
||||
if ((input.toolCallCount ?? 0) > (input.maxToolCalls ?? 2)) issues.push("tool_call_budget_exceeded");
|
||||
if (decision.action === "ask_question") {
|
||||
if (decision.action === "ask_question" && "opportunityId" in decision) {
|
||||
const opportunity = input.opportunities.find((item) => item.opportunityId === decision.opportunityId && item.active);
|
||||
if (!opportunity) issues.push("opportunity_not_active");
|
||||
if (opportunity?.kind === "clarify_event_subject" && !opportunity.targetEventId) issues.push("subject_clarification_requires_target_event");
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import path from "node:path";
|
||||
import { Agent } from "@mastra/core/agent";
|
||||
import { z } from "zod";
|
||||
import { defaultLanguageModel, resolveLanguageModel } from "@/mastra/model";
|
||||
import type { CandidateSnapshot, EvidenceDomain, EventKind, LifeEventRevision, PendingEvidence, RectificationV4Case, RectificationV4Turn } from "../rectification-v4/contracts.ts";
|
||||
import type { TargetDisposition } from "../rectification-v4/extraction.ts";
|
||||
import { rectificationCaseDossierSchema, rectificationTurnPlanSchema, type DiagnosticsSummary, type RectificationCaseDossier, type RectificationDiagnostic, type RectificationTurnPlan, type ToolCallTrace } from "./contracts.ts";
|
||||
|
||||
const skillPath = process.env.RECTIFICATION_SKILL_PATH?.trim() || path.resolve(process.cwd(), "..", "skills", "birth-time-rectification");
|
||||
const domains: EvidenceDomain[] = ["education", "relocation", "relationship", "career", "finance", "health_pressure", "family", "other"];
|
||||
const kinds: EventKind[] = ["education_milestone", "relocation", "relationship_start", "relationship_end", "relationship_change", "career_change", "finance_change", "self_health_event", "family_health_event", "family_bereavement", "family_event", "other"];
|
||||
const privatePattern = /(?:[0-9a-f]{8}-[0-9a-f-]{27,}|opportunity(?:id)?|snapshot(?:id)?|event(?:id)?|targetEventId|score|评分|得分|权重|rule[_ -]?id|贡献矩阵|tool[_ -]?call|cluster[_ -]?id)/iu;
|
||||
const exactMinutePattern = /(?:\b(?:[01]?\d|2[0-3]):[0-5]\d\b|(?:凌晨|清晨|上午|中午|下午|傍晚|晚上)?\s*[零〇一二两三四五六七八九十百\d]{1,4}\s*[点时]\s*[零〇一二两三四五六七八九十百\d]{1,4}\s*分)/u;
|
||||
type Generated = Readonly<{ object: unknown; totalUsage?: { inputTokens?: number; outputTokens?: number } | Promise<{ inputTokens?: number; outputTokens?: number }> }>;
|
||||
const regeneratedQuestionSchema = z.object({ question: z.string().trim().min(8).max(500) }).strict();
|
||||
export type RectificationDirectorGenerator = (prompt: string, phase: "evidence" | "final" | "after_diagnostic" | "repair") => Promise<Generated>;
|
||||
|
||||
function diagnosticResult(kind: RectificationDiagnostic, value: DiagnosticsSummary) {
|
||||
switch (kind) {
|
||||
case "leave_one_event_out": return { retentionRate: value.leaveOneEventOutRetentionRate, unstableEventIds: value.unstableEventIds };
|
||||
case "leave_one_domain_out": return { retentionRate: value.leaveOneDomainOutRetentionRate };
|
||||
case "date_sensitivity": return { retentionRate: value.dateSensitivityRetentionRate, events: value.eventDateSensitivity };
|
||||
case "neighbor_stability": return { supportMinutes: value.neighborSupportMinutes, clusterMassRatio: value.clusterMassRatio };
|
||||
case "candidate_split": return { marginPercent: value.primarySecondaryMarginPercent, splits: value.candidateSplits };
|
||||
}
|
||||
}
|
||||
|
||||
export function buildRectificationCaseDossier(input: Readonly<{ caseValue: RectificationV4Case; turns: readonly RectificationV4Turn[]; events: readonly LifeEventRevision[]; pendingEvidence?: readonly PendingEvidence[]; snapshot: CandidateSnapshot | null; previousSnapshot?: CandidateSnapshot | null; diagnostics: DiagnosticsSummary | null; targetDisposition: TargetDisposition; currentTargetEventId: string | null }>): RectificationCaseDossier {
|
||||
const latest = new Map<string, number>();
|
||||
input.events.forEach((event) => latest.set(event.eventId, Math.max(latest.get(event.eventId) ?? 0, event.revision)));
|
||||
const recent = input.turns.slice(-12);
|
||||
return rectificationCaseDossierSchema.parse({
|
||||
case: { candidateWindow: input.caseValue.calculationSpec.candidateRange, birthDate: input.caseValue.calculationSpec.birthDate, location: { latitude: input.caseValue.calculationSpec.latitude, longitude: input.caseValue.calculationSpec.longitude, timezoneId: input.caseValue.calculationSpec.timezoneId ?? null, timezoneOffsetHours: input.caseValue.calculationSpec.timezoneOffsetHours }, birthTimeSource: input.caseValue.calculationSpec.birthTimeSource ?? null, algorithmVersion: input.caseValue.algorithmVersion },
|
||||
conversation: { recentRawTurns: recent.map(({ question, answer }) => ({ question, answer })), earlierConversationSummary: input.turns.length > 12 ? `更早还有 ${input.turns.length - 12} 轮;完整事实以事件账本为准。` : null },
|
||||
eventLedger: input.events.map((event) => ({ eventId: event.eventId, revision: event.revision, summary: event.summary, rawText: event.rawText, domain: event.domain, eventKind: event.eventKind, subject: event.subject, relatedPerson: event.relatedPerson, dateRange: event.dateRange, scoreability: event.scoreability, status: latest.get(event.eventId) === event.revision ? "active" : "superseded" })),
|
||||
interviewState: { currentTargetEventId: input.currentTargetEventId, declinedDomains: [], unresolvedTargets: [...new Set([...(input.currentTargetEventId && ["unresolved", "answered_other_event"].includes(input.targetDisposition) ? [input.currentTargetEventId] : []), ...(input.pendingEvidence ?? []).flatMap((item) => item.targetEventId ? [item.targetEventId] : [])])], askedTopics: input.turns.slice(-50).map((turn) => turn.question), turnCount: input.turns.length, targetDisposition: input.targetDisposition },
|
||||
candidateState: { hasSnapshot: Boolean(input.snapshot), publicRangeAllowed: input.snapshot?.canAcceptRange ?? false, rangeChanged: input.previousSnapshot?.clusters[0]?.startTime !== input.snapshot?.clusters[0]?.startTime || input.previousSnapshot?.clusters[0]?.endTime !== input.snapshot?.clusters[0]?.endTime, topClusters: (input.snapshot?.clusters ?? []).slice(0, 4).map((cluster) => ({ rank: cluster.rank, widthMinutes: cluster.widthMinutes, stability: input.snapshot?.canAcceptRange ? "stable" : "unstable" })), contrasts: (input.diagnostics?.candidateSplits ?? []).map((split) => ({ techniqueLayers: split.techniqueLayers, relevantEventIds: split.eventIds })), eventDiagnostics: (input.diagnostics?.eventDateSensitivity ?? []).map((item) => ({ eventId: item.eventId, winnerRetentionRate: item.winnerRetentionRate, scoreVariance: item.scoreVariance })), gateReasons: input.snapshot?.gateReasons ?? [], currentSnapshotId: input.snapshot?.id ?? null },
|
||||
capabilities: { supportedDomains: domains, supportedEventKinds: kinds, maxQuestionsPerTurn: 1, maxDiagnosticsPerRun: 1, forbiddenPublicClaims: ["exact_birth_minute", "private_scores", "internal_ids", "technique_trace"] },
|
||||
});
|
||||
}
|
||||
|
||||
function fallback(dossier: RectificationCaseDossier, latestAnswer: string): RectificationTurnPlan {
|
||||
if (dossier.candidateState.publicRangeAllowed && dossier.candidateState.currentSnapshotId) return { contractVersion: "rectification-turn-plan-v1", targetDisposition: dossier.interviewState.targetDisposition, evidenceProposals: [], action: { type: "offer_candidate_range", snapshotId: dossier.candidateState.currentSnapshotId }, publicReply: { acknowledgement: "现有事件已经完成本轮复核。", candidateCommentary: "候选范围已通过当前稳定性门槛,可以作为工作范围查看。", limitation: "这仍不是对某个精确出生分钟的确认。" } };
|
||||
const keepTarget = Boolean(dossier.interviewState.currentTargetEventId && ["unresolved", "answered_other_event"].includes(dossier.interviewState.targetDisposition));
|
||||
const latestGroundedEvent = [...dossier.eventLedger].reverse().find((event) => event.status === "active" && (event.rawText === latestAnswer || latestAnswer.includes(event.summary)));
|
||||
const safeSummary = latestGroundedEvent && !privatePattern.test(latestGroundedEvent.summary) && !exactMinutePattern.test(latestGroundedEvent.summary)
|
||||
? latestGroundedEvent.summary.slice(0, 120)
|
||||
: null;
|
||||
return { contractVersion: "rectification-turn-plan-v1", targetDisposition: dossier.interviewState.targetDisposition, evidenceProposals: [], action: { type: "ask_question", focus: { mode: keepTarget ? "clarify_existing_event" : "collect_independent_event", targetEventId: keepTarget ? dossier.interviewState.currentTargetEventId : null, domain: null, requestedFacts: keepTarget ? ["day_or_period"] : ["independent_event", "year"], rationaleCodes: [keepTarget ? "unresolved_current_event" : "need_independent_dated_event"] }, question: keepTarget ? "关于刚才那件事,你还记得它大约发生在哪一年或哪个阶段吗?" : "你还能想到一件发生在你本人身上、时间大致确定的重要经历吗?", optionalQuickReplies: [] }, publicReply: { acknowledgement: safeSummary ? `你提到的“${safeSummary}”已经纳入本轮事件线索。` : latestAnswer.trim() ? "我已按你刚才的描述继续整理事件线索。" : "我们先从真实经历建立事件线索。", candidateCommentary: null, limitation: "在证据通过稳定性门槛前,我不会把某个具体分钟当成确定出生时间。" } };
|
||||
}
|
||||
|
||||
export function validateRectificationTurnPlan(input: Readonly<{ plan: unknown; dossier: RectificationCaseDossier; latestAnswer: string; phase: "evidence" | "final" }>): Readonly<{ plan: RectificationTurnPlan | null; issues: readonly string[] }> {
|
||||
const parsed = rectificationTurnPlanSchema.safeParse(input.plan);
|
||||
if (!parsed.success) return { plan: null, issues: ["turn_plan_schema_invalid"] };
|
||||
const plan = parsed.data;
|
||||
const issues: string[] = [];
|
||||
const known = new Set(input.dossier.eventLedger.map((event) => event.eventId));
|
||||
plan.evidenceProposals.forEach((proposal) => {
|
||||
if (!input.latestAnswer.includes(proposal.sourceSpan)) issues.push("evidence_source_not_in_latest_answer");
|
||||
if (proposal.dateText && !input.latestAnswer.includes(proposal.dateText)) issues.push("evidence_date_not_in_latest_answer");
|
||||
if (proposal.operation === "create" && proposal.targetEventId) issues.push("create_must_not_target_event");
|
||||
if (proposal.operation === "revise" && (!proposal.targetEventId || !known.has(proposal.targetEventId))) issues.push("revision_target_invalid");
|
||||
});
|
||||
const currentTarget = input.dossier.interviewState.currentTargetEventId;
|
||||
if (input.phase === "final") {
|
||||
if (plan.evidenceProposals.length) issues.push("final_plan_contains_evidence");
|
||||
if (plan.targetDisposition !== input.dossier.interviewState.targetDisposition) issues.push("final_target_disposition_changed");
|
||||
} else if (!currentTarget && plan.targetDisposition !== "not_applicable") {
|
||||
issues.push("target_disposition_requires_target");
|
||||
} else if (currentTarget) {
|
||||
const revisedCurrentTarget = plan.evidenceProposals.some((proposal) => proposal.operation === "revise" && proposal.targetEventId === currentTarget);
|
||||
const createdOtherEvent = plan.evidenceProposals.some((proposal) => proposal.operation === "create");
|
||||
if (plan.targetDisposition === "not_applicable") issues.push("target_disposition_missing");
|
||||
if (plan.targetDisposition === "resolved" && !revisedCurrentTarget) issues.push("resolved_target_not_revised");
|
||||
if (plan.targetDisposition === "answered_other_event" && !createdOtherEvent) issues.push("other_event_not_proposed");
|
||||
}
|
||||
const publicText = [plan.publicReply.acknowledgement, plan.publicReply.candidateCommentary, plan.publicReply.limitation, plan.action.type === "ask_question" ? plan.action.question : null].filter(Boolean).join(" ");
|
||||
if (privatePattern.test(publicText)) issues.push("private_detail_exposed");
|
||||
if (exactMinutePattern.test(publicText)) issues.push("exact_minute_claimed");
|
||||
if (plan.action.type === "ask_question") {
|
||||
if ((plan.action.question.match(/[??]/g) ?? []).length > 1) issues.push("multiple_questions");
|
||||
if (plan.action.focus.targetEventId && !known.has(plan.action.focus.targetEventId)) issues.push("focus_target_invalid");
|
||||
if (["unknown", "declined", "direction_change"].includes(plan.targetDisposition) && plan.action.focus.targetEventId === input.dossier.interviewState.currentTargetEventId) issues.push("declined_target_reopened");
|
||||
}
|
||||
if (plan.action.type === "offer_candidate_range" && (!input.dossier.candidateState.publicRangeAllowed || plan.action.snapshotId !== input.dossier.candidateState.currentSnapshotId)) issues.push("candidate_range_gate_failed");
|
||||
return { plan: issues.length ? null : plan, issues };
|
||||
}
|
||||
|
||||
export async function regenerateDirectorQuestion(input: Readonly<{
|
||||
caseValue: RectificationV4Case;
|
||||
currentQuestion: string;
|
||||
latestAnswer: string;
|
||||
acceptedEvents: readonly LifeEventRevision[];
|
||||
focus: Extract<RectificationTurnPlan["action"], { type: "ask_question" }>["focus"];
|
||||
generateQuestion?: (prompt: string, phase: "regenerate" | "repair") => Promise<Generated>;
|
||||
}>): Promise<string> {
|
||||
const model = (input.caseValue.orchestrationModelId ? resolveLanguageModel(input.caseValue.orchestrationModelId) : null) ?? defaultLanguageModel();
|
||||
const agent = model ? new Agent({ id: `rectification-director-regenerate-${model.id}`, name: "Birth Time Rectification Director", model: model.model, skills: [skillPath], instructions: "Rewrite one natural interview question while preserving the supplied structured focus. Do not expose internal ids, scores, tools, or a birth minute. Return only structured output." }) : null;
|
||||
const generate = input.generateQuestion ?? (async (prompt: string) => {
|
||||
if (!agent) throw new Error("director_model_unavailable");
|
||||
return agent.generate(prompt, { structuredOutput: { schema: regeneratedQuestionSchema, jsonPromptInjection: "inline" } });
|
||||
});
|
||||
const validate = (value: unknown) => {
|
||||
const parsed = regeneratedQuestionSchema.safeParse(value);
|
||||
if (!parsed.success) return { question: null, issues: ["question_schema_invalid"] };
|
||||
const issues: string[] = [];
|
||||
if ((parsed.data.question.match(/[??]/g) ?? []).length > 1) issues.push("multiple_questions");
|
||||
if (privatePattern.test(parsed.data.question)) issues.push("private_detail_exposed");
|
||||
if (exactMinutePattern.test(parsed.data.question)) issues.push("exact_minute_claimed");
|
||||
return { question: issues.length ? null : parsed.data.question, issues };
|
||||
};
|
||||
try {
|
||||
const context = { task: "Rewrite the current question without changing its structured focus.", currentQuestion: input.currentQuestion, latestAnswer: input.latestAnswer, focus: input.focus, acceptedEvents: input.acceptedEvents.map(({ summary, domain, eventKind, dateRange }) => ({ summary, domain, eventKind, dateRange })) };
|
||||
let result = validate((await generate(JSON.stringify(context), "regenerate")).object);
|
||||
if (!result.question) result = validate((await generate(JSON.stringify({ ...context, task: "Repair the rejected rewrite once.", validationIssues: result.issues }), "repair")).object);
|
||||
return result.question ?? input.currentQuestion;
|
||||
} catch {
|
||||
return input.currentQuestion;
|
||||
}
|
||||
}
|
||||
|
||||
export async function runRectificationDirector(input: Readonly<{ caseValue: RectificationV4Case; dossier: RectificationCaseDossier; latestAnswer: string; phase: "evidence" | "final"; diagnostics: DiagnosticsSummary; timeoutMs?: number; generatePlan?: RectificationDirectorGenerator }>) {
|
||||
const started = Date.now();
|
||||
const model = (input.caseValue.orchestrationModelId ? resolveLanguageModel(input.caseValue.orchestrationModelId) : null) ?? defaultLanguageModel();
|
||||
const agent = model ? new Agent({ id: `rectification-director-${model.id}`, name: "Birth Time Rectification Director", model: model.model, skills: [skillPath], instructions: "Direct the interview from the complete dossier. Propose every explicit event in the latest answer, choose the current focus, and write the public reply plus at most one natural question. Never write scores, internal ids, profile values, candidate minutes, status, phase, or database mutations. Return strict structured output." }) : null;
|
||||
const generate = input.generatePlan ?? (async (prompt: string) => {
|
||||
if (!agent) throw new Error("director_model_unavailable");
|
||||
return agent.generate(prompt, { abortSignal: AbortSignal.timeout(input.timeoutMs ?? 25_000), structuredOutput: { schema: rectificationTurnPlanSchema, jsonPromptInjection: "inline" } });
|
||||
});
|
||||
let inputTokens = 0, outputTokens = 0;
|
||||
let usageObserved = false;
|
||||
const toolCalls: ToolCallTrace[] = [];
|
||||
const addUsage = async (generated: Generated) => {
|
||||
if (!generated.totalUsage) return;
|
||||
const usage = await generated.totalUsage;
|
||||
inputTokens += Math.max(0, Math.trunc(usage.inputTokens ?? 0));
|
||||
outputTokens += Math.max(0, Math.trunc(usage.outputTokens ?? 0));
|
||||
usageObserved = true;
|
||||
};
|
||||
try {
|
||||
const first = await generate(JSON.stringify({ task: input.phase === "evidence" ? "Interpret the latest answer and propose every explicit event. The action is provisional." : "Choose the final action and public response. evidenceProposals must be empty because staging is complete.", latestAnswer: input.latestAnswer, dossier: input.dossier }), input.phase);
|
||||
await addUsage(first);
|
||||
let candidate = rectificationTurnPlanSchema.parse(first.object);
|
||||
if (input.phase === "final" && candidate.action.type === "request_diagnostic") {
|
||||
const toolStarted = Date.now();
|
||||
const result = diagnosticResult(candidate.action.diagnostic, input.diagnostics);
|
||||
toolCalls.push({ tool: "run_rectification_diagnostics", diagnostic: candidate.action.diagnostic, outcome: "succeeded", durationMs: Date.now() - toolStarted, errorCode: null });
|
||||
const second = await generate(JSON.stringify({ task: "Use the diagnostic result and return a final non-diagnostic action with no evidence proposals.", latestAnswer: input.latestAnswer, dossier: input.dossier, diagnosticResult: result }), "after_diagnostic");
|
||||
await addUsage(second);
|
||||
candidate = rectificationTurnPlanSchema.parse(second.object);
|
||||
}
|
||||
if (input.phase === "final" && candidate.action.type === "request_diagnostic") throw new Error("director_final_plan_not_final");
|
||||
let validated = validateRectificationTurnPlan({ plan: candidate, dossier: input.dossier, latestAnswer: input.latestAnswer, phase: input.phase });
|
||||
if (!validated.plan) {
|
||||
const repaired = await generate(JSON.stringify({ task: "Repair the rejected plan once. Preserve grounded facts, return one safe final plan, and address every validation issue.", latestAnswer: input.latestAnswer, dossier: input.dossier, rejectedPlan: candidate, validationIssues: validated.issues }), "repair");
|
||||
await addUsage(repaired);
|
||||
candidate = rectificationTurnPlanSchema.parse(repaired.object);
|
||||
if (candidate.action.type === "request_diagnostic") throw new Error("director_repair_requested_diagnostic");
|
||||
validated = validateRectificationTurnPlan({ plan: candidate, dossier: input.dossier, latestAnswer: input.latestAnswer, phase: input.phase });
|
||||
}
|
||||
if (!validated.plan) throw new Error(`director_plan_rejected:${validated.issues.join(",")}`);
|
||||
return { plan: validated.plan, mode: "agent" as const, fallbackReason: null, toolCalls, inputTokenCount: usageObserved ? inputTokens : null, outputTokenCount: usageObserved ? outputTokens : null, latencyMs: Date.now() - started };
|
||||
} catch (error) {
|
||||
return { plan: fallback(input.dossier, input.latestAnswer), mode: "deterministic_fallback" as const, fallbackReason: error instanceof Error ? error.message.slice(0, 120) : "director_failed", toolCalls, inputTokenCount: usageObserved ? inputTokens : null, outputTokenCount: usageObserved ? outputTokens : null, latencyMs: Date.now() - started };
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,8 @@ import type { CandidateEngineResult, RectificationV4CandidateEngine } from "../r
|
||||
import { buildCandidateClusters } from "../rectification-v4/candidate-clusters.ts";
|
||||
import type { CandidateSnapshot, RectificationAnalysisTrace, RectificationV4Question } from "../rectification-v4/contracts.ts";
|
||||
import { evaluateDecisionGate } from "../rectification-v4/decision-gate.ts";
|
||||
import { reconcileV4Evidence } from "../rectification-v4/extraction.ts";
|
||||
import { reconcileV4Evidence, stageAgentEvidenceProposals } from "../rectification-v4/extraction.ts";
|
||||
import { buildRectificationCaseDossier, runRectificationDirector } from "./director-agent.ts";
|
||||
import { extractEventWithModel } from "./event-extractor-agent.ts";
|
||||
import { evidenceSetHash } from "../rectification-v4/fingerprints.ts";
|
||||
import { latestEventRevisions, scoreableEvents } from "../rectification-v4/evidence-ledger.ts";
|
||||
@@ -11,7 +12,6 @@ import { projectLegacyV4Turn } from "../rectification-v4/legacy-projector.ts";
|
||||
import type { ClaimedRectificationV4Job } from "../rectification-v4/store.ts";
|
||||
import { deterministicDecision } from "./fallback-policy.ts";
|
||||
import { buildQuestionOpportunities } from "./opportunity-builder.ts";
|
||||
import { renderPublicTurn } from "./renderer-agent.ts";
|
||||
import { runBoundedReasoner } from "./reasoner-agent.ts";
|
||||
import { recordRectificationAgentTelemetry } from "./telemetry.ts";
|
||||
import {
|
||||
@@ -144,36 +144,51 @@ export async function processRectificationAgentTurn(input: Readonly<{
|
||||
};
|
||||
await enterPhase("extracting_evidence");
|
||||
const asOfDate = now.toISOString().slice(0, 10);
|
||||
let reconciliation = claimed.turn.answer ? reconcileV4Evidence({
|
||||
caseId: claimed.case.id,
|
||||
answer: claimed.turn.answer,
|
||||
sourceTurnId: claimed.turn.id,
|
||||
asOfDate,
|
||||
existing: claimed.events,
|
||||
targetEventId: claimed.turn.questionTargetEventId,
|
||||
now,
|
||||
}) : { revisions: [], pending: [], unansweredTargetEventId: null, targetDisposition: "not_applicable" as const };
|
||||
const needsAssistance = claimed.case.deploymentMode !== "v4_legacy" && (
|
||||
const provisionalDisposition = claimed.turn.questionTargetEventId ? "unresolved" as const : "not_applicable" as const;
|
||||
const provisionalDiagnostics = diagnosticsSummarySchema.parse({
|
||||
id: randomUUID(), caseId: claimed.case.id, snapshotId: claimed.case.latestSnapshot?.id ?? randomUUID(),
|
||||
primaryClusterRetentionRate: 0, leaveOneEventOutRetentionRate: 0, leaveOneDomainOutRetentionRate: 0,
|
||||
dateSensitivityRetentionRate: 0, neighborSupportMinutes: 0, primarySecondaryMarginPercent: 0,
|
||||
clusterMassRatio: 0, unstableEventIds: [], mostDiscriminatingLayers: [], eventDateSensitivity: [],
|
||||
candidateSplits: [], calculationHash: hash(claimed.events), createdAt: now.toISOString(),
|
||||
});
|
||||
let evidenceDirector: Awaited<ReturnType<typeof runRectificationDirector>> | null = null;
|
||||
let reconciliation;
|
||||
if (claimed.case.deploymentMode !== "v4_legacy" && claimed.turn.answer) {
|
||||
const dossier = buildRectificationCaseDossier({
|
||||
caseValue: claimed.case, turns: claimed.turns, events: claimed.events, snapshot: claimed.case.latestSnapshot,
|
||||
previousSnapshot: claimed.case.latestSnapshot, diagnostics: null, targetDisposition: provisionalDisposition,
|
||||
currentTargetEventId: claimed.turn.questionTargetEventId,
|
||||
});
|
||||
evidenceDirector = await runRectificationDirector({
|
||||
caseValue: claimed.case, dossier, latestAnswer: claimed.turn.answer, phase: "evidence", diagnostics: provisionalDiagnostics,
|
||||
});
|
||||
reconciliation = claimed.case.deploymentMode === "v5_agent" && evidenceDirector.mode === "agent" && evidenceDirector.plan.evidenceProposals.length
|
||||
? stageAgentEvidenceProposals({ caseId: claimed.case.id, rawText: claimed.turn.answer, sourceTurnId: claimed.turn.id, asOfDate, existing: claimed.events, proposals: evidenceDirector.plan.evidenceProposals, now })
|
||||
: reconcileV4Evidence({ caseId: claimed.case.id, answer: claimed.turn.answer, sourceTurnId: claimed.turn.id, asOfDate, existing: claimed.events, targetEventId: claimed.turn.questionTargetEventId, now });
|
||||
if (claimed.case.deploymentMode === "v5_agent" && evidenceDirector.mode === "agent") {
|
||||
const proposedDisposition = evidenceDirector.plan.targetDisposition;
|
||||
const currentTarget = claimed.turn.questionTargetEventId;
|
||||
const revisedCurrentTarget = Boolean(currentTarget && reconciliation.revisions.some((event) => event.eventId === currentTarget));
|
||||
const addedOtherEvent = reconciliation.revisions.some((event) => event.eventId !== currentTarget);
|
||||
const stagedDispositionIsValid = proposedDisposition !== "resolved" && proposedDisposition !== "answered_other_event"
|
||||
|| proposedDisposition === "resolved" && revisedCurrentTarget
|
||||
|| proposedDisposition === "answered_other_event" && addedOtherEvent;
|
||||
if (stagedDispositionIsValid) reconciliation = { ...reconciliation, targetDisposition: proposedDisposition };
|
||||
}
|
||||
} else {
|
||||
reconciliation = claimed.turn.answer ? reconcileV4Evidence({
|
||||
caseId: claimed.case.id, answer: claimed.turn.answer, sourceTurnId: claimed.turn.id, asOfDate,
|
||||
existing: claimed.events, targetEventId: claimed.turn.questionTargetEventId, now,
|
||||
}) : { revisions: [], pending: [], unansweredTargetEventId: null, targetDisposition: "not_applicable" as const };
|
||||
}
|
||||
const needsAssistance = claimed.case.deploymentMode === "v4_legacy" && (
|
||||
reconciliation.pending.some((event) => event.reasonCode === "event_unparsed")
|
||||
|| reconciliation.revisions.some((event) => event.scoreability === "pending_review" || event.scoreability === "unsupported")
|
||||
);
|
||||
if (needsAssistance) {
|
||||
const assisted = await extractEventWithModel({
|
||||
rawText: claimed.turn.answer,
|
||||
sourceTurnId: claimed.turn.id,
|
||||
asOfDate,
|
||||
modelId: claimed.case.orchestrationModelId,
|
||||
});
|
||||
if (assisted) reconciliation = reconcileV4Evidence({
|
||||
caseId: claimed.case.id,
|
||||
answer: claimed.turn.answer,
|
||||
sourceTurnId: claimed.turn.id,
|
||||
asOfDate,
|
||||
existing: claimed.events,
|
||||
targetEventId: claimed.turn.questionTargetEventId,
|
||||
assistedEvidence: [assisted],
|
||||
now,
|
||||
});
|
||||
const assisted = await extractEventWithModel({ rawText: claimed.turn.answer, sourceTurnId: claimed.turn.id, asOfDate, modelId: claimed.case.orchestrationModelId });
|
||||
if (assisted) reconciliation = reconcileV4Evidence({ caseId: claimed.case.id, answer: claimed.turn.answer, sourceTurnId: claimed.turn.id, asOfDate, existing: claimed.events, targetEventId: claimed.turn.questionTargetEventId, assistedEvidence: [assisted], now });
|
||||
}
|
||||
const extracted = reconciliation.revisions;
|
||||
const events = latestEventRevisions([...claimed.events, ...extracted]);
|
||||
@@ -336,6 +351,95 @@ export async function processRectificationAgentTurn(input: Readonly<{
|
||||
createdAt: now.toISOString(),
|
||||
});
|
||||
|
||||
if (claimed.case.deploymentMode !== "v4_legacy") {
|
||||
await enterPhase("planning_question");
|
||||
const dossier = buildRectificationCaseDossier({
|
||||
caseValue: claimed.case, turns: claimed.turns, events, pendingEvidence: reconciliation.pending,
|
||||
snapshot, previousSnapshot: claimed.case.latestSnapshot, diagnostics,
|
||||
targetDisposition: reconciliation.targetDisposition, currentTargetEventId: claimed.turn.questionTargetEventId,
|
||||
});
|
||||
await enterPhase("reasoning");
|
||||
const directed = await runRectificationDirector({
|
||||
caseValue: claimed.case, dossier, latestAnswer: claimed.turn.answer, phase: "final", diagnostics: safeDiagnostics,
|
||||
});
|
||||
const plan = directed.plan;
|
||||
const action = plan.action;
|
||||
if (action.type === "request_diagnostic") {
|
||||
throw new Error("rectification_director_diagnostic_loop_incomplete");
|
||||
}
|
||||
const decision = action.type === "ask_question"
|
||||
? { action: "ask_question" as const, focus: action.focus, question: action.question }
|
||||
: action.type === "offer_candidate_range"
|
||||
? { action: "offer_candidate_range" as const, snapshotId: action.snapshotId }
|
||||
: { action: "stop_low_confidence" as const, reasonCodes: action.reasonCodes };
|
||||
const validatedDecision: ValidatedDecision = {
|
||||
decision,
|
||||
mode: directed.mode,
|
||||
validationIssues: directed.fallbackReason ? [directed.fallbackReason] : [],
|
||||
selectedOpportunity: null,
|
||||
};
|
||||
await enterPhase("rendering");
|
||||
finishPhase();
|
||||
for (const call of directed.toolCalls) analysisToolCalls.push({
|
||||
category: "agent_diagnostic", label: call.diagnostic ? diagnosticLabels[call.diagnostic] : "只读诊断",
|
||||
outcome: call.outcome, durationMs: call.durationMs,
|
||||
});
|
||||
const publicMessage: StoredPublicMessage = {
|
||||
acknowledgement: plan.publicReply.acknowledgement,
|
||||
candidateUpdate: plan.publicReply.candidateCommentary,
|
||||
limitation: plan.publicReply.limitation,
|
||||
question: action.type === "ask_question" ? action.question : null,
|
||||
analysisTrace: {
|
||||
status: "completed", stages, toolCalls: analysisToolCalls, techniques: publicRectificationTechniques(engineResult),
|
||||
reasoningSummary: null, reasoningSource: "none",
|
||||
},
|
||||
};
|
||||
const targetEvent = action.type === "ask_question" && action.focus.targetEventId
|
||||
? events.find((event) => event.eventId === action.focus.targetEventId) ?? null
|
||||
: null;
|
||||
const nextQuestion: RectificationV4Question | null = action.type === "ask_question" ? {
|
||||
id: randomUUID(),
|
||||
domain: action.focus.domain ?? targetEvent?.domain ?? "other",
|
||||
targetEventId: action.focus.targetEventId,
|
||||
prompt: action.question,
|
||||
recallCost: "medium",
|
||||
reason: action.focus.rationaleCodes.join(",").slice(0, 240) || "agent_directed_focus",
|
||||
} : null;
|
||||
const status = action.type === "offer_candidate_range" ? "range_ready" as const
|
||||
: action.type === "stop_low_confidence" ? "paused" as const
|
||||
: "awaiting_answer" as const;
|
||||
const totalInput = [evidenceDirector?.inputTokenCount, directed.inputTokenCount].filter((value): value is number => value !== null && value !== undefined).reduce((sum, value) => sum + value, 0);
|
||||
const totalOutput = [evidenceDirector?.outputTokenCount, directed.outputTokenCount].filter((value): value is number => value !== null && value !== undefined).reduce((sum, value) => sum + value, 0);
|
||||
const fallbackReason = [evidenceDirector?.fallbackReason, directed.fallbackReason].filter(Boolean).join(";").slice(0, 120) || null;
|
||||
const agentRun: AgentRun = {
|
||||
id: randomUUID(), caseId: claimed.case.id, jobId: claimed.job.id, caseVersion: claimed.case.version,
|
||||
modelId: claimed.case.orchestrationModelId, skillVersion: claimed.case.skillVersion, promptVersion: claimed.case.promptVersion,
|
||||
deploymentMode: claimed.case.deploymentMode, deploymentSha: process.env.DEPLOYMENT_SHA?.trim() || null,
|
||||
decision, validatedDecision, toolCalls: [...directed.toolCalls], fallbackReason,
|
||||
inputTokenCount: totalInput || null, outputTokenCount: totalOutput || null,
|
||||
latencyMs: Math.min(300_000, (evidenceDirector?.latencyMs ?? 0) + directed.latencyMs), createdAt: now.toISOString(),
|
||||
};
|
||||
if (claimed.case.deploymentMode === "v5_shadow") {
|
||||
const legacy = projectLegacyV4Turn({
|
||||
events,
|
||||
newEvents: extracted,
|
||||
attemptedRefinementEventIds: claimed.attemptedRefinementEventIds,
|
||||
latestAnswer: claimed.turn.answer,
|
||||
snapshot,
|
||||
});
|
||||
return {
|
||||
newEventRevisions: extracted, pendingEvidence: [...reconciliation.pending], snapshot, diagnostics, featureSnapshot,
|
||||
validatedDecision, publicMessage: { ...legacy.publicMessage, analysisTrace: publicMessage.analysisTrace },
|
||||
nextQuestion: legacy.nextQuestion, agentRun, status: legacy.status, phase: legacy.phase,
|
||||
};
|
||||
}
|
||||
return {
|
||||
newEventRevisions: extracted, pendingEvidence: [...reconciliation.pending], snapshot, diagnostics, featureSnapshot,
|
||||
validatedDecision, publicMessage, nextQuestion, agentRun, status,
|
||||
phase: status === "awaiting_answer" ? "collecting_evidence" as const : "complete" as const,
|
||||
};
|
||||
}
|
||||
|
||||
await enterPhase("planning_question");
|
||||
const opportunities = buildQuestionOpportunities({
|
||||
caseId: claimed.case.id,
|
||||
@@ -394,7 +498,7 @@ export async function processRectificationAgentTurn(input: Readonly<{
|
||||
}
|
||||
const finalDecision = validation.decision;
|
||||
if (!finalDecision) throw new Error("rectification_v5_fallback_validation_failed");
|
||||
const selectedOpportunity = finalDecision.action === "ask_question"
|
||||
const selectedOpportunity = finalDecision.action === "ask_question" && "opportunityId" in finalDecision
|
||||
? opportunities.find((item) => item.opportunityId === finalDecision.opportunityId) ?? null
|
||||
: null;
|
||||
const validatedDecision: ValidatedDecision = {
|
||||
@@ -412,37 +516,8 @@ export async function processRectificationAgentTurn(input: Readonly<{
|
||||
latestAnswer: claimed.turn.answer,
|
||||
snapshot,
|
||||
});
|
||||
const agentVisible = claimed.case.deploymentMode === "v5_agent";
|
||||
let rendererRealization: "model_validated" | "server_fallback" | null = null;
|
||||
let rendererFallbackReason: "model_unavailable" | "question_rejected" | "model_failed" | null = null;
|
||||
const renderedMessage = agentVisible
|
||||
? await renderPublicTurn({
|
||||
caseValue: claimed.case,
|
||||
latestAnswer: claimed.turn.answer,
|
||||
acceptedEvents: extracted,
|
||||
pendingEvidence: reconciliation.pending,
|
||||
snapshot,
|
||||
previousSnapshot: claimed.case.latestSnapshot,
|
||||
validated: validatedDecision,
|
||||
onRealization: (outcome) => {
|
||||
rendererRealization = outcome.mode;
|
||||
rendererFallbackReason = outcome.reason;
|
||||
},
|
||||
})
|
||||
: legacyProjection.publicMessage;
|
||||
const renderedMessage = legacyProjection.publicMessage;
|
||||
finishPhase();
|
||||
if (agentVisible && rendererRealization) {
|
||||
stages.push({
|
||||
phase: "rendering",
|
||||
label: rendererRealization === "model_validated"
|
||||
? "自然语言问题已通过安全校验"
|
||||
: rendererFallbackReason === "question_rejected"
|
||||
? "模型问题未通过安全校验,已使用服务器安全问题"
|
||||
: "模型回复不可用,已使用服务器安全问题",
|
||||
status: "completed",
|
||||
durationMs: null,
|
||||
});
|
||||
}
|
||||
for (const call of reasoned.toolCalls) {
|
||||
analysisToolCalls.push({
|
||||
category: "agent_diagnostic",
|
||||
@@ -453,7 +528,7 @@ export async function processRectificationAgentTurn(input: Readonly<{
|
||||
}
|
||||
const reasoningSummary = reasoned.mode === "agent" && !fallbackReason ? reasoned.reasoningSummary : null;
|
||||
const analysisTrace: RectificationAnalysisTrace = {
|
||||
status: claimed.case.deploymentMode === "v4_legacy" ? "legacy" : "completed",
|
||||
status: "legacy",
|
||||
stages,
|
||||
toolCalls: analysisToolCalls,
|
||||
techniques: publicRectificationTechniques(engineResult),
|
||||
@@ -461,28 +536,9 @@ export async function processRectificationAgentTurn(input: Readonly<{
|
||||
reasoningSource: reasoningSummary ? "provider_summary" : "none",
|
||||
};
|
||||
const publicMessage: StoredPublicMessage = { ...renderedMessage, analysisTrace };
|
||||
const nextQuestion = agentVisible && selectedOpportunity ? {
|
||||
id: randomUUID(),
|
||||
domain: selectedOpportunity.domain,
|
||||
targetEventId: selectedOpportunity.targetEventId,
|
||||
prompt: publicMessage.question ?? selectedOpportunity.fallbackPrompt,
|
||||
recallCost: selectedOpportunity.privacyCost >= .2
|
||||
? "high" as const
|
||||
: selectedOpportunity.recallEase < .6
|
||||
? "medium" as const
|
||||
: "low" as const,
|
||||
reason: selectedOpportunity.reason,
|
||||
} : agentVisible ? null : legacyProjection.nextQuestion;
|
||||
const status = agentVisible
|
||||
? finalDecision.action === "offer_candidate_range"
|
||||
? "range_ready" as const
|
||||
: finalDecision.action === "stop_low_confidence"
|
||||
? "paused" as const
|
||||
: "awaiting_answer" as const
|
||||
: legacyProjection.status;
|
||||
const phase = agentVisible
|
||||
? status === "awaiting_answer" ? "collecting_evidence" as const : "complete" as const
|
||||
: legacyProjection.phase;
|
||||
const nextQuestion = legacyProjection.nextQuestion;
|
||||
const status = legacyProjection.status;
|
||||
const phase = legacyProjection.phase;
|
||||
|
||||
const agentRun: AgentRun = {
|
||||
id: randomUUID(),
|
||||
|
||||
Reference in New Issue
Block a user