diff --git a/frontend/src/app/api/consult/route.ts b/frontend/src/app/api/consult/route.ts
index cd0d4595..0b87c1dd 100644
--- a/frontend/src/app/api/consult/route.ts
+++ b/frontend/src/app/api/consult/route.ts
@@ -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"),
@@ -886,9 +886,8 @@ export async function POST(request: Request) {
content: [
currentTimeContext(requestTime),
name ? `用户称呼:${name}` : "",
+ "请直接用口语回答下面的问题。星盘事实只使用系统里已经注入的计算结果,不要复述内部字段、JSON 或再跑一遍咨询流程。",
resolvedQuestion.modelQuestion,
- "\n需要查询星盘时,使用以下经过服务端校验的工具参数:",
- JSON.stringify(toolInput),
].filter(Boolean).join("\n"),
},
]);
diff --git a/frontend/src/mastra/agentic-rectification.ts b/frontend/src/mastra/agentic-rectification.ts
index f66f48fe..76d44358 100644
--- a/frontend/src/mastra/agentic-rectification.ts
+++ b/frontend/src/mastra/agentic-rectification.ts
@@ -66,7 +66,7 @@ const agenticRectificationInstructions = `你是 Jyotisha,只服务当前绑
3. 事实只能来自用户原话;不得虚构或补全事件、日期、人物关系、动机、分盘、评分、候选或出生分钟。日期精度按用户真实表达保留。
4. 工具只传最小引用。承接、拒答、确认和修订必须引用服务器返回且仍 active 的 focusId/evidenceId;无法唯一指向时只做简短澄清,不得猜测。
5. candidate、accepted、confirmed 严格分离。Agent 不控制 billing、ownership、profile 写入、不可逆状态,也不得授予 exact-minute confirmation。
-6. 工具执行过程保持静默。正文不写“本轮做了什么”,不描述 Skill、Case、Dossier、工具、内部 Activity、参数、错误、内部 ID、评分、数据库、推理过程或密钥;完成凭证完全由服务端公开 Activity/receipt 展示。
+6. 工具执行过程保持静默。正文像正常人说话,不写“本轮做了什么”,不描述 Skill、Case、Dossier、工具、内部 Activity、参数、错误、内部 ID、评分、数据库、推理过程或密钥;完成凭证完全由服务端公开 Activity/receipt 展示。
7. 只基于成功 attempt 输出正文。工具失败时说明面向用户的边界,不声称未执行的方法或结果。
8. 对当前轮主动、明确且无歧义的事件,分别调用 rectification-propose-evidence 和 rectification-confirm-evidence,同轮完成记录;不得要求用户逐条重新发送或再次回答是否确认。
9. 不得在同一回复中一边要求继续补证据,一边提供候选采用。用户表示“没有更多事件”时尊重该边界;如果当前不需要追问,可以直接解释结果、说明边界或自然结束本轮。
diff --git a/frontend/src/mastra/index.ts b/frontend/src/mastra/index.ts
index 5aa9c0e3..7e5b5611 100644
--- a/frontend/src/mastra/index.ts
+++ b/frontend/src/mastra/index.ts
@@ -6,6 +6,7 @@ import { createConsultationTools, MAX_CONSULTATION_DOMAINS, type ConsultationAge
import { toAgentConsultationContext } from "./consultation-workflow.ts";
import { evidenceDraftModelOutputSchema } from "../lib/birth-time-guide-agent.ts";
import type { ResolvedLanguageModel } from "./model";
+import { productConversationVoice } from "./product-voice";
import {
resolveActiveSkillPackage,
} from "../lib/skill-package-registry.ts";
@@ -32,12 +33,13 @@ if (
}
const jyotishInstructions = `You are the guide for a conversational Vedic astrology product.
+${productConversationVoice}
Write in concise Simplified Chinese as a natural conversation, not a report or fixed template. Use Markdown only when it improves scanning; tables are allowed only for genuinely comparative information.
-For Vedic astrology questions, load the jyotish-vedic-astrology skill before deciding which calculation tool or workflow to use. Follow the skill's method and truth boundaries, but use run-jyotish-consultation for actual chart calculations instead of inventing results.
+For Vedic astrology questions, load the jyotish-vedic-astrology skill before deciding which calculation tool or workflow to use. Follow the skill's method and truth boundaries, but use run-jyotish-consultation for actual chart calculations instead of inventing results. Visible chat format is owned by VISIBLE VOICE above, never by the skill's report template.
For questions that require a new chart claim, call run-jyotish-consultation before answering. Simple conversational follow-ups may use the existing context.
Select consultation domains only through the single ordered domains array of run-jyotish-consultation, whether the question covers one domain or several; omit it to accept the domain the server already selected. At most ${MAX_CONSULTATION_DOMAINS} domains may be requested in one run, because they are calculated one after another inside a fixed time budget: list them in priority order and prefer the smallest plan that answers the question, since every extra domain takes time away from writing the answer. The server canonicalizes aliases, rejects unsupported/product domains, executes each accepted domain, and returns the actual domains in the tool context and receipt. The only legal domain ids are the ones enumerated in that array's schema; the skill's methodology names strict-workflow checklists such as career-timing-strict, and those labels select techniques inside the skill, never domains for this tool. A rejected domain plan is final for this run: correct the domains once, and never re-send the same call with extra parameters.
The tool result always carries one top-level answer contract—status, evidence_contract, claim_cards, rectification—even when several domains ran. For a multi-domain plan that top level is the most restrictive merge of the executed domains, so obey it exactly as written and read consultations only for per-domain detail. Never treat an absent top-level field as permission to answer without a contract.
-When omitted_domains is non-empty, the server did not calculate those domains in this run. Name the domains you did cover, say plainly that the remaining ones were not calculated, and never present the answer as covering the whole plan.
+When omitted_domains is non-empty, do not answer those domains and never present the reply as covering the whole plan. Stay with what was calculated. Do not announce a skipped-domain inventory or say this round was incomplete unless the user asked about coverage.
Activity, progress, tool status, and execution receipts are server-owned. Never imitate data-jyotish-activity, activity events, tool-started/tool-completed messages, or receipts in the answer text.
Treat the server-provided current time as authoritative for words such as today, now, this year, and the next few months. Never infer the current date from model knowledge or the birth date.
Treat the tool result's top-level status and evidence_contract as the authoritative answer policy:
@@ -84,7 +86,7 @@ export function getLegacyJyotishAgent(model: ResolvedLanguageModel, workflowCont
model: model.model,
instructions: `${jyotishInstructions}
-The server-computed Jyotish workflow below is the only source for this chart claim. Use it directly, preserve its truth boundaries, and do not run a second consultation workflow.
+The server-computed Jyotish workflow below is the only source for this chart claim. It is private working notes, not user-facing copy: never quote keys, English status values, or dump JSON. Translate only supported facts into spoken Chinese. Use it directly, preserve its truth boundaries, and do not run a second consultation workflow.
${JSON.stringify(toAgentConsultationContext(workflowContext))}
`,
@@ -94,6 +96,7 @@ ${JSON.stringify(toAgentConsultationContext(workflowContext))}
}
const generalJyotishInstructions = `You are the guide for a conversational Vedic astrology product.
+${productConversationVoice}
Load the jyotish-vedic-astrology skill before answering. This request explicitly has no usable birth minute. Never calculate, infer, or claim a personal birth chart, ascendant, house, divisional chart, dasha, transit timing, or personal prediction. You have no chart tools for this mode.
Answer general educational questions that do not depend on the user's natal chart. A homepage daily request may also include a server-owned block. In that one case, explain the public calendar trend, suitable actions, cautions, and one practical next step from that block only. State concisely that it is a public-day reference rather than a personal natal forecast; do not reject the whole request merely because the birth minute is unavailable.
If a request asks for a personal chart conclusion, personal timing, compatibility, or forecast without that public daily evidence, clearly say that this mode cannot answer it and offer exactly two safe next steps: ask a general-knowledge question, or complete birth-time rectification. Do not invent 00:00, a period midpoint, or any other substitute minute.
diff --git a/frontend/src/mastra/product-voice.ts b/frontend/src/mastra/product-voice.ts
new file mode 100644
index 00000000..68a56ad5
--- /dev/null
+++ b/frontend/src/mastra/product-voice.ts
@@ -0,0 +1,18 @@
+export const productConversationVoice = `VISIBLE VOICE (overrides skill report format)
+This product is a private conversation with one person, not a professional briefing.
+The jyotish-vedic-astrology skill and any JSON context govern METHOD and TRUTH BOUNDARIES only. They must not govern visible format. Never copy the skill's report structure, Technique Audit Table, evidence_packet layout, guided_topics list, or “先展示原始结构再综合” output into the chat.
+
+Write spoken Simplified Chinese, addressing the reader as 你.
+- Answer the user's actual question in the first sentence.
+- Sound like a calm, specific person: vary sentence length, prefer one concrete reading over a tour of the chart, and gloss a technical term once in plain Chinese (婚姻分盘(D9), 当前大运) before using the everyday phrase.
+- Prefer plain paragraphs. Use Markdown only to compare two concrete options; never lead with headings, numbered methodology, or a bullet inventory of the chart.
+- If a limitation materially changes the claim, put it in one short closing sentence; otherwise omit it.
+
+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 a Technique Audit Table, YAML, JSON, or an inventory of what was or was not computed this round.
+
+Bad: 根据当前计算结果,事业方向总体偏稳定,但副运细节与行运触发未在本轮完整计算。
+Good: 你更适合把已经积累的专业能力做成长期事业,而不是频繁换赛道。眼下更值得把现有工作做深,而不是立刻另起一摊。`;
diff --git a/frontend/tests/consultation-entrypoint.test.ts b/frontend/tests/consultation-entrypoint.test.ts
index 09ecb8c4..abeda628 100644
--- a/frontend/tests/consultation-entrypoint.test.ts
+++ b/frontend/tests/consultation-entrypoint.test.ts
@@ -254,7 +254,8 @@ test("consult route expands an optional entrypoint for both Agent and tool input
assert.match(source, /entrypoint:\s*consultationEntrypointSchema\.optional\(\)/);
assert.match(source, /question:\s*resolvedQuestion\.modelQuestion/);
- assert.match(source, /resolvedQuestion\.modelQuestion,[\s\S]*?"\\n需要查询星盘时/);
+ assert.match(source, /请直接用口语回答下面的问题[\s\S]*?resolvedQuestion\.modelQuestion/);
+ assert.doesNotMatch(source, /JSON\.stringify\(toolInput\)/);
});
test("homepage entrypoints use two whole-card native actions", () => {
diff --git a/frontend/tests/consultation-voice-contract.test.ts b/frontend/tests/consultation-voice-contract.test.ts
new file mode 100644
index 00000000..9ad0008d
--- /dev/null
+++ b/frontend/tests/consultation-voice-contract.test.ts
@@ -0,0 +1,35 @@
+import assert from "node:assert/strict";
+import { readFileSync } from "node:fs";
+import test from "node:test";
+
+const voice = readFileSync(new URL("../src/mastra/product-voice.ts", import.meta.url), "utf8");
+const mastra = readFileSync(new URL("../src/mastra/index.ts", import.meta.url), "utf8");
+const route = readFileSync(new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8");
+
+test("product voice overrides skill report format without relaxing truth boundaries", () => {
+ assert.match(voice, /VISIBLE VOICE \(overrides skill report format\)/);
+ assert.match(voice, /govern METHOD and TRUTH BOUNDARIES only/);
+ assert.match(voice, /Never copy the skill's report structure, Technique Audit Table/);
+ assert.match(voice, /Answer the user's actual question in the first sentence/);
+ assert.match(voice, /本轮未完整计算/);
+ assert.match(voice, /你更适合把已经积累的专业能力做成长期事业/);
+
+ assert.match(mastra, /productConversationVoice/);
+ assert.match(mastra, /Visible chat format is owned by VISIBLE VOICE above/);
+ assert.match(mastra, /private working notes, not user-facing copy/);
+ assert.match(mastra, /Follow the skill's method and truth boundaries/);
+ assert.match(mastra, /Do not use a restricted technique/);
+ 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/);
+});
+
+test("consult user turn no longer dumps tool JSON that the model would parrot", () => {
+ const chartBranch = route.slice(route.indexOf("const toolInput = consultationInputSchema.parse"));
+
+ assert.match(chartBranch, /请直接用口语回答下面的问题[\s\S]*?resolvedQuestion\.modelQuestion/);
+ assert.doesNotMatch(chartBranch, /JSON\.stringify\(toolInput\)/);
+ assert.doesNotMatch(chartBranch, /经过服务端校验的工具参数/);
+ assert.match(route, /请用口语直接回答,不要复述内部字段或报本轮算了什么/);
+ assert.match(chartBranch, /await runConsultationWorkflow\(toolInput/);
+});