fix(consult): stop the model spending its step budget on invalid tool params
A staging consultation calculated the chart and then returned nothing but the ensureFinalResponseText fallback. The model had made four calls to run-jyotish-consultation, and two of them never reached a calculation: they set both domains and theme, which canonicalDomainPlan rejects at execution. The schema declared those two fields as independent optionals, the description never mentioned the constraint, and the instructions actively told the model to use theme for a single-domain retry. Each attempt therefore bought a rule the contract never stated, and because the throw happens before the step-recording try/catch, it left no trace in the receipt either. Make the constraint unrepresentable instead of enforced. The model-facing schema keeps only question and domains, so Mastra refuses the pair before the tool body runs; the description states the single-array contract, and the instruction that advertised theme is gone. canonicalDomainPlan still resolves the single-value form for callers that build a plan without that schema, and is now exported so that path has its own tests. maxSteps and the abort timeout bound the same run but were hard-coded apart. One calculation takes about 20s against a 110s budget, so time is the binding constraint and three failed calculations exhaust it whatever the step count. The budget only has to cover the longest useful shape, so it moves to 8 beside the timeout with that reasoning recorded, and the recorded step list is sized to match so an exhausted run cannot truncate its own evidence. Step exhaustion was only ever inferable by counting events, since finishReason was recorded nowhere and progressive-disclosure reads never reach the public stream. Capture it as a closed enum plus a step count, normalizing anything unrecognized, and log both as controlled fields. Neither may enter the client receipt, whose step schema is strict and would fail a successful run. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -36,6 +36,7 @@ import { streamAgentResponse } from "@/lib/stream-agent-response";
|
||||
import type { AgentExecutionReceipt, WorkflowReceipt } from "@/lib/consultation-agent-events";
|
||||
import {
|
||||
createConsultationAgentContext,
|
||||
consultationModelStepTelemetry,
|
||||
consultationStepBudgetReceipt,
|
||||
createConsultationRuntimeHooks,
|
||||
createConsultationRuntimeState,
|
||||
@@ -62,6 +63,16 @@ import { z } from "zod";
|
||||
export const runtime = "nodejs";
|
||||
export const maxDuration = 120;
|
||||
|
||||
// These two limits bound the same Agent run and must be changed together. One
|
||||
// chart calculation takes about 20s and maxDuration caps the request near 120s,
|
||||
// so wall-clock time, not steps, is the binding constraint: three failed
|
||||
// calculations exhaust the timeout no matter how many steps remain. The step
|
||||
// budget therefore only has to cover the longest useful shape—skill load, a
|
||||
// couple of progressive-disclosure reference reads, one calculation plus one
|
||||
// retry, and the answer turn—since a larger budget cannot buy more time.
|
||||
const AGENT_MAX_STEPS = 8;
|
||||
const AGENT_TIMEOUT_MS = 110_000;
|
||||
|
||||
const chatRequestMetadataSchema = z.object({
|
||||
requestId: z.string().uuid(),
|
||||
sessionId: z.string().uuid(),
|
||||
@@ -513,7 +524,10 @@ export async function POST(request: Request) {
|
||||
name: string,
|
||||
generalDailyContext: GeneralDailyPanchangaContext | null,
|
||||
) {
|
||||
const state = createConsultationRuntimeState();
|
||||
// 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<Usage>[] = [];
|
||||
const agentStartedAt = Date.now();
|
||||
@@ -581,6 +595,7 @@ export async function POST(request: Request) {
|
||||
},
|
||||
],
|
||||
retryCount: Math.max(0, usages.length - 1),
|
||||
...consultationModelStepTelemetry(state),
|
||||
...(errorCode === undefined ? {} : { errorCode }),
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
@@ -621,10 +636,10 @@ export async function POST(request: Request) {
|
||||
].filter(Boolean).join("\n"),
|
||||
},
|
||||
];
|
||||
const agentAbortSignal = AbortSignal.timeout(110_000);
|
||||
const agentAbortSignal = AbortSignal.timeout(AGENT_TIMEOUT_MS);
|
||||
const streamOptions = {
|
||||
runId: requestId,
|
||||
maxSteps: 6,
|
||||
maxSteps: AGENT_MAX_STEPS,
|
||||
abortSignal: agentAbortSignal,
|
||||
hooks,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user