fix(consultation): ensure final response after tool completion
This commit is contained in:
@@ -32,6 +32,14 @@ async function* readChunks(stream: ChunkStream): AsyncIterable<Chunk> {
|
||||
}
|
||||
type Status = "ready" | "degraded" | "blocked";
|
||||
|
||||
export const ENSURE_FINAL_RESPONSE_FALLBACK =
|
||||
"本次计算已完成,但暂时没有生成可展示的回答。请换一个角度提问,我会基于已完成的计算继续说明。";
|
||||
|
||||
export function ensureFinalResponseText(output: string, contractIsReady: boolean) {
|
||||
if (!contractIsReady || /\S/.test(output)) return null;
|
||||
return ENSURE_FINAL_RESPONSE_FALLBACK;
|
||||
}
|
||||
|
||||
type EventOptions = {
|
||||
runId: string;
|
||||
requestId: string;
|
||||
@@ -202,6 +210,19 @@ export function streamAgentResponse(options: StreamAgentResponseOptions) {
|
||||
await consumeAttempt(controller, await options.retry());
|
||||
}
|
||||
if (!contractReady(options)) throw new Error("runtime_contract_incomplete");
|
||||
const ensuredFinalResponse = ensureFinalResponseText(fullOutput, contractReady(options));
|
||||
if (ensuredFinalResponse) {
|
||||
if (options.state.steps.length < 32) {
|
||||
options.state.steps.push({ sequence: options.state.steps.length + 1, kind: "validation", name: "ensure-final-response", status: "completed" });
|
||||
}
|
||||
if (!firstOutput) {
|
||||
firstOutput = true;
|
||||
await options.onFirstOutput?.();
|
||||
}
|
||||
send(controller, { type: "answer.delta", text: ensuredFinalResponse });
|
||||
fullOutput = ensuredFinalResponse;
|
||||
emitted = true;
|
||||
}
|
||||
if (!/\S/.test(fullOutput)) {
|
||||
if (/\S/.test(first.held) || /\S/.test(first.attemptOutput)) throw new Error("runtime_contract_incomplete");
|
||||
throw new Error("empty_answer");
|
||||
|
||||
@@ -3,7 +3,7 @@ import test from "node:test";
|
||||
import { createConsultationTools, createConsultationRuntimeState } from "../src/mastra/consultation-tools.ts";
|
||||
import { getJyotishAgent } from "../src/mastra/index.ts";
|
||||
import { consultationAgentPublicEventSchema, createNdjsonParser } from "../src/lib/consultation-agent-events.ts";
|
||||
import { collectAgentPublicEvents, streamAgentResponse } from "../src/lib/stream-agent-response.ts";
|
||||
import { collectAgentPublicEvents, ensureFinalResponseText, streamAgentResponse } from "../src/lib/stream-agent-response.ts";
|
||||
|
||||
const serverChart = {
|
||||
name: "测试",
|
||||
@@ -166,6 +166,31 @@ test("incomplete runtime contract fails without saving a successful answer", asy
|
||||
assert.equal(events.filter((event) => (event as { type?: string }).type === "run.failed").length, 1);
|
||||
});
|
||||
|
||||
test("ensures a controlled final response after a successful tool-only run", async () => {
|
||||
const state = createConsultationRuntimeState();
|
||||
state.jyotishSkillLoaded = true;
|
||||
state.consultationToolCallCount = 1;
|
||||
state.consultationToolCompleted = true;
|
||||
state.workflowReceipt = { route: "career", status: "ready", preciseTiming: "blocked", missingLayers: [] };
|
||||
let completedOutput = "";
|
||||
async function* chunks() {
|
||||
yield { type: "tool-result", payload: { toolCallId: "tool-1", toolName: "run-jyotish-consultation", result: {} } };
|
||||
}
|
||||
const response = streamAgentResponse({
|
||||
runId: "run", requestId: "req", state, stream: chunks(), requireTool: true,
|
||||
toolStatus: () => "ready", receipt: () => receipt(state),
|
||||
onComplete: (output) => { completedOutput = output; },
|
||||
});
|
||||
const events: unknown[] = [];
|
||||
const parser = createNdjsonParser((event) => events.push(event));
|
||||
parser.finish(await response.text());
|
||||
assert.equal(ensureFinalResponseText("", true), completedOutput);
|
||||
assert.match(completedOutput, /计算已完成/);
|
||||
assert.equal(events.filter((event) => (event as { type?: string }).type === "answer.delta").length, 1);
|
||||
assert.equal(events.filter((event) => (event as { type?: string }).type === "run.completed").length, 1);
|
||||
assert.equal(state.steps.at(-1)?.name, "ensure-final-response");
|
||||
});
|
||||
|
||||
|
||||
test("persistence failure emits run.failed instead of run.completed", async () => {
|
||||
const state = createConsultationRuntimeState();
|
||||
|
||||
Reference in New Issue
Block a user