fix(consult): fold the technique audit out of the spoken answer
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled

Keep comparative tables in chat, but hide the long audit behind a collapsed control so the reply stays readable.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-19 15:29:04 +08:00
parent 7c7a25773c
commit e635c40224
18 changed files with 590 additions and 28 deletions
+7 -1
View File
@@ -641,7 +641,7 @@ export async function POST(request: Request) {
? generalDailyContext
? "当前是无出生分钟的公共今日趋势咨询。可依据服务器提供的公共 Panchanga 摘要回答,但不得计算或推断个人星盘;不得补 00:00、时段中点或任何候选分钟。"
: "当前是用户明确选择的无出生分钟一般咨询。不得计算或推断个人星盘;不得补 00:00、时段中点或任何候选分钟。"
: "先加载 Jyotish Skill;如需新的个人星盘结论,必须调用服务器绑定的排盘工具。请用口语直接回答;回答末尾附上技法审计表,不要复述内部字段。",
: "先加载 Jyotish Skill;如需新的个人星盘结论,必须调用服务器绑定的排盘工具。请用口语直接回答;不要把技法审计表写进正文,不要复述内部字段。",
generalDailyContextPrompt(generalDailyContext),
resolvedQuestion.modelQuestion,
].filter(Boolean).join("\n"),
@@ -691,6 +691,9 @@ export async function POST(request: Request) {
stepBudget: consultationStepBudgetReceipt(state),
workflow: workflowReceipt,
techniqueTruth: generalDailyContext ? "public-panchanga-only" : "not-applicable",
...(state.techniqueAuditTable?.length
? { techniqueAuditTable: state.techniqueAuditTable }
: {}),
});
return streamAgentResponse({
runId: requestId,
@@ -774,6 +777,9 @@ export async function POST(request: Request) {
stepBudget: consultationStepBudgetReceipt(state),
workflow: state.workflowReceipt ?? workflowReceipt,
techniqueTruth: state.techniqueTruth ?? "unknown",
...(state.techniqueAuditTable?.length
? { techniqueAuditTable: state.techniqueAuditTable }
: {}),
});
return streamAgentResponse({
runId: requestId,
+131 -3
View File
@@ -195,8 +195,46 @@ button:disabled { cursor: default; opacity: .45; }
.raw-evidence-receipt summary { padding: var(--space-1) 0; color: var(--color-ink-tertiary); font-size: 12px; }
.raw-evidence-receipt pre { max-height: 180px; overflow: auto; margin: var(--space-2) 0 0; padding: var(--space-3); border-radius: var(--radius-md); background: var(--color-canvas); color: var(--color-ink-secondary); font-size: 11px; line-height: 1.5; }
.message-markdown p { margin: 0 0 14px; }
.message-markdown ul, .message-markdown ol { margin: 10px 0 16px; padding-left: 24px; }
.message-markdown li { margin: 6px 0; }
.message-markdown ul, .message-markdown ol { margin: 8px 0 18px; padding-inline-start: 1.15em; }
.message-markdown li { margin: 0; padding-block: 4px; padding-inline-start: 4px; }
.message-markdown li::marker { color: var(--color-ink-tertiary); }
.message-markdown li > p,
.message-markdown th > p,
.message-markdown td > p { margin: 0; }
.message-markdown li > p + p { margin-top: 6px; }
.message-answer { min-width: 0; }
.markdown-table {
max-width: 100%;
margin: 14px 0 18px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.markdown-table table {
width: 100%;
min-width: 0;
border-collapse: collapse;
font-size: var(--type-body-sm);
line-height: 1.55;
}
.markdown-table th,
.markdown-table td {
padding: 8px 10px 8px 0;
border-bottom: 1px solid color-mix(in srgb, var(--color-border) 72%, transparent);
vertical-align: top;
text-align: start;
overflow-wrap: anywhere;
}
.markdown-table thead th {
color: var(--color-ink-secondary);
font-size: var(--type-overline);
font-weight: 650;
letter-spacing: .02em;
}
.markdown-table tbody th {
font-weight: 550;
}
.markdown-table tr:last-child th,
.markdown-table tr:last-child td { border-bottom: 0; }
.agent-activity-status { min-height: 24px; display: flex; align-items: center; gap: var(--space-2); color: var(--color-ink-tertiary); font-size: var(--type-body-sm); line-height: 1.5; }
.agent-activity-status canvas { flex: 0 0 auto; }
.agent-activity-status__text {
@@ -223,7 +261,7 @@ button:disabled { cursor: default; opacity: .45; }
from { background-position: 120% 0; }
to { background-position: -120% 0; }
}
.agent-activity-status + .message-markdown { margin-top: var(--space-2); }
.agent-activity-status + .message-answer { margin-top: var(--space-2); }
.composer textarea::placeholder { color: var(--color-ink-tertiary); }
.composer button svg { width: 19px; height: 19px; }
.dialog-close svg { width: 19px; height: 19px; }
@@ -720,6 +758,86 @@ button:disabled { cursor: default; opacity: .45; }
.message-markdown strong { color: var(--color-ink); font-weight: 500; }
.message-markdown a { text-underline-offset: 3px; color: var(--color-action); }
.message-markdown code { padding: 2px 5px; font-size: .9em; border-radius: var(--radius-xs); background: var(--color-canvas-muted); font-family: var(--font-mono); }
.technique-audit {
margin-top: var(--space-5);
padding-top: var(--space-3);
border-top: 1px solid color-mix(in srgb, var(--color-border) 72%, transparent);
}
.technique-audit > summary {
width: fit-content;
display: flex;
align-items: center;
gap: var(--space-2);
min-height: 32px;
margin-inline: calc(var(--space-1) * -1);
padding: 0 var(--space-2);
border-radius: var(--radius-sm);
color: var(--color-ink-tertiary);
cursor: pointer;
list-style: none;
font-size: var(--type-caption);
font-weight: 500;
}
.technique-audit > summary::-webkit-details-marker { display: none; }
.technique-audit > summary::before {
width: 6px;
height: 6px;
border-inline-end: 1.5px solid currentColor;
border-bottom: 1.5px solid currentColor;
content: "";
transform: rotate(-45deg);
transition: transform 120ms ease-out;
}
.technique-audit[open] > summary::before { transform: rotate(45deg) translate(-1px, -1px); }
.technique-audit > summary:hover { background: var(--color-canvas-muted); color: var(--color-ink-secondary); }
.technique-audit > summary:focus-visible {
outline: 2px solid color-mix(in srgb, var(--color-focus) 52%, transparent);
outline-offset: 1px;
}
.technique-audit > summary small {
color: var(--color-ink-tertiary);
font-size: inherit;
font-variant-numeric: tabular-nums;
}
.technique-audit-panel {
max-width: 100%;
margin-top: var(--space-2);
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.technique-audit-panel table {
width: 100%;
border-collapse: collapse;
font-size: var(--type-caption);
line-height: 1.5;
}
.technique-audit-panel th,
.technique-audit-panel td {
padding: 7px 10px 7px 0;
border-bottom: 1px solid color-mix(in srgb, var(--color-border) 64%, transparent);
vertical-align: top;
text-align: start;
overflow-wrap: anywhere;
}
.technique-audit-panel thead th {
color: var(--color-ink-tertiary);
font-size: 11px;
font-weight: 650;
letter-spacing: .04em;
}
.technique-audit-panel tbody th {
width: 28%;
color: var(--color-ink);
font-weight: 550;
}
.technique-audit-panel td[data-status] {
width: 4.5em;
color: var(--color-ink-secondary);
white-space: nowrap;
}
.technique-audit-panel td:last-child { color: var(--color-ink-secondary); }
.technique-audit-panel tr:last-child th,
.technique-audit-panel tr:last-child td { border-bottom: 0; }
.error-message { margin: 12px 0 0; padding: 12px 14px; border-left: 3px solid var(--color-danger); background: var(--color-danger-muted); color: var(--color-danger); font-size: 13px; line-height: 1.6; border-color: var(--color-danger); border-radius: 0 var(--radius-md) var(--radius-md) 0; }
.composer-wrap { position: sticky; bottom: 0; z-index: 2; min-width: 0; border-top: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent); padding: var(--space-3) var(--space-6) var(--space-4); background: var(--color-frosted); backdrop-filter: saturate(130%) blur(20px); }
@@ -1490,6 +1608,15 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
margin-bottom: var(--space-4);
}
.conversation:not(.is-empty):not(.is-rectification) .message-markdown ul,
.conversation:not(.is-empty):not(.is-rectification) .message-markdown ol {
margin: 4px 0 var(--space-5);
}
.conversation:not(.is-empty):not(.is-rectification) .markdown-table {
margin: var(--space-5) 0;
}
.conversation:not(.is-empty):not(.is-rectification) .message-markdown h2,
.conversation:not(.is-empty):not(.is-rectification) .message-markdown h3 {
margin: var(--space-8) 0 var(--space-3);
@@ -2168,4 +2295,5 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
-webkit-text-fill-color: currentColor;
}
.rectification-activity-receipt__toggle svg { transition: none; }
.technique-audit > summary::before { transition: none; }
}
@@ -14,6 +14,8 @@ const markdownComponents: Components = {
<table>{children}</table>
</div>
),
ul: ({ children }) => <ul className="markdown-list">{children}</ul>,
ol: ({ children }) => <ol className="markdown-list">{children}</ol>,
};
export function renderChatMarkdown(text: string) {
@@ -4,6 +4,12 @@ import { useEffect, useState, type ReactNode } from "react";
import { prefetchOnIdle } from "@/components/chat-chunk-prefetch";
import { plainParagraphs } from "@/components/chat-message-paragraphs";
import { TechniqueAuditDisclosure } from "@/components/technique-audit-disclosure";
import type { TechniqueAuditRow } from "@/lib/consultation-agent-events";
import {
resolveTechniqueAuditRows,
splitSpokenAnswerAndTechniqueAudit,
} from "@/lib/consultation-technique-audit";
type MarkdownRenderer = (text: string) => ReactNode;
@@ -35,16 +41,30 @@ function useMarkdownRenderer() {
return renderer;
}
export function ChatMessageContent({ text }: { text: string }) {
export function ChatMessageContent({
text,
auditRows,
}: {
text: string;
auditRows?: readonly TechniqueAuditRow[];
}) {
const renderMarkdown = useMarkdownRenderer();
const split = splitSpokenAnswerAndTechniqueAudit(text);
const rows = resolveTechniqueAuditRows(split, auditRows);
const spoken = split.spoken;
return (
<div className="message-markdown">
{renderMarkdown
? renderMarkdown(text)
: (plainParagraphs(text) ?? []).map((paragraph, index) => (
<p key={index}>{paragraph}</p>
))}
<div className="message-answer">
{spoken ? (
<div className="message-markdown">
{renderMarkdown
? renderMarkdown(spoken)
: (plainParagraphs(spoken) ?? []).map((paragraph, index) => (
<p key={index}>{paragraph}</p>
))}
</div>
) : null}
{rows.length > 0 ? <TechniqueAuditDisclosure rows={rows} /> : null}
</div>
);
}
+6 -1
View File
@@ -97,7 +97,12 @@ export function ChatMessageRow({
{showActivity && (
<AgentActivityStatus state={activityState} label={activityLabel} />
)}
{message.text && <ChatMessageContent text={message.text} />}
{message.text && (
<ChatMessageContent
text={message.text}
auditRows={message.agentExecutionReceipt?.techniqueAuditTable}
/>
)}
</>
) : <p>{message.text}</p>}
</div>
@@ -0,0 +1,45 @@
"use client";
import {
displayTechniqueAuditLabel,
displayTechniqueAuditNote,
techniqueAuditStatusLabel,
} from "@/lib/consultation-technique-audit";
import type { TechniqueAuditRow } from "@/lib/consultation-agent-events";
export function TechniqueAuditDisclosure({
rows,
}: {
readonly rows: readonly TechniqueAuditRow[];
}) {
if (rows.length === 0) return null;
return (
<details className="technique-audit">
<summary>
<small>{rows.length} </small>
</summary>
<div className="technique-audit-panel">
<table>
<caption className="sr-only"></caption>
<thead>
<tr>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{rows.map((row, index) => (
<tr key={`${row.technique}-${index}`}>
<th scope="row">{displayTechniqueAuditLabel(row.technique)}</th>
<td data-status={row.status}>{techniqueAuditStatusLabel(row.status)}</td>
<td>{displayTechniqueAuditNote(row.note)}</td>
</tr>
))}
</tbody>
</table>
</div>
</details>
);
}
@@ -29,6 +29,17 @@ export const workflowReceiptSchema: z.ZodType<WorkflowReceipt> = z.object({
omittedDomains: z.array(consultationDomainSchema).min(1).max(6).optional(),
}).strict();
export const techniqueAuditStatusSchema = z.enum(["executed", "blocked", "not_applicable"]);
export type TechniqueAuditStatus = z.infer<typeof techniqueAuditStatusSchema>;
export const techniqueAuditRowSchema = z.object({
technique: z.string().trim().min(1).max(160),
status: techniqueAuditStatusSchema,
note: z.string().max(320).optional(),
}).strict();
export type TechniqueAuditRow = z.infer<typeof techniqueAuditRowSchema>;
const executionStepSchema = z.object({
sequence: z.number().int().min(1).max(32),
kind: z.enum(["skill", "tool", "validation"]),
@@ -64,6 +75,7 @@ export const agentExecutionReceiptSchema = z.object({
stepBudget: stepBudgetSchema.optional(),
workflow: workflowReceiptSchema,
techniqueTruth: z.string().max(120).optional(),
techniqueAuditTable: z.array(techniqueAuditRowSchema).max(80).optional(),
}).strict();
export type AgentExecutionReceipt = z.infer<typeof agentExecutionReceiptSchema>;
@@ -0,0 +1,203 @@
import type { TechniqueAuditRow, TechniqueAuditStatus } from "./consultation-agent-events.ts";
export type SpokenAnswerAndTechniqueAudit = Readonly<{
spoken: string;
rows: readonly TechniqueAuditRow[];
complete: boolean;
}>;
const AUDIT_HEADER = /^\|\s*技法\s*\|\s*状态\s*\|\s*说明\s*\|?\s*$/;
const AUDIT_SEPARATOR = /^\|?(?:\s*:?-{2,}:?\s*\|){2,}\s*:?-{2,}:?\s*\|?\s*$/;
const MACHINE_KEY = /^[a-z][a-z0-9_]{2,}$/i;
const STATUS_LABELS: Readonly<Record<TechniqueAuditStatus, string>> = {
executed: "已执行",
blocked: "阻塞",
not_applicable: "不适用",
};
const TECHNIQUE_LABELS: Readonly<Record<string, string>> = {
"VedAstro Cloud State": "VedAstro 云状态",
"VedAstro Raw Archive Manifest": "VedAstro 原始档案清单",
"External Engine Cross-Validation": "外部引擎交叉验证",
"Western Cross-Validation": "西洋交叉验证",
"Cross-System Arbitration": "跨系统仲裁",
"Evidence Packet": "证据包",
"Blind Technical Mode": "盲技术模式",
"MEVG / Global Web Evidence": "MEVG / 全网证据",
"Real Case Calibration": "真实案例校准",
"Timing Precision Gate": "时间精度门",
"Functional Benefic/Malefic": "功能吉凶星",
"Formal Vargas D1D60": "正式分盘 D1D60",
"Research D-N through D60": "研究型 D-N 至 D60",
"Extended Vargas D81/D108/D144": "扩展分盘 D81/D108/D144",
"Arudha / UL / A10": "Arudha / UL / A10",
"Narayana Dasha": "Narayana Dasha",
"Vimshottari sub-periods": "Vimshottari 小运",
Ashtakavarga: "Ashtakavarga",
"KP cusps": "KP 宫头",
"Gulika / Mandi": "Gulika / Mandi",
"Shadbala components": "Shadbala 分量",
"Ashtakavarga Kakshya": "Ashtakavarga Kakshya",
"Western natal (tropical)": "西洋本命(回归黄道)",
"Prashna chart": "问卦盘(Prashna",
"Ashtakoota matching": "合盘 Ashtakoota",
"Adhana / Niseka": "Adhana / Niseka",
"Muhurta election": "择时 Muhurta",
};
const WESTERN_LAYER_LABELS: Readonly<Record<string, string>> = {
transits: "行运",
solar_return: "太阳回归盘",
secondary_progressions: "次限",
solar_arc_directions: "太阳弧",
converse_secondary_progressions: "逆行次限",
converse_solar_arc_directions: "逆行太阳弧",
midpoints: "中点",
lunar_return: "月亮回归盘",
transit_duration_scan: "行运时长扫描",
parans: "parans",
};
const NOTE_LABELS: Readonly<Record<string, string>> = {
confidence_capped_without_verified_official_cloud: "未经核验的官方云证据,置信度封顶",
official_raw_archive_not_auditable_for_this_run: "官方原始档案本轮不可审计",
official_raw_archive_is_auditable: "官方原始档案可审计",
official_cloud_evidence_available: "官方云证据可用",
claims_capped_until_pyjhora_jhora_jyotishganit_are_invoked_for_this_run: "结论上限受外部引擎调用限制",
three_engine_runtime_closure_available: "三引擎运行时已闭环",
complete_packet_required_for_high_confidence: "高置信度需完整证据包",
supports_high_confidence: "支持高置信度",
normal_runtime_mode: "常规运行模式",
prevents_conversation_feedback_leakage: "防止会话反馈泄漏",
caps_claims_until_global_web_evidence_runs: "全局网页证据未运行,声明受限",
partial_reference_only_until_outcome_replay: "仅部分参考,未做结果回放",
supports_calibration: "支持校准",
blocks_false_precision_until_control_date_rankings_pass: "控制日期排名未通过前禁止伪精度",
high_rigor_claims_blocked_until_functional_nature_layer_is_present: "功能属性与自然属性冲突时降置信",
"observation for A10/UL; commercial claims stay non-deterministic": "仅观察层,商业断语保持非确定",
"component parity remains partial": "分量对齐仍为部分",
"KP remains non-deterministic in this product": "本产品内 KP 非确定性",
observation_only: "仅观察层",
"Chesta/Sthana/Dig/Drik/Kala/Naisargika as components; absolute Virupa truth is not closed": "绝对 Virupa 真值未闭环",
"observation_only; not verified day/month timing": "仅观察层,不作已核验日期",
"this run is a natal consultation": "本轮为本命咨询",
horary_path: "问卦路径",
"requires a partner chart": "需要伴侣盘",
"research_only; not a natal consultation layer": "仅研究用途",
"not an electional request": "非择时请求",
layer_not_materialized: "本层未物化",
};
export function techniqueAuditStatusLabel(status: TechniqueAuditStatus): string {
return STATUS_LABELS[status];
}
export function displayTechniqueAuditLabel(technique: string): string {
const mapped = TECHNIQUE_LABELS[technique];
if (mapped) return mapped;
const western = /^Western (.+)$/i.exec(technique);
if (!western?.[1]) return technique;
const layer = western[1].trim();
const layerLabel = WESTERN_LAYER_LABELS[layer] ?? layer.replaceAll("_", " ");
return `西洋${layerLabel}`;
}
export function displayTechniqueAuditNote(note: string | undefined): string {
const trimmed = note?.trim() ?? "";
if (!trimmed) return "";
const mapped = NOTE_LABELS[trimmed];
if (mapped) return mapped;
const formal = /^(\d+)\/20 named traditional charts/i.exec(trimmed);
if (formal) return `${formal[1]}/20 传统命名分盘`;
const research = /^(\d+) generic D-N charts/i.exec(trimmed);
if (research) return `${research[1]} 张通用分盘,非传统命名`;
const extended = /^(\d+) super-divisional charts/i.exec(trimmed);
if (extended) return `${extended[1]} 张超细分盘`;
if (MACHINE_KEY.test(trimmed) || trimmed.includes("_")) return "";
return trimmed;
}
export function parseTechniqueAuditStatus(value: string): TechniqueAuditStatus {
const normalized = value.trim().toLowerCase();
if (
value.trim() === "已执行"
|| ["used", "executed", "complete", "official_verified", "pass", "ready", "ok", "partial"].includes(normalized)
) {
return "executed";
}
if (
value.trim() === "不适用"
|| ["not_applicable", "n/a", "na", "not applicable"].includes(normalized)
) {
return "not_applicable";
}
return "blocked";
}
export function normalizeTechniqueAuditRows(value: unknown): TechniqueAuditRow[] | undefined {
if (!Array.isArray(value)) return undefined;
const rows: TechniqueAuditRow[] = [];
for (const item of value.slice(0, 80)) {
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
const record = item as Record<string, unknown>;
const technique = String(record.technique ?? record.name ?? "").trim();
if (!technique) continue;
const noteValue = record.note ?? record.boundary ?? record.effect_on_confidence;
const note = typeof noteValue === "string" ? noteValue.trim().slice(0, 320) : "";
rows.push({
technique: technique.slice(0, 160),
status: parseTechniqueAuditStatus(String(record.status ?? "")),
...(note ? { note } : {}),
});
}
return rows.length > 0 ? rows : undefined;
}
export function splitSpokenAnswerAndTechniqueAudit(text: string): SpokenAnswerAndTechniqueAudit {
const lines = text.replace(/\r\n/g, "\n").split("\n");
let headerIndex = -1;
for (let index = 0; index < lines.length; index += 1) {
if (AUDIT_HEADER.test(lines[index].trim())) headerIndex = index;
}
if (headerIndex < 0) {
return { spoken: text.replace(/\s+$/, ""), rows: [], complete: true };
}
const spoken = lines.slice(0, headerIndex).join("\n").replace(/\s+$/, "");
const tableLines = lines.slice(headerIndex).map((line) => line.trim());
const separator = tableLines[1] && AUDIT_SEPARATOR.test(tableLines[1]) ? tableLines[1] : null;
if (!separator) {
return { spoken, rows: [], complete: false };
}
const rows: TechniqueAuditRow[] = [];
for (const line of tableLines.slice(2)) {
if (!line) break;
if (!line.startsWith("|")) break;
const cells = splitGfmRow(line);
if (cells.length < 3) return { spoken, rows, complete: false };
const technique = cells[0]?.trim() ?? "";
if (!technique) continue;
const note = cells.slice(2).join(" | ").trim().slice(0, 320);
rows.push({
technique: technique.slice(0, 160),
status: parseTechniqueAuditStatus(cells[1] ?? ""),
...(note ? { note } : {}),
});
}
return { spoken, rows, complete: rows.length > 0 };
}
export function resolveTechniqueAuditRows(
split: SpokenAnswerAndTechniqueAudit,
delivered?: readonly TechniqueAuditRow[],
): readonly TechniqueAuditRow[] {
if (split.complete && split.rows.length > 0) return split.rows;
return delivered && delivered.length > 0 ? delivered : [];
}
function splitGfmRow(line: string): string[] {
return line.replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
}
+6 -1
View File
@@ -10,7 +10,8 @@ import { applyBirthTimeModeToWorkflowContext, type ConsultationBirthTimeMode } f
import { consultationMethodologyForDomains } from "../lib/consultation-methodology.ts";
import type { ServerChartConsultation } from "../lib/consultation-route-service.ts";
import { createConsultationPlan, type ConsultationPlan } from "../lib/consultation-plan.ts";
import type { WorkflowReceipt } from "../lib/consultation-agent-events.ts";
import type { TechniqueAuditRow, WorkflowReceipt } from "../lib/consultation-agent-events.ts";
import { normalizeTechniqueAuditRows } from "../lib/consultation-technique-audit.ts";
import type { AgentModelFinishReason } from "../lib/agent-observability.ts";
import {
consultationEvidencePacketSchema,
@@ -106,6 +107,7 @@ export type ConsultationRuntimeState = {
consultationToolDurationMs?: number;
workflowReceipt?: WorkflowReceipt;
techniqueTruth?: string;
techniqueAuditTable?: TechniqueAuditRow[];
steps: ConsultationRuntimeStep[];
stepBudget: ConsultationStepBudget;
stepsTruncated: boolean;
@@ -579,6 +581,9 @@ export function createConsultationTools(ctx: ConsultationAgentContext) {
appendConsultationRuntimeStep(ctx.state, { kind: "tool", name: "run-jyotish-consultation", status: "completed", durationMs: ctx.state.consultationToolDurationMs });
const modelContext = toModelDomainPlanContext(executions, omittedDomains);
ctx.state.methodologySectionCount = modelContext.methodology?.sections.length ?? 0;
ctx.state.techniqueAuditTable = normalizeTechniqueAuditRows(
modelContext.evidence_contract?.technique_audit_table,
);
return modelContext;
} catch (error) {
ctx.state.consultationToolDurationMs = now() - startedAt;
+2 -2
View File
@@ -28,7 +28,7 @@ Treat the server-provided current time as authoritative for words such as today,
Treat the tool result's top-level status and evidence_contract as the authoritative answer policy:
- When status is ready and evidence_contract.answer_policy.can_answer_direction is true, answer the user's actual question directly. Do not begin with infrastructure or confidence disclaimers.
- An unavailable optional provider or external cross-check is not a calculation failure. Never call it an internal error.
- Do not mention VedAstro, snapshot, fallback, gateway, archive, provider, MEVG, or calibration unless the user explicitly asks about methodology, or the missing layer materially blocks the exact claim they requested. The required Technique Audit Table may list those rows by their delivered labels without discussing provider internals.
- Do not mention VedAstro, snapshot, fallback, gateway, archive, provider, MEVG, or calibration unless the user explicitly asks about methodology, or the missing layer materially blocks the exact claim they requested. The collapsed Technique Audit Table may list those rows by their delivered labels without discussing provider internals.
When reference_transparency is present:
- Present candidate_windows and exact_triggers when relevant, but describe exact_triggers as technical trigger points, never guaranteed events.
@@ -44,7 +44,7 @@ When reference_transparency is present:
- Only say the chart calculation failed when evidence_contract.hard_blockers is non-empty.
- Never claim D2, D11, D9, D10, A10, UL, Narayana Dasha, a delivered Varga, or a delivered Western layer is missing when it appears in evidence_contract.available_layers, evidence_contract.varga_spectrum, evidence_contract.western_spectrum, chart, or local_layers.
- evidence_contract.technique_audit_table is the invocation record for this run. Use every executed layer that is relevant to the question. Status executed means the server ran it; blocked means it could not; not_applicable means it does not apply. Never treat an omitted row as permission to invent the technique, and never silently skip an executed layer the question needs.
- The visible answer must end with that Technique Audit Table in spoken Chinese labels ( / / ). This is the one table VISIBLE VOICE requires; do not also dump JSON.
- Do not paste that Technique Audit Table into the spoken answer. The product UI renders it collapsed from the delivered rows. This is the one audit surface; do not also dump JSON.
- local_layers.dasha_sub_periods carries the antardasha boundaries inside the running mahadasha. When it is present, use those boundaries and never say sub-periods were not calculated; when it is absent, say so once instead of implying the calculation broke.
- Treat evidence_contract.answer_policy as a hard output contract. When can_answer_precise_timing is false, provide only direction or structure and do not state a month, date, or guaranteed timing outcome.
- Treat answer_policy.deterministic_claims_forbidden_for as a hard prohibition. Do not use a restricted technique to make a deterministic conclusion. reference_only, partial, blocked, research_only_blocked, and partial_registry_only are commercial claim boundaries, not validated capabilities.
+4 -4
View File
@@ -5,16 +5,16 @@ Write spoken Simplified Chinese, addressing the reader as 你.
- Answer the user's actual question in the first sentence.
- Then use the delivered evidence the way the local skill does: unified parameters and raw structure first (degrees, houses, vargas, Dasha boundaries, Shadbala/AV, functional benefic/malefic, Western layers), then parallel systems, conflicts, and only then a conditional synthesis.
- Gloss a technical term once in plain Chinese (D9, ) before using the everyday phrase.
- Prefer paragraphs. Use Markdown to compare concrete options, to show raw structure that would otherwise be a wall of numbers, and for the required Technique Audit Table at the end.
- If a limitation materially changes the claim, put it in one short closing sentence before the table; otherwise omit it.
- Prefer paragraphs. Use Markdown to compare concrete options or to show raw structure that would otherwise be a wall of numbers. Do not paste the Technique Audit Table into the spoken answer; the product UI renders those delivered rows in a collapsed control.
- If a limitation materially changes the claim, put it in one short closing sentence; otherwise omit it.
After the spoken answer, include exactly one compact Markdown Technique Audit Table with columns / / . Copy rows from the delivered technique_audit_table. Translate status to / / . Do not invent, omit, or upgrade a row. Do not dump JSON keys or YAML.
Do not copy the delivered technique_audit_table into the spoken answer. The product UI renders it collapsed from the server-delivered rows. Still obey every row: do not invent, omit, or upgrade a technique. Translate nothing into JSON or YAML. Status labels in that UI are / / .
Never do these in the visible answer:
- Quote JSON keys, English status codes, or field names (core_status, blocked, ready, consumer_context, missing_layers, omitted_domains).
- Say , , , , , , or as an opener.
- Restate the user's question, pad to a template length, or use a fixed / / skeleton.
- Dump YAML, JSON, or a second inventory besides that one table.
- Dump YAML, JSON, or a Technique Audit Table in the spoken answer.
- Skip an executed layer the question needs in order to keep the answer short.
Bad: 根据当前计算结果
@@ -45,8 +45,10 @@ test("the pre-markdown fallback renders prose but never raw markdown", () => {
// And: one stable wrapper serves both paths, so no styling or caret is lost on upgrade.
assert.equal(contentSource.match(/className="message-markdown"/g)?.length, 1);
assert.match(contentSource, /className="message-answer"/);
assert.match(contentSource, /TechniqueAuditDisclosure/);
assert.match(contentSource, /<p key=\{index\}>\{paragraph\}<\/p>/);
assert.match(contentSource, /plainParagraphs\(text\) \?\? \[\]/);
assert.match(contentSource, /plainParagraphs\(spoken\) \?\? \[\]/);
});
test("gsap loads on demand while keeping the reduced-motion gate", () => {
@@ -0,0 +1,112 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { agentExecutionReceiptSchema } from "../src/lib/consultation-agent-events.ts";
import {
displayTechniqueAuditLabel,
displayTechniqueAuditNote,
normalizeTechniqueAuditRows,
resolveTechniqueAuditRows,
splitSpokenAnswerAndTechniqueAudit,
techniqueAuditStatusLabel,
} from "../src/lib/consultation-technique-audit.ts";
const disclosureSource = readFileSync(new URL("../src/components/technique-audit-disclosure.tsx", import.meta.url), "utf8");
const contentSource = readFileSync(new URL("../src/components/chat-message-content.tsx", import.meta.url), "utf8");
const toolsSource = readFileSync(new URL("../src/mastra/consultation-tools.ts", import.meta.url), "utf8");
const routeSource = readFileSync(new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8");
const globalStyles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
const spoken = [
"今天适合推进沟通,不适合拍板承诺。",
"",
"| 适合推进 | 需要避开 |",
"|---|---|",
"| 汇报、提案、写作 | 冲动承诺、夸大其词 |",
"",
"给一个现在就能做的行动。",
].join("\n");
const auditTable = [
"| 技法 | 状态 | 说明 |",
"|---|---|---|",
"| VedAstro 云状态 | 阻塞 | 未经核验的官方云证据,置信度封顶 |",
"| 功能吉凶星 | 已执行 | 功能属性与自然属性冲突时降置信 |",
"| 问卦盘(Prashna | 不适用 | 本轮为本命咨询 |",
].join("\n");
test("a trailing technique audit table is split out of the spoken answer", () => {
const split = splitSpokenAnswerAndTechniqueAudit(`${spoken}\n\n${auditTable}`);
assert.equal(split.complete, true);
assert.equal(split.spoken, spoken);
assert.equal(split.spoken.includes("适合推进"), true);
assert.equal(split.spoken.includes("技法"), false);
assert.deepEqual(split.rows, [
{ technique: "VedAstro 云状态", status: "blocked", note: "未经核验的官方云证据,置信度封顶" },
{ technique: "功能吉凶星", status: "executed", note: "功能属性与自然属性冲突时降置信" },
{ technique: "问卦盘(Prashna", status: "not_applicable", note: "本轮为本命咨询" },
]);
});
test("an incomplete streaming audit table is hidden rather than shown as pipes", () => {
const split = splitSpokenAnswerAndTechniqueAudit(`${spoken}\n\n| 技法 | 状态 | 说明 |\n|---|---|---|\n| VedAstro 云状态 | 阻塞 |`);
assert.equal(split.complete, false);
assert.equal(split.spoken, spoken);
assert.deepEqual(split.rows, []);
});
test("delivered receipt rows fill the folded table when the spoken answer has no dump", () => {
const delivered = normalizeTechniqueAuditRows([
{ technique: "VedAstro Cloud State", status: "blocked", boundary: "confidence_capped_without_verified_official_cloud" },
{ technique: "Formal Vargas D1D60", status: "executed", boundary: "20/20 named traditional charts; remaining D-N are separate" },
{ technique: "Western transits", status: "used", note: "layer_not_materialized" },
]);
assert.deepEqual(delivered, [
{ technique: "VedAstro Cloud State", status: "blocked", note: "confidence_capped_without_verified_official_cloud" },
{ technique: "Formal Vargas D1D60", status: "executed", note: "20/20 named traditional charts; remaining D-N are separate" },
{ technique: "Western transits", status: "executed", note: "layer_not_materialized" },
]);
assert.equal(displayTechniqueAuditLabel("VedAstro Cloud State"), "VedAstro 云状态");
assert.equal(displayTechniqueAuditLabel("Western transits"), "西洋行运");
assert.equal(displayTechniqueAuditNote("confidence_capped_without_verified_official_cloud"), "未经核验的官方云证据,置信度封顶");
assert.equal(displayTechniqueAuditNote("20/20 named traditional charts; remaining D-N are separate"), "20/20 传统命名分盘");
assert.equal(displayTechniqueAuditNote("layer_not_materialized"), "本层未物化");
assert.equal(displayTechniqueAuditNote("unmapped_machine_key"), "");
assert.equal(techniqueAuditStatusLabel("blocked"), "阻塞");
const split = splitSpokenAnswerAndTechniqueAudit(spoken);
assert.deepEqual(resolveTechniqueAuditRows(split, delivered), delivered);
});
test("the public receipt may carry the audit rows and still rejects internal diagnostics", () => {
const receipt = agentExecutionReceiptSchema.parse({
runId: "run",
runtime: "mastra-agentic",
skill: { name: "jyotish-vedic-astrology", loaded: true, referenceReads: 0, methodologySections: 0 },
steps: [{ sequence: 1, kind: "skill", name: "jyotish-vedic-astrology", status: "completed" }],
workflow: { route: "timing", status: "ready", preciseTiming: "allowed", missingLayers: [] },
techniqueAuditTable: [
{ technique: "Narayana Dasha", status: "executed", note: "本轮无独立当前读数" },
],
});
assert.equal(receipt.techniqueAuditTable?.[0]?.technique, "Narayana Dasha");
assert.throws(() => agentExecutionReceiptSchema.parse({
...receipt,
techniqueAuditTable: [{ technique: "Narayana Dasha", status: "used" }],
}));
});
test("the folded audit control stays closed and never remounts the internal panel", () => {
assert.match(disclosureSource, /<details className="technique-audit">/);
assert.doesNotMatch(disclosureSource, /<details[^>]*\sopen/);
assert.doesNotMatch(disclosureSource, /EvidenceAuditPanel/);
assert.doesNotMatch(disclosureSource, /techniqueTruth|workflowReceipt|rawReceipt/);
assert.match(contentSource, /splitSpokenAnswerAndTechniqueAudit/);
assert.match(contentSource, /TechniqueAuditDisclosure/);
assert.match(toolsSource, /ctx\.state\.techniqueAuditTable = normalizeTechniqueAuditRows/);
assert.match(routeSource, /techniqueAuditTable: state\.techniqueAuditTable/);
assert.match(globalStyles, /\.markdown-table/);
assert.match(globalStyles, /\.technique-audit >/);
assert.match(globalStyles, /\.markdown-table th,/);
});
@@ -12,7 +12,7 @@ test("product voice keeps skill technique density without dumping JSON", () => {
assert.match(voice, /governs METHOD, TECHNIQUE INVOCATION, and TRUTH BOUNDARIES/);
assert.match(voice, /must not drop techniques, raw structures, or audit rows/);
assert.match(voice, /unified parameters and raw structure first/);
assert.match(voice, /required Technique Audit Table/);
assert.match(voice, /Do not paste the Technique Audit Table/);
assert.match(voice, /已执行 \/ 阻塞 \/ 不适用/);
assert.match(voice, /Answer the user's actual question in the first sentence/);
assert.match(voice, /本轮未完整计算/);
@@ -26,6 +26,7 @@ test("product voice keeps skill technique density without dumping JSON", () => {
assert.match(binding, /never quote it, reveal it, or present its report template as the chat format/);
assert.match(mastra, /The bound skill method is this product's answering contract/);
assert.match(mastra, /Do not use a restricted technique/);
assert.match(mastra, /Do not paste that Technique Audit Table into the spoken answer/);
assert.match(mastra, /Do not announce a skipped-domain inventory/);
assert.doesNotMatch(mastra, /Name the domains you did cover, say plainly that the remaining ones were not calculated/);
assert.doesNotMatch(mastra, /2-5 short paragraphs/);
@@ -37,6 +38,6 @@ test("consult user turn no longer dumps tool JSON that the model would parrot",
assert.match(chartBranch, /请直接用口语回答下面的问题[\s\S]*?resolvedQuestion\.modelQuestion/);
assert.doesNotMatch(chartBranch, /JSON\.stringify\(toolInput\)/);
assert.doesNotMatch(chartBranch, /经过服务端校验的工具参数/);
assert.match(route, /请用口语直接回答;回答末尾附上技法审计表,不要复述内部字段/);
assert.match(route, /请用口语直接回答;不要把技法审计表写进正文,不要复述内部字段/);
assert.match(chartBranch, /await runConsultationWorkflow\(toolInput/);
});
+6 -1
View File
@@ -3,14 +3,17 @@ import { readFileSync } from "node:fs";
import test from "node:test";
const rowSource = readFileSync(new URL("../src/components/chat-message-row.tsx", import.meta.url), "utf8");
const contentSource = readFileSync(new URL("../src/components/chat-message-content.tsx", import.meta.url), "utf8");
const panelSource = readFileSync(new URL("../src/components/evidence-audit-panel.tsx", import.meta.url), "utf8");
const pageSource = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
const globalStyles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
test("assistant messages do not expose the internal Technique Audit Table", () => {
test("assistant messages fold the user-facing Technique Audit Table and never remount the internal panel", () => {
assert.doesNotMatch(rowSource, /EvidenceAuditPanel/);
assert.doesNotMatch(rowSource, /claimStatus=\{message\.techniqueTruth\}/);
assert.doesNotMatch(rowSource, /workflowReceipt=\{message\.workflowReceipt\}/);
assert.match(contentSource, /TechniqueAuditDisclosure/);
assert.match(rowSource, /auditRows=\{message\.agentExecutionReceipt\?\.techniqueAuditTable\}/);
assert.match(panelSource, /Technique Audit Table/);
assert.match(panelSource, /Workflow route:/);
assert.match(panelSource, /Precise timing:/);
@@ -25,6 +28,8 @@ test("assistant messages do not expose the internal Technique Audit Table", () =
assert.match(panelSource, /查看非敏感 raw receipt/);
assert.match(globalStyles, /\.evidence-audit-panel/);
assert.match(globalStyles, /\.raw-evidence-receipt/);
assert.match(globalStyles, /\.technique-audit >/);
assert.match(globalStyles, /\.markdown-table/);
});
test("consult responses persist workflow receipt headers for evidence rendering", () => {
+1 -1
View File
@@ -60,7 +60,7 @@ test("the bound method is the runtime excerpt, not the maintainer manual", () =>
assert.ok(body.includes(heading), heading);
assert.ok(bound.includes(heading), heading);
}
for (const phrase of ["全谱系真实调用", "P0/P1 观察层", "网页对话在口语回答末尾给出 Technique Audit Table", "静态分析10步"]) {
for (const phrase of ["全谱系真实调用", "P0/P1 观察层", "网页对话的技法审计表由界面折叠展示", "静态分析10步"]) {
assert.ok(bound.includes(phrase), phrase);
}
for (const heading of ["施工判断原则", "冲顶路线", "37大子命令", "验证与错题体系", "开源复用边界冻结"]) {