fix(consult): complete natal chart tool on homepage topic cards (BUG-630)
Thinking models were spending the first natal step on skill_read or a Level 2 draft, so home chips such as 家庭 never satisfied the calculation contract. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -25,6 +25,7 @@ import {
|
||||
} from "@/lib/agent-observability";
|
||||
import {
|
||||
consultationEntrypointSchema,
|
||||
pinsConsultationDomains,
|
||||
resolveConsultationQuestion,
|
||||
shouldLoadGeneralDailyPanchanga,
|
||||
} from "@/lib/consultation-entrypoint";
|
||||
@@ -46,6 +47,7 @@ import {
|
||||
AGENT_SLICE_MAX_STEPS,
|
||||
consultationContinueGenerationSettings,
|
||||
consultationGenerationSettings,
|
||||
consultationNatalPrepareStep,
|
||||
consultationSliceGenerationSettings,
|
||||
createConsultationAgentContext,
|
||||
createWindowConsultationAgentContext,
|
||||
@@ -127,7 +129,7 @@ const generalChatRequestSchema = z.object({
|
||||
consultationMode: z.literal("general_no_birth_time"),
|
||||
question: z.string().trim().min(1).max(500),
|
||||
theme: consultationDomainSchema,
|
||||
entrypoint: z.literal("daily_starlanguage").optional(),
|
||||
entrypoint: consultationEntrypointSchema.exclude(["birth_time_rectification"]).optional(),
|
||||
}).strict();
|
||||
|
||||
const windowChatRequestSchema = z.object({
|
||||
@@ -135,7 +137,7 @@ const windowChatRequestSchema = z.object({
|
||||
consultationMode: z.literal("declared_birth_window"),
|
||||
question: z.string().trim().min(1).max(500),
|
||||
theme: consultationDomainSchema,
|
||||
entrypoint: z.literal("daily_starlanguage").optional(),
|
||||
entrypoint: consultationEntrypointSchema.exclude(["birth_time_rectification"]).optional(),
|
||||
}).strict();
|
||||
|
||||
const chatRequestSchema = z.union([
|
||||
@@ -838,9 +840,12 @@ export async function POST(request: Request) {
|
||||
}
|
||||
};
|
||||
const cacheBoundary = cachedSystemMessage("【上下文缓存边界】后续内容为本轮请求输入。", selectedModel.model);
|
||||
const natalToolInstruction = pinsConsultationDomains(consultEntrypoint)
|
||||
? "如需新的个人星盘结论,必须调用服务器绑定的排盘工具。调用时不要填写 domains,沿用服务器已选定的主题。"
|
||||
: "如需新的个人星盘结论,必须调用服务器绑定的排盘工具。";
|
||||
const natalInstruction = consultEntrypoint === "daily_starlanguage"
|
||||
? "先加载 Jyotish Skill;如需新的个人星盘结论,必须调用服务器绑定的排盘工具。按三节写:今日趋势、适合推进 / 需要避开、一个行动(把技法审计表和「探索性日提示,不是确定预测」放进最后一节)。不要复述内部 JSON 字段。"
|
||||
: "先加载 Jyotish Skill;如需新的个人星盘结论,必须调用服务器绑定的排盘工具。事业/财富/婚恋/家庭按 skill Level 2 模板写:原始结构、六步宫位、Yoga 表、时机、综合、文末技法审计表,然后才是现代生活措辞。不要复述内部 JSON 字段。";
|
||||
? `${natalToolInstruction}按三节写:今日趋势、适合推进 / 需要避开、一个行动(把技法审计表和「探索性日提示,不是确定预测」放进最后一节)。不要复述内部 JSON 字段。`
|
||||
: `${natalToolInstruction}事业/财富/婚恋/家庭按 skill Level 2 模板写:原始结构、六步宫位、Yoga 表、时机、综合、文末技法审计表,然后才是现代生活措辞。不要复述内部 JSON 字段。`;
|
||||
const adoptedRangeNote = consultationMode === "verified_chart"
|
||||
&& prepared.serverChart?.truth.birthTimeStatus === "accepted"
|
||||
&& prepared.serverChart.toolInput.candidate_range
|
||||
@@ -884,14 +889,21 @@ export async function POST(request: Request) {
|
||||
hooks,
|
||||
...consultationGenerationSettings(selectedModel.model),
|
||||
};
|
||||
async function streamWithOverflowRetry(agent: {
|
||||
stream: (
|
||||
messages: typeof baseMessages,
|
||||
options: typeof streamOptions,
|
||||
) => Promise<{ fullStream: AsyncIterable<unknown> | ReadableStream<unknown>; totalUsage: Promise<Usage> }>;
|
||||
}) {
|
||||
const natalStreamOptions = {
|
||||
...streamOptions,
|
||||
prepareStep: consultationNatalPrepareStep,
|
||||
};
|
||||
async function streamWithOverflowRetry(
|
||||
agent: {
|
||||
stream: (
|
||||
messages: typeof baseMessages,
|
||||
options: typeof streamOptions | typeof natalStreamOptions,
|
||||
) => Promise<{ fullStream: AsyncIterable<unknown> | ReadableStream<unknown>; totalUsage: Promise<Usage> }>;
|
||||
},
|
||||
options: typeof streamOptions | typeof natalStreamOptions = streamOptions,
|
||||
) {
|
||||
try {
|
||||
const result = await agent.stream(baseMessages, streamOptions);
|
||||
const result = await agent.stream(baseMessages, options);
|
||||
usages.push(result.totalUsage);
|
||||
return result;
|
||||
} catch (error) {
|
||||
@@ -900,7 +912,7 @@ export async function POST(request: Request) {
|
||||
// clients cannot parse rectification `attempt.reset`, so the retry stays
|
||||
// server-side and never opens a second user-visible wait.
|
||||
baseMessages = consultationBaseMessages(true);
|
||||
const overflow = await agent.stream(baseMessages, streamOptions);
|
||||
const overflow = await agent.stream(baseMessages, options);
|
||||
usages.push(overflow.totalUsage);
|
||||
return overflow;
|
||||
}
|
||||
@@ -1114,7 +1126,7 @@ export async function POST(request: Request) {
|
||||
state,
|
||||
});
|
||||
const agent = getJyotishAgent(selectedModel, agentContext);
|
||||
const result = await streamWithOverflowRetry(agent);
|
||||
const result = await streamWithOverflowRetry(agent, natalStreamOptions);
|
||||
const retry = async () => {
|
||||
const retried = await agent.stream([
|
||||
...baseMessages,
|
||||
@@ -1122,7 +1134,7 @@ export async function POST(request: Request) {
|
||||
role: "user" as const,
|
||||
content: "运行合同不完整:本次尚未取得服务器计算结果。请调用 run-jyotish-consultation 完成计算,再据此回答;不要在工具参数中添加出生资料。",
|
||||
},
|
||||
], streamOptions);
|
||||
], natalStreamOptions);
|
||||
usages.push(retried.totalUsage);
|
||||
return retried.fullStream;
|
||||
};
|
||||
@@ -1136,7 +1148,7 @@ export async function POST(request: Request) {
|
||||
role: "user" as const,
|
||||
content: "服务器计算已经完成,但上一轮没有输出任何回答文本。请重新取回本次计算结果,然后直接给出回答;不要只描述过程或工具调用。",
|
||||
},
|
||||
], streamOptions);
|
||||
], natalStreamOptions);
|
||||
usages.push(retried.totalUsage);
|
||||
return retried.fullStream;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
import type { ConsultationEntrypoint } from "@/lib/consultation-entrypoint";
|
||||
import type { DailyStarlanguageState, Theme } from "@/lib/home-types";
|
||||
import type { RectificationEntrySummary } from "@/lib/rectification-entry";
|
||||
|
||||
@@ -35,7 +36,11 @@ export type StarterHomeProps = {
|
||||
readonly rectificationErrorMessage: string;
|
||||
readonly starterThemes: readonly StarterHomeTheme[];
|
||||
readonly starterSuggestions: readonly StarterHomeSuggestion[];
|
||||
readonly startSuggestedConsultation: (text: string, theme: Theme) => void;
|
||||
readonly startSuggestedConsultation: (
|
||||
text: string,
|
||||
theme: Theme,
|
||||
entrypoint?: ConsultationEntrypoint | null,
|
||||
) => void;
|
||||
readonly onboardingError: string;
|
||||
};
|
||||
|
||||
@@ -141,7 +146,7 @@ export function StarterHome({
|
||||
type="button"
|
||||
aria-label={`${theme?.label || "开始"}:${item.text}`}
|
||||
disabled={productEntrypointsDisabled}
|
||||
onClick={() => void startSuggestedConsultation(item.text, item.theme)}
|
||||
onClick={() => void startSuggestedConsultation(item.text, item.theme, "guided_topic")}
|
||||
>
|
||||
<span className="starter-content">
|
||||
<b>{theme?.label || "开始"}</b>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { z } from "zod";
|
||||
export const consultationEntrypointSchema = z.enum([
|
||||
"daily_starlanguage",
|
||||
"birth_time_rectification",
|
||||
"guided_topic",
|
||||
]);
|
||||
|
||||
export type ConsultationEntrypoint = z.infer<typeof consultationEntrypointSchema>;
|
||||
@@ -61,7 +62,9 @@ function publicDailyExpansion(currentDate: string): ResolvedConsultationQuestion
|
||||
}
|
||||
|
||||
export function pinsConsultationDomains(entrypoint: ConsultationEntrypoint | undefined): boolean {
|
||||
return entrypoint === "daily_starlanguage" || entrypoint === "birth_time_rectification";
|
||||
return entrypoint === "daily_starlanguage"
|
||||
|| entrypoint === "birth_time_rectification"
|
||||
|| entrypoint === "guided_topic";
|
||||
}
|
||||
|
||||
export function resolveConsultationQuestion(
|
||||
@@ -91,6 +94,8 @@ export function resolveConsultationQuestion(
|
||||
"候选时间必须标为待验证,不能声称是出生记录中的确定分钟,也不能在没有新证据时循环重启相同流程。",
|
||||
].join("\n"),
|
||||
};
|
||||
case "guided_topic":
|
||||
return { kind: "plain", modelQuestion: input.visibleQuestion };
|
||||
default: {
|
||||
const exhaustive: never = input.entrypoint;
|
||||
return exhaustive;
|
||||
|
||||
@@ -56,6 +56,27 @@ export { AGENT_MAX_OUTPUT_TOKENS as CONSULTATION_MAX_OUTPUT_TOKENS } from "../li
|
||||
export const AGENT_MAX_STEPS = 8;
|
||||
export const AGENT_TIMEOUT_MS = 110_000;
|
||||
export const AGENT_SLICE_MAX_STEPS = 1;
|
||||
export const CONSULTATION_NATAL_CALC_TOOL_ID = "run-jyotish-consultation";
|
||||
|
||||
/**
|
||||
* Thinking-mode providers reject named/required tool_choice. Restrict the first
|
||||
* natal model step to the chart tool and keep tool_choice auto so the run cannot
|
||||
* spend that step on skill_read or a spoken Level 2 draft before any evidence
|
||||
* exists. Later steps leave the rest of the bound tools available.
|
||||
*
|
||||
* Window and general agents must not share this hook: they do not own this tool.
|
||||
*/
|
||||
export function consultationNatalPrepareStep(input: { stepNumber: number }) {
|
||||
return input.stepNumber === 0
|
||||
? {
|
||||
activeTools: [CONSULTATION_NATAL_CALC_TOOL_ID],
|
||||
toolChoice: "auto" as const,
|
||||
}
|
||||
: {
|
||||
toolChoice: "auto" as const,
|
||||
};
|
||||
}
|
||||
|
||||
const CONSULTATION_DOMAIN_DURATION_MS = 21_000;
|
||||
const CONSULTATION_ANSWER_RESERVE_MS = 45_000;
|
||||
export const CONSULTATION_DOMAIN_WALL_CLOCK_MS = AGENT_TIMEOUT_MS - CONSULTATION_ANSWER_RESERVE_MS;
|
||||
|
||||
Reference in New Issue
Block a user