feat(consult): classify consultation workflow failures for diagnosis
Every workflow fault except abort and timeout collapsed into the single calculation_failed code, and the upstream message was discarded, so a failing run left no evidence of whether the API rejected the call or returned a payload that missed the response contract. Classify failures into a closed vocabulary carried on ConsultationWorkflowError and record it as the failureCode of the runtime step. The observability tool call schema gains one controlled optional field; upstream error text stays out of logs, as that contract requires. Forward request_id to the API so a run can be aligned with its access log. Build public receipts from an explicit allowlist. The internal failure code must not reach the client contract, whose step schema is strict and would otherwise reject a successful run. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,7 +5,13 @@ import {
|
||||
consultationStepBudgetReceipt,
|
||||
createConsultationTools,
|
||||
createConsultationRuntimeState,
|
||||
publicConsultationRuntimeSteps,
|
||||
} from "../src/mastra/consultation-tools.ts";
|
||||
import {
|
||||
ConsultationWorkflowError,
|
||||
consultationWorkflowFailureCode,
|
||||
} from "../src/mastra/consultation-workflow.ts";
|
||||
import { agentExecutionReceiptSchema } from "../src/lib/consultation-agent-events.ts";
|
||||
import { getJyotishAgent } from "../src/mastra/index.ts";
|
||||
import { consultationAgentPublicEventSchema, createNdjsonParser } from "../src/lib/consultation-agent-events.ts";
|
||||
import { createConsultationPlan } from "../src/lib/consultation-plan.ts";
|
||||
@@ -212,6 +218,64 @@ test("a rejected workflow promise is cleared before a later tool call", async ()
|
||||
assert.equal(state.consultationToolSuccessCount, 1);
|
||||
});
|
||||
|
||||
test("a failed calculation records why it failed and forwards the request id", async () => {
|
||||
const state = createConsultationRuntimeState();
|
||||
const seen: Array<string | undefined> = [];
|
||||
const tool = createConsultationTools({
|
||||
userId: "u", sessionId: "s", requestId: "req-correlation", consultationMode: "verified_chart",
|
||||
serverChart, state,
|
||||
runWorkflow: async (_input, options) => {
|
||||
seen.push(options?.requestId);
|
||||
throw new ConsultationWorkflowError("workflow_queue_full", "Async job queue is full");
|
||||
},
|
||||
})["run-jyotish-consultation"];
|
||||
|
||||
await assert.rejects(
|
||||
tool.execute!({ question: "队列满时的表现", theme: "career" }, { observe: { span: async (_n: string, fn: () => Promise<unknown>) => fn(), log() {} } } as never),
|
||||
/Async job queue is full/,
|
||||
);
|
||||
|
||||
assert.deepEqual(seen, ["req-correlation"]);
|
||||
const failedStep = state.steps.find((step) => step.status === "failed");
|
||||
assert.equal(failedStep?.name, "run-jyotish-consultation");
|
||||
assert.equal(failedStep?.failureCode, "workflow_queue_full");
|
||||
});
|
||||
|
||||
test("workflow failure codes classify transport and contract faults", () => {
|
||||
assert.equal(consultationWorkflowFailureCode(new ConsultationWorkflowError("workflow_rate_limited", "x")), "workflow_rate_limited");
|
||||
assert.equal(consultationWorkflowFailureCode(new DOMException("slow", "TimeoutError")), "workflow_timeout");
|
||||
assert.equal(consultationWorkflowFailureCode(new DOMException("stop", "AbortError")), "workflow_aborted");
|
||||
assert.equal(consultationWorkflowFailureCode(new Error("anything else")), undefined);
|
||||
});
|
||||
|
||||
test("the public receipt never carries the internal failure classification", () => {
|
||||
const state = createConsultationRuntimeState();
|
||||
appendConsultationRuntimeStep(state, {
|
||||
kind: "tool", name: "run-jyotish-consultation", status: "failed", durationMs: 12, failureCode: "workflow_server_error",
|
||||
});
|
||||
appendConsultationRuntimeStep(state, { kind: "skill", name: "jyotish-vedic-astrology", status: "completed" });
|
||||
|
||||
const steps = publicConsultationRuntimeSteps(state);
|
||||
assert.equal(steps.every((step) => !("failureCode" in step)), true);
|
||||
assert.equal(state.steps[0]?.failureCode, "workflow_server_error");
|
||||
|
||||
// A strict receipt schema would reject the internal field, so this also
|
||||
// guards the run from failing while building a successful response.
|
||||
const receipt = agentExecutionReceiptSchema.parse({
|
||||
runId: "run", runtime: "mastra-agentic",
|
||||
skill: { name: "jyotish-vedic-astrology", loaded: true },
|
||||
steps,
|
||||
workflow: { route: "career", status: "ready", preciseTiming: "blocked", missingLayers: [] },
|
||||
});
|
||||
assert.equal(receipt.steps.length, 2);
|
||||
assert.throws(() => agentExecutionReceiptSchema.parse({
|
||||
runId: "run", runtime: "mastra-agentic",
|
||||
skill: { name: "jyotish-vedic-astrology", loaded: true },
|
||||
steps: state.steps,
|
||||
workflow: { route: "career", status: "ready", preciseTiming: "blocked", missingLayers: [] },
|
||||
}));
|
||||
});
|
||||
|
||||
test("personal Agent exposes the Jyotish Skill and named server tool", async () => {
|
||||
const state = createConsultationRuntimeState();
|
||||
const agent = getJyotishAgent({
|
||||
|
||||
Reference in New Issue
Block a user