import { NextResponse } from "next/server"; import { consultationInputSchema, consultationWorkflowReceipt, getGeneralJyotishAgent, getJyotishAgent, getLegacyJyotishAgent, getWindowJyotishAgent, runConsultationWorkflow, } from "@/mastra"; import { blocksPromptExtraction } from "@/lib/consult-safety"; import { consultationDomainSchema } from "@/lib/consultation-domain-registry"; import { parseAgentReply } from "@/lib/agent-reply"; import { createConsultationReplyMetadata } from "@/lib/consultation-reply-metadata"; import { ConsultationPlanValidationError, createConsultationPlan, type ConsultationPlan, } from "@/lib/consultation-plan"; import { logAgentObservability, settlementTelemetryOutcome, toAgentObservabilityErrorCode, type AgentSettlementResult, } from "@/lib/agent-observability"; import { consultationEntrypointSchema, pinsConsultationDomains, resolveConsultationQuestion, shouldLoadGeneralDailyPanchanga, } from "@/lib/consultation-entrypoint"; import { CreditRpcError } from "@/lib/consultation-billing"; import { cachedHistoryMessage, cachedSystemMessage, mergePromptCacheUsage, promptCacheUsage } from "@/lib/agent-generation-settings"; import { FeaturePricingError, resolveFeaturePricing } from "@/lib/feature-pricing"; import { reserveConsultationModel } from "@/lib/consultation-model-selection"; import { resolveSessionLanguageModel, loadLanguageModelCatalog } from "@/lib/model-catalog"; import { jsonForSupabaseSetupFailure } from "@/lib/api/service-unavailable"; import { createAdminSupabaseClient } from "@/lib/supabase/admin"; import { createServerSupabaseClient } from "@/lib/supabase/server"; import { streamTextResponse } from "@/lib/stream-text-response"; import { consultationPublicActivityEvent, streamAgentResponse } from "@/lib/stream-agent-response"; import type { AgentExecutionReceipt, WorkflowReceipt } from "@/lib/consultation-agent-events"; import { consultationComposePrompt, consultationContinuePrompt, natalConsultationThinkingPlan, type PublicThinkingSection } from "@/lib/consultation-thinking-plan"; import { AGENT_MAX_STEPS, AGENT_TIMEOUT_MS, AGENT_SLICE_MAX_STEPS, consultationContinueGenerationSettings, consultationGenerationSettings, consultationNatalPrepareStep, consultationWindowPrepareStep, createConsultationAgentContext, createWindowConsultationAgentContext, precomputeWindowConsultation, windowPrecomputedPacketMessage, consultationModelStepTelemetry, consultationStepBudgetReceipt, createConsultationRuntimeHooks, createConsultationRuntimeState, publicConsultationRuntimeSteps, } from "@/mastra/consultation-tools"; import { applyBirthTimeModeToWorkflowContext, ACCEPTED_RANGE_READING_INSTRUCTION, consultationBirthTimeModeSchema, shouldRunBirthChartWorkflow, shouldRunDeclaredWindowWorkflow, type ConsultationBirthTimeMode, } from "@/lib/consultation-birth-time-mode"; import { ConsultationProfileTruthError, prepareConsultationRoute, type PreparedConsultationRoute, } from "@/lib/consultation-route-service"; import { loadReportCandidateRange } from "@/lib/report-candidate-range"; import { loadGeneralDailyPanchangaContext, type GeneralDailyPanchangaContext, } from "@/lib/general-daily-panchanga"; import { consultationHistoryWindow, consultationUserTurnContent, isContextOverflowError, lastConsultationPair, parseSessionContextSummary, } from "@/lib/consultation-session-history"; import { checkpointSessionContextSummary, generateSessionContextSummaryText, } from "@/lib/session-context-summary"; import { persistGuardedSessionTitle } from "@/lib/session-title-guard"; import { generateSessionTitle, shouldGenerateSessionTitle } from "@/lib/session-title-agent"; import { z } from "zod"; export const runtime = "nodejs"; export const maxDuration = 120; // The step budget, the wall-clock budget and the domain cap all bound this same // run, so they are declared as one group in @/mastra/consultation-tools with the // reasoning that ties them together. maxDuration above is the ceiling they must // stay under; raising it here without raising that is meaningless. const chatRequestMetadataSchema = z.object({ requestId: z.string().uuid(), sessionId: z.string().uuid(), modelId: z.string().trim().min(1).max(64), name: z.string().trim().max(80).optional().default(""), history: z .array( z.object({ role: z.enum(["user", "assistant"]), text: z.string().max(4000), }), ) .max(20) .optional() .default([]), }); const chartChatRequestSchema = consultationInputSchema.extend({ ...chatRequestMetadataSchema.shape, consultationMode: consultationBirthTimeModeSchema.exclude(["general_no_birth_time", "declared_birth_window"]) .optional() .default("verified_chart"), entrypoint: consultationEntrypointSchema.optional(), }).strict(); const generalChatRequestSchema = z.object({ ...chatRequestMetadataSchema.shape, consultationMode: z.literal("general_no_birth_time"), question: z.string().trim().min(1).max(500), theme: consultationDomainSchema, entrypoint: consultationEntrypointSchema.exclude(["birth_time_rectification"]).optional(), }).strict(); const windowChatRequestSchema = z.object({ ...chatRequestMetadataSchema.shape, consultationMode: z.literal("declared_birth_window"), question: z.string().trim().min(1).max(500), theme: consultationDomainSchema, entrypoint: consultationEntrypointSchema.exclude(["birth_time_rectification"]).optional(), }).strict(); const chatRequestSchema = z.union([ generalChatRequestSchema, windowChatRequestSchema, chartChatRequestSchema, ]); const consultationReservationSchema = z.object({ success: z.boolean(), reservation_id: z.string().uuid().nullable(), source: z.enum(["subscription", "credits"]).nullable(), credits: z.number().int().nullable(), subscription_id: z.string().uuid().nullable(), reason: z.string().nullable(), retry_after_seconds: z.number().int().nullable(), }); const consultationCompletionSchema = z.object({ success: z.boolean(), credits: z.number().int().nullable(), error_code: z.string().nullable().optional(), }); const consultationQuestionAppendSchema = z.object({ success: z.boolean(), error_code: z.string().nullable().optional(), }); function first(value: T | T[]): T { return Array.isArray(value) ? value[0] : value; } const detachedSettlementAttempts = 3; // ponytail: Staging MVP ceiling—without a queue/worker, detached settlement gets only three short retries. async function retryDetachedSettlement(action: () => Promise): Promise { let lastError: unknown; for (let attempt = 1; attempt <= detachedSettlementAttempts; attempt += 1) { try { return await action(); } catch (error) { lastError = error; if (attempt < detachedSettlementAttempts) { await new Promise((resolve) => setTimeout(resolve, attempt * 150)); } } } throw lastError; } function currentTimeContext(now = new Date()) { const chinaTime = new Date(now.getTime() + 8 * 60 * 60 * 1000) .toISOString() .replace("T", " ") .slice(0, 19); return `服务端当前时间(权威):${now.toISOString()};中国标准时间(UTC+8):${chinaTime}。涉及“现在、今天、今年、未来几个月”等相对时间时,以此为准。`; } function generalDailyContextPrompt(context: GeneralDailyPanchangaContext | null) { if (!context) return ""; return [ "以下是服务器计算并校验结构后的公共日历证据。只能在其边界内解释,不得补充个人命盘结论。", "", JSON.stringify(context), "", ].join("\n"); } function generalNoMinuteInstruction(hasPublicDaily: boolean) { return hasPublicDaily ? "当前是无出生分钟的公共今日趋势咨询。可依据服务器提供的公共 Panchanga 摘要回答,但不得计算或推断个人星盘;不得补 00:00、时段中点或任何候选分钟。" : "当前没有具体出生分钟。不得计算或推断个人星盘,也不得补 00:00、时段中点或任何候选分钟。请直接回答用户的问题:能答的部分照实说,需要出生分钟的部分明确标出限制。不要把整轮对话改成「一般知识或生时校正」二选一,也不要把服务端当前时间当成用户提供的出生时间。"; } function declaredWindowInstruction() { return "当前是声明出生窗口咨询,没有单一出生分钟。如需个人结构结论,必须调用 run-jyotish-window-consultation。探针时刻不是出生时间,不得把 00:00、时段中点、中午或任一探针写成出生分钟。只能根据稳定层作方向性回答;上升或宫位若在窗口内变化,列出可能星座,不得写成「你的上升是 X」。精确应期一律不可用。这只是粗看。"; } function usesPublicDailyGeneralAgent( consultationMode: ConsultationBirthTimeMode, generalDailyContext: GeneralDailyPanchangaContext | null, ) { return consultationMode === "general_no_birth_time" || (consultationMode === "declared_birth_window" && Boolean(generalDailyContext)); } function chinaCalendarDate(now: Date) { return new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString().slice(0, 10); } type Usage = { inputTokens?: number; outputTokens?: number; cache?: ReturnType; }; function mergeUsage(usages: Promise[]): Promise { return Promise.all(usages).then((items) => items.reduce((total, item) => { const usage = item && typeof item === "object" ? item as Record : {}; const cache = promptCacheUsage(usage); return { inputTokens: (total.inputTokens ?? 0) + (typeof usage.inputTokens === "number" ? usage.inputTokens : 0), outputTokens: (total.outputTokens ?? 0) + (typeof usage.outputTokens === "number" ? usage.outputTokens : 0), cache: mergePromptCacheUsage([total.cache, cache]), }; }, {} as Usage)); } function shouldUseAgenticRuntime(user: { id: string; app_metadata?: Record }) { const mode = process.env.CONSULTATION_AGENTIC_RUNTIME?.trim().toLowerCase() ?? "enabled"; if (mode === "legacy") return false; if (mode === "enabled") return true; if (mode !== "canary") return true; const ids = new Set((process.env.CONSULTATION_AGENTIC_CANARY_USER_IDS ?? "") .split(",").map((value) => value.trim()).filter(Boolean)); const roles = Array.isArray(user.app_metadata?.roles) ? user.app_metadata.roles : []; return ids.has(user.id) || user.app_metadata?.role === "admin" || roles.includes("admin"); } function workflowStatus(status: string | undefined): "ready" | "degraded" | "blocked" { return status === "ready" || status === "degraded" ? status : "blocked"; } export async function POST(request: Request) { let supabase: Awaited>; let accounting: ReturnType; try { supabase = await createServerSupabaseClient(); accounting = createAdminSupabaseClient(); } catch (error) { return jsonForSupabaseSetupFailure(error, "POST /api/consult"); } const { data: { user }, error: authError, } = await supabase.auth.getUser(); if (authError || !user) { return NextResponse.json( { error: "请先登录", message: "登录后才能开始咨询。" }, { status: 401 }, ); } const parsed = chatRequestSchema.safeParse( await request.json().catch(() => null), ); if (!parsed.success) { return NextResponse.json( { error: "出生资料或问题格式不正确", details: parsed.error.flatten() }, { status: 400 }, ); } const { data: chatSession, error: chatSessionError } = await supabase .from("chat_sessions") .select("id,model_id,model_config_version,session_type,messages,title,theme,chart_profile_role,context_summary") .eq("id", parsed.data.sessionId) .eq("user_id", user.id) .maybeSingle(); if (chatSessionError) { return NextResponse.json( { error: "暂时无法读取咨询会话", message: "请稍后重试。" }, { status: 503 }, ); } if (!chatSession) { return NextResponse.json( { error: "咨询会话不存在", message: "请重新进入咨询。" }, { status: 404 }, ); } if (chatSession.session_type !== "consultation") { return NextResponse.json( { error: "这是生时校正会话", code: "session_not_consultation", message: "请在生时校正里继续。", }, { status: 409 }, ); } if (!chatSession.model_id || chatSession.model_id !== parsed.data.modelId) { return NextResponse.json( { error: "会话模型已经变化", message: "请刷新会话后重新发送,本次不会扣点。" }, { status: 409 }, ); } const sessionModel = await resolveSessionLanguageModel( chatSession.model_id, chatSession.model_config_version, ); if (!sessionModel) { return NextResponse.json( { error: "模型暂不可用", message: "当前会话绑定的数据库模型配置不可用,请联系管理员,本次不会扣点。", }, { status: 503 }, ); } const sessionContextWindow = sessionModel.contextWindow; if (parsed.data.entrypoint === "birth_time_rectification") { return NextResponse.json( { error: "旧版生时校正入口已停用", message: "请从首页生时校正卡片开始或继续对话式校正,本次不会扣点。", }, { status: 409 }, ); } const requestTime = new Date(); const currentDate = chinaCalendarDate(requestTime); const userId = user.id; const requestId = parsed.data.requestId; const sessionId = parsed.data.sessionId; const consultationTheme = parsed.data.theme; const visibleQuestion = parsed.data.question; // Client `history` stays in the request schema for old bundles and is not read. const contextSummary = parseSessionContextSummary(chatSession.context_summary); const historyWindow = consultationHistoryWindow(chatSession.messages, contextSummary, { contextWindow: sessionContextWindow, }); const storedHistory = historyWindow.tail; const userControlledPrompt = [ parsed.data.question, historyWindow.summaryText, ...storedHistory.filter((message) => message.role === "user").map((message) => message.text), ].filter(Boolean).join("\n"); if (blocksPromptExtraction(userControlledPrompt)) { return NextResponse.json( { error: "无法处理该请求", message: "我不能提供系统提示词、技能原文或任何密钥。你可以继续询问占星相关问题。", }, { status: 400 }, ); } type SelectedModel = NonNullable>>; type ReservationResult = { success: boolean; credits: number | null; error_code: string | null }; type ModelSelection = Awaited>>; let prepared: PreparedConsultationRoute; try { prepared = await prepareConsultationRoute({ userId, mode: parsed.data.consultationMode, async loadProfile(profileUserId) { const { data, error } = await supabase .from("profiles") .select("name,birth_date,reported_birth_time,active_birth_time,birth_time_source,birth_time_period,declared_window_start,declared_window_end,birth_time_status,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source,ayanamsa") .eq("id", profileUserId) .single(); if (error || !data) throw new ConsultationProfileTruthError("profile_unavailable"); return data; }, loadCandidateRange: (profileUserId) => loadReportCandidateRange(accounting, { userId: profileUserId }), beforeReserve: ({ consultationMode }) => createConsultationPlan({ userIntent: parsed.data.question, theme: consultationTheme, consultationMode, modelCreditCost: sessionModel.creditCost, }), reserve: () => reserveConsultationModel( chatSession.model_id, (modelId) => sessionModel?.id === modelId ? sessionModel : null, async (model) => { const pricing = await resolveFeaturePricing(accounting, "chat.standard", model.id); const { data, error } = await accounting.rpc("reserve_consultation_usage", { p_user_id: userId, p_request_id: requestId, p_session_id: sessionId, p_requested_model_id: model.id, p_credit_cost: pricing.credit_cost, }); const reservation = consultationReservationSchema.safeParse(first(data ?? [])); if (error || !reservation.success) { throw new CreditRpcError(error?.message || "invalid_reservation_response"); } return { success: reservation.data.success, credits: reservation.data.credits, error_code: reservation.data.reason, }; }, ), }); } catch (error) { if (error instanceof FeaturePricingError) { console.error( `[billing] reservation failed request=${requestId} reason=${error.code}`, ); return NextResponse.json( { error: "计费配置不可用", message: "当前服务的计费配置尚未完成,请联系支持人员,本次不会扣点。", code: "pricing_configuration_unavailable", }, { status: 503 }, ); } if (error instanceof ConsultationPlanValidationError) { return NextResponse.json( { error: "当前咨询计划不可用", message: error.code === "plan_cost_ceiling_exceeded" ? "当前会话模型超出本次咨询的点数上限,请选择标准模型后重试,本次不会扣点。" : "当前咨询模式与回答精度边界不一致,请刷新后重试,本次不会扣点。", }, { status: 409 }, ); } if (error instanceof ConsultationProfileTruthError) { const modeChanged = error.code === "mode_changed"; return NextResponse.json( modeChanged ? { error: "出生时间状态已经变化", message: "请刷新后重新选择使用填报时间、一般咨询或先完成校正,本次不会扣点。", } : { error: "暂时无法核对完整出生资料", message: "出生日期、时间来源或出生地点资料不完整或不一致,请重新保存后再试,本次不会扣点。", }, { status: modeChanged ? 409 : 503 }, ); } const reason = error instanceof Error ? error.name : "UnknownError"; console.error( `[billing] reservation failed request=${requestId} reason=${reason}`, ); return NextResponse.json( { error: "暂时无法确认咨询点数", message: "请稍后重试。" }, { status: 503 }, ); } const resolvedQuestion = resolveConsultationQuestion({ visibleQuestion: parsed.data.question, entrypoint: parsed.data.entrypoint, currentDate, consultationMode: prepared.consultationMode, }); const modelSelection = prepared.reservation; if (modelSelection.status === "unavailable") { return NextResponse.json( { error: "模型暂不可用", message: "请选择其他模型后重新发送,本次不会扣除点数。", }, { status: 409 }, ); } const selectedModel = modelSelection.model; const reserveResult = modelSelection.reservation; if (!reserveResult.success) { const insufficient = reserveResult.error_code === "insufficient_credits"; return NextResponse.json( { error: insufficient ? "咨询点数不足" : "暂时无法扣除咨询点数", message: insufficient ? "请先兑换咨询点数后再继续。" : reserveResult.error_code || "请稍后重试。", }, { status: insufficient ? 402 : 503 }, ); } async function cancel(): Promise { try { const result = await retryDetachedSettlement(async () => { const { data, error } = await accounting.rpc("cancel_consultation_credit", { p_user_id: userId, p_request_id: requestId, }); const parsedCancellation = consultationCompletionSchema.safeParse(first(data ?? [])); if (error || !parsedCancellation.success) { throw new CreditRpcError(error?.message || "invalid_cancellation_response"); } return parsedCancellation.data; }); if (!result.success && result.error_code !== "request_completed") { throw new CreditRpcError(result.error_code || "cancellation_rejected"); } return "cancelled"; } catch (error) { const reason = error instanceof Error ? error.name : "UnknownError"; console.error( `[billing] cancellation failed request=${requestId} reason=${reason}`, ); return "failed"; } } let appendedQuestion: { success: boolean; error_code?: string | null }; try { appendedQuestion = await retryDetachedSettlement(async () => { const { data, error } = await accounting.rpc("append_consultation_question", { p_user_id: userId, p_request_id: requestId, p_session_id: sessionId, p_question_message: { role: "user", text: visibleQuestion }, }); const parsedAppend = consultationQuestionAppendSchema.safeParse(first(data ?? [])); if (error || !parsedAppend.success) { throw new CreditRpcError(error?.message || "invalid_question_append_response"); } return parsedAppend.data; }); } catch { await cancel(); return NextResponse.json( { error: "暂时无法保存问题", message: "请稍后重试。" }, { status: 503 }, ); } if (!appendedQuestion.success) { await cancel(); if (appendedQuestion.error_code === "session_full") { return NextResponse.json( { error: "这段对话已写满", message: "这段对话已写满,开个新对话继续吧", code: "session_full", }, { status: 409 }, ); } return NextResponse.json( { error: "暂时无法保存问题", message: "请稍后重试。" }, { status: 503 }, ); } const expectedTitle = typeof chatSession.title === "string" ? chatSession.title : ""; const { data: titleRow } = await supabase .from("chat_sessions") .select("title") .eq("id", sessionId) .eq("user_id", userId) .maybeSingle(); const usageStartedAt = Date.now(); async function checkpointConsultationContext() { try { const { data: sessionRow, error } = await supabase .from("chat_sessions") .select("messages, context_summary") .eq("id", sessionId) .eq("user_id", userId) .maybeSingle(); if (error || !sessionRow) return; const catalog = await loadLanguageModelCatalog(); const summaryModel = catalog.defaultModelId ? catalog.models.find((model) => model.id === catalog.defaultModelId) ?? selectedModel : selectedModel; await checkpointSessionContextSummary({ messages: sessionRow.messages, summary: sessionRow.context_summary, contextWindow: sessionContextWindow, generateText: (prompt, signal) => generateSessionContextSummaryText(summaryModel, prompt, signal), update: async (summary, seenUpdatedAt) => { let query = supabase.from("chat_sessions") .update({ context_summary: summary }) .eq("id", sessionId) .eq("user_id", userId); query = seenUpdatedAt ? query.eq("context_summary->>updatedAt", seenUpdatedAt) : query.is("context_summary", null); const { data, error: writeError } = await query.select("id"); if (writeError) throw writeError; return Boolean(data?.length); }, }); } catch (error) { console.warn("session context summary failed", error); } } async function usagePayload(usage: Promise<{ inputTokens?: number; outputTokens?: number }>) { const resolved = await usage; const usageRecord = resolved as Record; const cache = promptCacheUsage(usageRecord); const inputTokens = Math.max(0, Math.trunc(typeof usageRecord.inputTokens === "number" ? usageRecord.inputTokens : 0)); const outputTokens = Math.max(0, Math.trunc(typeof usageRecord.outputTokens === "number" ? usageRecord.outputTokens : 0)); return { eventKey: requestId, actualModelId: selectedModel.id, modelConfigVersion: selectedModel.configVersion, inputTokens, outputTokens, costMicrousd: Math.round(( inputTokens * (selectedModel.inputCostMicrousdPerMillion ?? 0) + outputTokens * (selectedModel.outputCostMicrousdPerMillion ?? 0) ) / 1_000_000), durationMs: Date.now() - usageStartedAt, ...(cache ? { metadata: { cache: { ...cache, hit: cache.readTokens > 0 } } } : {}), }; } async function completeResponse( rawTransformedText: string, usage: Promise<{ inputTokens?: number; outputTokens?: number }>, techniqueTruth: string, workflowReceipt: WorkflowReceipt, agentExecutionReceipt?: AgentExecutionReceipt, thinkingText?: string, thinkingSections?: PublicThinkingSection[], ): Promise { try { const reply = parseAgentReply( rawTransformedText, createConsultationReplyMetadata({ question: visibleQuestion }), ); if (!reply.text) throw new Error("empty_agent_reply"); const persistedThinking = thinkingText?.trim().slice(0, 4_000); const responseMessage = { role: "assistant" as const, text: reply.text, ...(persistedThinking ? { thinkingText: persistedThinking } : {}), ...(thinkingSections?.length ? { thinkingSections } : {}), techniqueTruth, workflowReceipt, ...(agentExecutionReceipt ? { agentExecutionReceipt } : {}), }; const actualUsage = await usagePayload(usage); const completion = await retryDetachedSettlement(async () => { const { data, error } = await accounting.rpc("complete_consultation_response", { p_user_id: userId, p_request_id: requestId, p_session_id: sessionId, p_response_message: responseMessage, p_actual_usage: actualUsage, }); const parsedCompletion = consultationCompletionSchema.safeParse(first(data ?? [])); if (error || !parsedCompletion.success) { throw new CreditRpcError(error?.message || "invalid_completion_response"); } return parsedCompletion.data; }); if (!completion.success && completion.error_code !== "request_cancelled") { throw new CreditRpcError(completion.error_code || "completion_rejected"); } if (completion.success) { void checkpointConsultationContext(); } return "completed"; } catch (error) { await cancel(); throw error; } } let settlement: Promise | null = null; function settleResult(action: () => Promise) { settlement ??= action(); return settlement; } async function settle(action: () => Promise): Promise { await settleResult(action); } // A run that fails before streamAgentResponse exists never reaches its error // path, so the closed observability event—the only place the tool failure // code, per-step durations and the model finish reason are recorded—would be // lost for exactly the runs that failed hardest. The agentic setup publishes // its settle-and-log entry point here so the request-level catch below can // still emit it. const agenticFailure: { report?: (error: unknown) => Promise } = {}; const titleAfterRpc = typeof titleRow?.title === "string" ? titleRow.title : expectedTitle; const titleSideEvent = shouldGenerateSessionTitle({ title: expectedTitle, sessionType: chatSession.session_type, }, storedHistory) ? generateSessionTitle({ model: selectedModel, question: parsed.data.question, theme: consultationTheme, chartRole: chatSession.chart_profile_role === "other" ? "other" : "self", signal: request.signal, }).then(async (title) => { if (!title) return null; await persistGuardedSessionTitle({ client: supabase, sessionId, userId, expectedTitle: titleAfterRpc, title, }); return { type: "session.title" as const, title }; }).catch((error) => { console.warn("session title failed", error); return null; }) : Promise.resolve(null); async function runAgenticConsultation( consultationMode: ConsultationBirthTimeMode, history: Array<{ role: "user" | "assistant"; text: string }>, name: string, generalDailyContext: GeneralDailyPanchangaContext | null, ) { // The recorded step list has to be able to hold everything the model loop // can produce, otherwise a run that exhausts its steps also truncates the // evidence of having done so. const state = createConsultationRuntimeState({ plannedSteps: AGENT_MAX_STEPS }); const hooks = createConsultationRuntimeHooks(state); const usages: Promise[] = []; const agentStartedAt = Date.now(); let firstActivityMs = -1; let firstTextMs = -1; let logged = false; const markFirstActivity = () => { if (firstActivityMs < 0) firstActivityMs = Date.now() - agentStartedAt; }; const markFirstText = () => { if (firstTextMs < 0) firstTextMs = Date.now() - agentStartedAt; }; const consultEntrypoint = parsed.success ? parsed.data.entrypoint : undefined; const logRun = async ( errorCode: string | undefined, settlementResult: AgentSettlementResult, ) => { if (logged) return; logged = true; const resolvedUsage: Usage = await mergeUsage(usages).catch(() => ({})); const inputTokens = Math.max(0, Math.trunc(resolvedUsage.inputTokens ?? 0)); const outputTokens = Math.max(0, Math.trunc(resolvedUsage.outputTokens ?? 0)); const skillStep = state.steps.find((step) => step.kind === "skill"); const runStatus = errorCode === undefined ? "completed" : errorCode === "cancelled" ? "cancelled" : "failed"; logAgentObservability({ runId: requestId, requestId, sessionId, agentVersion: "consultation-agentic-v1", modelVersion: String(selectedModel.configVersion), policyVersion: "consultation-runtime-contract-v1", toolCalls: state.steps .filter((step) => step.kind === "tool") .map((step) => ({ name: step.name, durationMs: Math.max(0, Math.trunc(step.durationMs ?? 0)), status: step.status, ...(step.failureCode ? { failureCode: step.failureCode } : {}), })), contractPhases: [ { phase: "skill.load", ...(skillStep?.durationMs === undefined ? {} : { durationMs: Math.max(0, Math.trunc(skillStep.durationMs)) }), status: state.jyotishSkillBound ? "completed" : "failed", }, { phase: "answer.first_activity", ...(firstActivityMs < 0 ? {} : { durationMs: firstActivityMs }), status: firstActivityMs < 0 ? "skipped" : "completed", }, { phase: "answer.first_output", ...(firstTextMs < 0 ? {} : { durationMs: firstTextMs }), status: firstTextMs < 0 ? "skipped" : "completed", }, { phase: "run.total", durationMs: Date.now() - agentStartedAt, status: runStatus, }, { phase: "billing.settled", status: settlementResult, }, ], retryCount: Math.max(0, usages.length - 1), ...consultationModelStepTelemetry(state), ...(errorCode === undefined ? {} : { errorCode }), inputTokens, outputTokens, themeCoverage: state.workflowReceipt?.domains ?? [consultationTheme], billingSettlementResult: settlementResult, }); }; const settleRun = async ( action: () => Promise, errorCode: string | undefined, ) => { try { const actualSettlementResult = await settleResult(action); const outcome = settlementTelemetryOutcome(actualSettlementResult, errorCode); await logRun(outcome.errorCode, outcome.billingSettlementResult); } catch (error) { const outcome = settlementTelemetryOutcome("failed", errorCode); await logRun(outcome.errorCode, outcome.billingSettlementResult); throw error; } }; agenticFailure.report = async (error) => { try { await settleRun(cancel, toAgentObservabilityErrorCode(error)); } catch { // Settlement already reported itself through logRun; the request-level // failure response must not depend on it succeeding. } }; const cacheBoundary = cachedSystemMessage("【上下文缓存边界】后续内容为本轮请求输入。", selectedModel.model); const natalToolInstruction = pinsConsultationDomains(consultEntrypoint) ? "如需新的个人星盘结论,必须调用服务器绑定的排盘工具。调用时不要填写 domains,沿用服务器已选定的主题。" : "如需新的个人星盘结论,必须调用服务器绑定的排盘工具。"; const natalInstruction = consultEntrypoint === "daily_starlanguage" ? `${natalToolInstruction}按三节写:今日趋势、适合推进 / 需要避开、一个行动(把技法审计表和「探索性日提示,不是确定预测」放进最后一节)。不要复述内部 JSON 字段。` : `${natalToolInstruction}事业/财富/婚恋/家庭先给口语开场,再按四个标题写结论:先回答你的问题、盘里支持这个判断的地方、时间怎么看、这周可以做的一件事。不要把统一参数或技法审计表写进正文。不要复述内部 JSON 字段。`; const adoptedRangeNote = consultationMode === "verified_chart" && prepared.serverChart?.truth.birthTimeStatus === "accepted" && prepared.serverChart.toolInput.candidate_range ? ` ${ACCEPTED_RANGE_READING_INSTRUCTION}` : ""; const modeInstruction = consultationMode === "general_no_birth_time" || (consultationMode === "declared_birth_window" && generalDailyContext) ? generalNoMinuteInstruction(Boolean(generalDailyContext)) : consultationMode === "declared_birth_window" ? declaredWindowInstruction() : `${natalInstruction}${adoptedRangeNote}`; const consultationBaseMessages = (overflow: boolean) => { const tail = overflow ? lastConsultationPair(history) : history; const mapped = tail.map((message) => message.role === "user" ? { role: "user" as const, content: message.text } : { role: "assistant" as const, content: message.text }); const cachedTail = mapped.length > 0 ? [...mapped.slice(0, -1), cachedHistoryMessage(mapped[mapped.length - 1]!, selectedModel.model)] : mapped; return [ ...(cacheBoundary ? [cacheBoundary] : []), ...cachedTail, { role: "user" as const, content: consultationUserTurnContent({ currentTime: currentTimeContext(requestTime), name, instruction: modeInstruction, extra: generalDailyContextPrompt(generalDailyContext), summaryText: historyWindow.summaryText, droppedCount: historyWindow.droppedCount, question: resolvedQuestion.modelQuestion, }), }, ]; }; let baseMessages = consultationBaseMessages(false); const agentAbortSignal = AbortSignal.timeout(AGENT_TIMEOUT_MS); const streamOptions = { runId: requestId, maxSteps: AGENT_MAX_STEPS, abortSignal: agentAbortSignal, hooks, ...consultationGenerationSettings(selectedModel.model), }; const natalStreamOptions = { ...streamOptions, prepareStep: consultationNatalPrepareStep, }; const windowStreamOptions = { ...streamOptions, prepareStep: consultationWindowPrepareStep, }; async function streamWithOverflowRetry( agent: { stream: ( messages: typeof baseMessages, options: typeof streamOptions | typeof natalStreamOptions | typeof windowStreamOptions, ) => Promise<{ fullStream: AsyncIterable | ReadableStream; totalUsage: Promise }>; }, options: typeof streamOptions | typeof natalStreamOptions | typeof windowStreamOptions = streamOptions, ) { try { const result = await agent.stream(baseMessages, options); usages.push(result.totalUsage); return result; } catch (error) { if (!isContextOverflowError(error)) throw error; // Same request, same wait: shrink to summary + last pair. Consultation // 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, options); usages.push(overflow.totalUsage); return overflow; } } const workflowReceipt: WorkflowReceipt = usesPublicDailyGeneralAgent(consultationMode, generalDailyContext) ? { route: generalDailyContext ? "general-daily-panchanga" : "general-no-birth-time", status: "ready", preciseTiming: "blocked", missingLayers: ["birth-minute"], } : consultationMode === "declared_birth_window" ? { route: "declared-birth-window", status: "blocked", preciseTiming: "blocked", missingLayers: ["birth-minute"], } : { route: "pending", status: "blocked", preciseTiming: "blocked", missingLayers: [] }; if (usesPublicDailyGeneralAgent(consultationMode, generalDailyContext)) { state.workflowReceipt = workflowReceipt; const agent = getGeneralJyotishAgent(selectedModel); const result = await streamWithOverflowRetry(agent); // This mode has no calculation to require and no chart method to bind, // so there is no contract for a retry to repair. const retryForAnswer = async () => { const retried = await agent.stream([ ...baseMessages, { role: "user" as const, content: "上一轮没有输出任何回答文本。请直接给出这个问题的回答,不要只说明过程。" }, ], streamOptions); usages.push(retried.totalUsage); return retried.fullStream; }; const continueAfterLength = async (output: string) => { const continued = await agent.stream([ ...baseMessages, { role: "assistant" as const, content: output }, { role: "user" as const, content: consultationContinuePrompt(output) }, ], { ...streamOptions, ...consultationContinueGenerationSettings(selectedModel.model), }); usages.push(continued.totalUsage); return continued.fullStream; }; const executionReceipt = (): AgentExecutionReceipt => ({ runId: requestId, runtime: "mastra-agentic", skill: { name: "jyotish-vedic-astrology", loaded: state.jyotishSkillBound, referenceReads: state.skillReferenceReadCount, methodologySections: state.methodologySectionCount, }, steps: publicConsultationRuntimeSteps(state), stepBudget: consultationStepBudgetReceipt(state), workflow: workflowReceipt, techniqueTruth: generalDailyContext ? "public-panchanga-only" : "not-applicable", ...(state.techniqueAuditTable?.length ? { techniqueAuditTable: state.techniqueAuditTable } : {}), }); return streamAgentResponse({ runId: requestId, requestId, sideEvent: titleSideEvent, state, stream: result.fullStream, requireTool: false, retryForAnswer, continueAfterLength, continueAfterDisconnect: true, pass4Mode: generalDailyContext ? "general_no_birth_time" : consultationMode, toolStatus: () => "ready", receipt: executionReceipt, headers: { "x-jyotish-birth-time-mode": consultationMode }, onFirstActivity: markFirstActivity, onFirstOutput: markFirstText, onComplete: (output, agentExecutionReceipt, thinkingText, thinkingSections) => settleRun(() => completeResponse( output, mergeUsage(usages), generalDailyContext ? "public-panchanga-only" : "not-applicable", workflowReceipt, agentExecutionReceipt, thinkingText, thinkingSections, ), undefined), onError: (error) => settleRun( cancel, toAgentObservabilityErrorCode(error), ), onCancel: () => settleRun(cancel, "cancelled"), }); } if (shouldRunDeclaredWindowWorkflow(consultationMode)) { if (!prepared.declaredWindow) throw new Error("declared_window_truth_missing"); const agentContext = createWindowConsultationAgentContext({ userId, sessionId, requestId, consultationMode: "declared_birth_window", plan: prepared.preReserveResult, theme: consultationTheme, declaredWindow: prepared.declaredWindow, abortSignal: agentAbortSignal, state, }); const agent = getWindowJyotishAgent(selectedModel, agentContext); const retry = async () => { const retried = await agent.stream([ ...baseMessages, { role: "user" as const, content: "运行合同不完整:本次尚未取得声明窗口计算结果。请调用 run-jyotish-window-consultation 完成计算,再据此回答;不要在工具参数中添加出生分钟。", }, ], windowStreamOptions); usages.push(retried.totalUsage); return retried.fullStream; }; const retryForAnswer = async () => { const retried = await agent.stream([ ...baseMessages, { role: "user" as const, content: "服务器窗口计算已经完成,但上一轮没有输出任何回答文本。请重新取回本次计算结果,然后直接给出回答;不要只描述过程或工具调用。", }, ], streamOptions); usages.push(retried.totalUsage); return retried.fullStream; }; const continueAfterLength = async (output: string) => { const continued = await agent.stream([ ...baseMessages, { role: "assistant" as const, content: output }, { role: "user" as const, content: consultationContinuePrompt(output) }, ], { ...streamOptions, ...consultationContinueGenerationSettings(selectedModel.model), }); usages.push(continued.totalUsage); return continued.fullStream; }; const executionReceipt = (): AgentExecutionReceipt => ({ runId: requestId, runtime: "mastra-agentic", skill: { name: "jyotish-vedic-astrology", loaded: state.jyotishSkillBound, referenceReads: state.skillReferenceReadCount, methodologySections: state.methodologySectionCount, }, steps: publicConsultationRuntimeSteps(state), stepBudget: consultationStepBudgetReceipt(state), workflow: state.workflowReceipt ?? workflowReceipt, techniqueTruth: state.techniqueTruth ?? "declared-window", ...(state.techniqueAuditTable?.length ? { techniqueAuditTable: state.techniqueAuditTable } : {}), }); return streamAgentResponse({ runId: requestId, requestId, sideEvent: titleSideEvent, state, warmup: async (send) => { try { const packet = await precomputeWindowConsultation(agentContext, { question: resolvedQuestion.modelQuestion, writer: { custom: async (chunk) => { if (!chunk || typeof chunk !== "object") return; const value = chunk as { type?: unknown; data?: unknown }; if (value.type !== "data-jyotish-activity") return; const event = consultationPublicActivityEvent(value.data); if (event) send(event); }, }, }); baseMessages = [ ...baseMessages, { role: "user" as const, content: windowPrecomputedPacketMessage(packet) }, ]; } catch { // Leave the contract red; the model may still call the tool. } }, stream: async () => { const streamed = await streamWithOverflowRetry(agent, windowStreamOptions); return streamed.fullStream; }, requireTool: true, retry, retryForAnswer, continueAfterLength, continueAfterDisconnect: true, pass4Mode: consultationMode, toolStatus: () => workflowStatus(state.workflowReceipt?.status), receipt: executionReceipt, headers: { "x-jyotish-birth-time-mode": consultationMode }, onFirstActivity: markFirstActivity, onFirstOutput: markFirstText, onComplete: (output, agentExecutionReceipt, thinkingText, thinkingSections) => settleRun(() => completeResponse( output, mergeUsage(usages), state.techniqueTruth ?? "declared-window", state.workflowReceipt ?? workflowReceipt, agentExecutionReceipt, thinkingText, thinkingSections, ), undefined), onError: (error) => settleRun( cancel, toAgentObservabilityErrorCode(error), ), onCancel: () => settleRun(cancel, "cancelled"), }); } if (!prepared.serverChart) throw new Error("server_chart_truth_missing"); state.thinkingPlan = natalConsultationThinkingPlan({ domains: [consultationTheme], }); const agentContext = createConsultationAgentContext({ userId, sessionId, requestId, consultationMode: consultationMode === "verified_chart" ? "verified_chart" : "unverified_birth_time", plan: prepared.preReserveResult, theme: consultationTheme, entrypoint: consultEntrypoint, serverChart: prepared.serverChart, abortSignal: agentAbortSignal, state, }); const agent = getJyotishAgent(selectedModel, agentContext); const result = await streamWithOverflowRetry(agent, natalStreamOptions); const retry = async () => { const retried = await agent.stream([ ...baseMessages, { role: "user" as const, content: "运行合同不完整:本次尚未取得服务器计算结果。请调用 run-jyotish-consultation 完成计算,再据此回答;不要在工具参数中添加出生资料。", }, ], natalStreamOptions); usages.push(retried.totalUsage); return retried.fullStream; }; // The tool caches this request's calculation, so this attempt gets the same // evidence back without paying for it twice; keeping the tools available is // what puts that evidence in front of the model at all. const retryForAnswer = async () => { const retried = await agent.stream([ ...baseMessages, { role: "user" as const, content: "服务器计算已经完成,但上一轮没有输出任何回答文本。请重新取回本次计算结果,然后直接给出回答;不要只描述过程或工具调用。", }, ], natalStreamOptions); usages.push(retried.totalUsage); return retried.fullStream; }; const continueAfterLength = async (output: string) => { const continued = await agent.stream([ ...baseMessages, { role: "assistant" as const, content: output }, { role: "user" as const, content: consultationContinuePrompt(output) }, ], { ...streamOptions, ...consultationContinueGenerationSettings(selectedModel.model), }); usages.push(continued.totalUsage); return continued.fullStream; }; const interpretFindings = async () => ( (state.thinkingPlan ?? []).map((section) => ({ id: section.id })) ); const composeAnswer = async (_: readonly { id: string; text?: string }[] = [], retryHint?: string) => { const composed = await agent.stream([ ...baseMessages, { role: "user" as const, content: `${consultationComposePrompt()}${retryHint ? `\n${retryHint}` : ""}` }, ], { ...streamOptions, maxSteps: AGENT_SLICE_MAX_STEPS, toolChoice: "none", ...consultationContinueGenerationSettings(selectedModel.model), }); usages.push(composed.totalUsage); return composed.fullStream; }; const executionReceipt = (): AgentExecutionReceipt => ({ runId: requestId, runtime: "mastra-agentic", skill: { name: "jyotish-vedic-astrology", loaded: state.jyotishSkillBound, referenceReads: state.skillReferenceReadCount, methodologySections: state.methodologySectionCount, }, steps: publicConsultationRuntimeSteps(state), stepBudget: consultationStepBudgetReceipt(state), workflow: state.workflowReceipt ?? workflowReceipt, techniqueTruth: state.techniqueTruth ?? "unknown", ...(state.techniqueAuditTable?.length ? { techniqueAuditTable: state.techniqueAuditTable } : {}), }); return streamAgentResponse({ runId: requestId, requestId, sideEvent: titleSideEvent, state, stream: result.fullStream, requireTool: true, retry, retryForAnswer, continueAfterLength, interpretFindings, composeAnswer, pass4Mode: consultationMode, continueAfterDisconnect: true, toolStatus: () => workflowStatus(state.workflowReceipt?.status), receipt: executionReceipt, headers: { "x-jyotish-birth-time-mode": consultationMode }, onFirstActivity: markFirstActivity, onFirstOutput: markFirstText, onComplete: (output, agentExecutionReceipt, thinkingText, thinkingSections) => settleRun(() => completeResponse( output, mergeUsage(usages), state.techniqueTruth ?? "unknown", state.workflowReceipt ?? workflowReceipt, agentExecutionReceipt, thinkingText, thinkingSections, ), undefined), onError: (error) => settleRun( cancel, toAgentObservabilityErrorCode(error), ), onCancel: () => settleRun(cancel, "cancelled"), }); } try { const history = storedHistory; const name = prepared.serverChart?.name ?? prepared.declaredWindow?.name ?? parsed.data.name; const consultationMode: ConsultationBirthTimeMode = prepared.consultationMode; const generalDailyContext = shouldLoadGeneralDailyPanchanga({ consultationMode, entrypoint: parsed.data.entrypoint, visibleQuestion: parsed.data.question, }) ? await loadGeneralDailyPanchangaContext({ date: currentDate, reference: prepared.generalDailyReference, }) : null; if (shouldUseAgenticRuntime(user) || shouldRunDeclaredWindowWorkflow(consultationMode)) { return await runAgenticConsultation(consultationMode, history, name, generalDailyContext); } if (!shouldRunBirthChartWorkflow(consultationMode)) { const cacheBoundary = cachedSystemMessage("【上下文缓存边界】后续内容为本轮请求输入。", selectedModel.model); const result = await getGeneralJyotishAgent(selectedModel).stream([ ...(cacheBoundary ? [cacheBoundary] : []), { role: "user", content: consultationUserTurnContent({ currentTime: currentTimeContext(requestTime), name, instruction: generalNoMinuteInstruction(Boolean(generalDailyContext)), extra: generalDailyContextPrompt(generalDailyContext), summaryText: historyWindow.summaryText, droppedCount: historyWindow.droppedCount, question: resolvedQuestion.modelQuestion, }), }, ]); const workflowReceipt: WorkflowReceipt = { route: generalDailyContext ? "general-daily-panchanga" : "general-no-birth-time", status: "ready", preciseTiming: "blocked", missingLayers: ["birth-minute"], }; const settleErrored = (emitted: boolean, output: string) => settle( emitted ? () => completeResponse( output, result.totalUsage, generalDailyContext ? "public-panchanga-only" : "not-applicable", workflowReceipt, ) : cancel, ); return streamTextResponse(result.textStream, { mode: "mastra", requestId, continueAfterDisconnect: true, headers: { "x-jyotish-workflow-route": workflowReceipt.route, "x-jyotish-workflow-status": workflowReceipt.status, "x-jyotish-technique-truth": generalDailyContext ? "public-panchanga-only" : "not-applicable", "x-jyotish-precise-timing": workflowReceipt.preciseTiming, "x-jyotish-missing-layers": workflowReceipt.missingLayers.join(","), "x-jyotish-birth-time-mode": consultationMode, }, onComplete: (rawTransformedText) => settle(() => completeResponse( rawTransformedText, result.totalUsage, generalDailyContext ? "public-panchanga-only" : "not-applicable", workflowReceipt, )), onError: (_error, emitted, output: string) => settleErrored(emitted, output), onCancel: () => settle(cancel), }); } if (!prepared.serverChart) throw new Error("server_chart_truth_missing"); const toolInput = consultationInputSchema.parse({ ...prepared.serverChart.toolInput, // A user-reported concrete minute is a normal chart calculation. Keep its // provenance, but let server evidence—not rectification purchase state—own // precise-timing permission. Never reactivate the retired questionnaire. entryMode: "direct_chart", question: resolvedQuestion.modelQuestion, theme: parsed.data.theme, }); const workflowContext = applyBirthTimeModeToWorkflowContext( await runConsultationWorkflow(toolInput, { foreground: true, plan: prepared.preReserveResult, }), consultationMode, { birthTimeSource: prepared.serverChart.truth.birthTimeSource }, ); const workflowReceipt = consultationWorkflowReceipt(workflowContext); const cacheBoundary = cachedSystemMessage("【上下文缓存边界】后续内容为本轮请求输入。", selectedModel.model); const legacyHistory = history.map((message) => message.role === "user" ? { role: "user" as const, content: message.text } : { role: "assistant" as const, content: message.text }); const cachedLegacyHistory = legacyHistory.length > 0 ? [...legacyHistory.slice(0, -1), cachedHistoryMessage(legacyHistory[legacyHistory.length - 1]!, selectedModel.model)] : legacyHistory; const legacyMessages = [ ...(cacheBoundary ? [cacheBoundary] : []), ...cachedLegacyHistory, { role: "user" as const, content: consultationUserTurnContent({ currentTime: currentTimeContext(requestTime), name, instruction: "先用不超过 400 字口语直接回答下面的问题,不要加标题;形状为:先给反差(表面 A,底下 B,命名成一个格局)、再说清谁在推谁在修、然后「别去应 X 的象,去扮演 Y 的象」、收尾最多三条短行动(破折号短句,每条不超过 20 字);然后再按 skill Level 2 骨架写:原始结构、六步宫位、Yoga 表、时机、综合、文末技法审计表,最后才是现代生活。骨架不可省略。星盘事实只使用系统里已经注入的计算结果,不要复述内部字段、JSON 或再跑一遍咨询流程。", summaryText: historyWindow.summaryText, droppedCount: historyWindow.droppedCount, question: resolvedQuestion.modelQuestion, }), }, ]; const legacyAgent = getLegacyJyotishAgent(selectedModel, workflowContext); let result; try { result = await legacyAgent.stream(legacyMessages); } catch (error) { if (!isContextOverflowError(error)) throw error; const overflowHistory = lastConsultationPair(legacyHistory); const overflowCached = overflowHistory.length > 0 ? [...overflowHistory.slice(0, -1), cachedHistoryMessage(overflowHistory[overflowHistory.length - 1]!, selectedModel.model)] : overflowHistory; result = await legacyAgent.stream([ ...(cacheBoundary ? [cacheBoundary] : []), ...overflowCached, { role: "user" as const, content: consultationUserTurnContent({ currentTime: currentTimeContext(requestTime), name, instruction: "先用不超过 400 字口语直接回答下面的问题,不要加标题;形状为:先给反差(表面 A,底下 B,命名成一个格局)、再说清谁在推谁在修、然后「别去应 X 的象,去扮演 Y 的象」、收尾最多三条短行动(破折号短句,每条不超过 20 字);然后再按 skill Level 2 骨架写:原始结构、六步宫位、Yoga 表、时机、综合、文末技法审计表,最后才是现代生活。骨架不可省略。星盘事实只使用系统里已经注入的计算结果,不要复述内部字段、JSON 或再跑一遍咨询流程。", summaryText: historyWindow.summaryText, droppedCount: historyWindow.droppedCount, question: resolvedQuestion.modelQuestion, }), }, ]); } const responseWorkflowReceipt = { route: workflowReceipt.route, status: workflowReceipt.status, preciseTiming: workflowReceipt.preciseTiming, missingLayers: workflowReceipt.missingLayers === "none" ? [] : workflowReceipt.missingLayers.split(",").map((item) => item.trim()).filter(Boolean), }; const settleErrored = (emitted: boolean, output: string) => settle( emitted ? () => completeResponse( output, result.totalUsage, workflowReceipt.techniqueTruth, responseWorkflowReceipt, ) : cancel, ); return streamTextResponse(result.textStream, { mode: "mastra", requestId, continueAfterDisconnect: true, headers: { "x-jyotish-workflow-route": workflowReceipt.route, "x-jyotish-workflow-status": workflowReceipt.status, "x-jyotish-technique-truth": workflowReceipt.techniqueTruth, "x-jyotish-precise-timing": workflowReceipt.preciseTiming, "x-jyotish-missing-layers": workflowReceipt.missingLayers, "x-jyotish-birth-time-mode": consultationMode, }, onComplete: (rawTransformedText) => settle(() => completeResponse( rawTransformedText, result.totalUsage, workflowReceipt.techniqueTruth, responseWorkflowReceipt, )), onError: (_error, emitted, output: string) => settleErrored(emitted, output), onCancel: () => settle(cancel), }); } catch (error) { if (agenticFailure.report) await agenticFailure.report(error); else await cancel(); const reason = error instanceof Error ? error.name : "UnknownError"; console.error( `[consult] generation failed request=${requestId} model=${selectedModel.id} reason=${reason}`, ); return NextResponse.json( { error: "暂时无法生成解读", message: "咨询服务暂时不可用,请稍后再试。", recovery: "稍后重试,或换一个模型继续。", }, { status: 503 }, ); } }