fix(consult): 降级正文过 Pass 4,去重并停止空跑 compose(BUG-959/960/961)
降级交付复用 releasePass4Sentences;每次 attempt 清空 uncontractedText;成功降级后跳过 compose。
This commit is contained in:
@@ -466,6 +466,9 @@ export function streamAgentResponse(options: StreamAgentResponseOptions) {
|
||||
stream: ChunkStream,
|
||||
attempt: { drainSpoken?: boolean; suppressCompositionActivity?: boolean } = {},
|
||||
) {
|
||||
// Each attempt owns its own uncontracted buffer. Accumulating across the
|
||||
// contract retry delivered the first draft and the retry as one answer.
|
||||
uncontractedText = "";
|
||||
const visible = createVisibleTextTransformer(options.transformText ?? ((value) => value));
|
||||
let held = "";
|
||||
let composingSent = Boolean(attempt.suppressCompositionActivity);
|
||||
@@ -687,6 +690,46 @@ export function streamAgentResponse(options: StreamAgentResponseOptions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function deliverDegradedAnswer(
|
||||
controller: ReadableStreamDefaultController<Uint8Array> | undefined,
|
||||
) {
|
||||
const origin = fullOutput;
|
||||
if (options.pass4Mode) {
|
||||
await releasePass4Sentences(controller, uncontractedText, false);
|
||||
await releasePass4Sentences(controller, "", true);
|
||||
} else if (/\S/.test(uncontractedText)) {
|
||||
if (!firstOutput) {
|
||||
firstOutput = true;
|
||||
await options.onFirstOutput?.();
|
||||
}
|
||||
send(controller, { type: "answer.delta", text: uncontractedText });
|
||||
fullOutput += uncontractedText;
|
||||
emitted = true;
|
||||
}
|
||||
if (!/\S/.test(fullOutput.slice(origin.length))) {
|
||||
if (options.pass4Mode === "general_no_birth_time") {
|
||||
if (!firstOutput) {
|
||||
firstOutput = true;
|
||||
await options.onFirstOutput?.();
|
||||
}
|
||||
send(controller, { type: "answer.delta", text: GENERAL_NO_BIRTH_TIME_REFUSAL });
|
||||
fullOutput += GENERAL_NO_BIRTH_TIME_REFUSAL;
|
||||
emitted = true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
appendConsultationRuntimeStep(options.state, {
|
||||
kind: "validation",
|
||||
name: CONTRACT_DEGRADED_STEP,
|
||||
status: "failed",
|
||||
});
|
||||
send(controller, { type: "answer.delta", text: CONTRACT_DEGRADED_NOTE });
|
||||
fullOutput += CONTRACT_DEGRADED_NOTE;
|
||||
emitted = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
const body = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
const sideEvent = options.sideEvent
|
||||
@@ -700,6 +743,7 @@ export function streamAgentResponse(options: StreamAgentResponseOptions) {
|
||||
for (const event of skillBoundEvents) send(controller, event);
|
||||
flushThinkingPlan(controller);
|
||||
try {
|
||||
let deliveredDegraded = false;
|
||||
await consumeAttempt(controller, options.stream, {
|
||||
drainSpoken: Boolean(options.composeAnswer),
|
||||
});
|
||||
@@ -715,21 +759,9 @@ export function streamAgentResponse(options: StreamAgentResponseOptions) {
|
||||
&& options.state.consultationToolSuccessCount === 0
|
||||
&& /\S/.test(uncontractedText);
|
||||
if (canDegrade) {
|
||||
appendConsultationRuntimeStep(options.state, {
|
||||
kind: "validation",
|
||||
name: CONTRACT_DEGRADED_STEP,
|
||||
status: "failed",
|
||||
});
|
||||
if (!firstOutput) {
|
||||
firstOutput = true;
|
||||
await options.onFirstOutput?.();
|
||||
}
|
||||
send(controller, { type: "answer.delta", text: uncontractedText });
|
||||
fullOutput += uncontractedText;
|
||||
send(controller, { type: "answer.delta", text: CONTRACT_DEGRADED_NOTE });
|
||||
fullOutput += CONTRACT_DEGRADED_NOTE;
|
||||
emitted = true;
|
||||
} else {
|
||||
deliveredDegraded = await deliverDegradedAnswer(controller);
|
||||
}
|
||||
if (!deliveredDegraded) {
|
||||
appendConsultationRuntimeStep(options.state, {
|
||||
kind: "validation",
|
||||
name: RUNTIME_CONTRACT_INCOMPLETE_STEP,
|
||||
@@ -738,18 +770,20 @@ export function streamAgentResponse(options: StreamAgentResponseOptions) {
|
||||
throw new Error("runtime_contract_incomplete");
|
||||
}
|
||||
}
|
||||
const findings = await publishFindings(controller);
|
||||
const composed = await composeOnce(controller, findings);
|
||||
if (!composed) {
|
||||
await continueCurrentAnswer(controller);
|
||||
if (options.pass4Mode) await finishPass4(controller, "", findings);
|
||||
}
|
||||
if (!/\S/.test(fullOutput) && options.retryForAnswer) {
|
||||
appendConsultationRuntimeStep(options.state, { kind: "validation", name: "answer-retry", status: "completed" });
|
||||
send(controller, { type: "activity", phase: "answer-composition", label: "正在组织回答" });
|
||||
const retryOrigin = fullOutput;
|
||||
await consumeAttempt(controller, await options.retryForAnswer());
|
||||
if (options.pass4Mode) await finishPass4(controller, retryOrigin, findings, false);
|
||||
if (!deliveredDegraded) {
|
||||
const findings = await publishFindings(controller);
|
||||
const composed = await composeOnce(controller, findings);
|
||||
if (!composed) {
|
||||
await continueCurrentAnswer(controller);
|
||||
if (options.pass4Mode) await finishPass4(controller, "", findings);
|
||||
}
|
||||
if (!/\S/.test(fullOutput) && options.retryForAnswer) {
|
||||
appendConsultationRuntimeStep(options.state, { kind: "validation", name: "answer-retry", status: "completed" });
|
||||
send(controller, { type: "activity", phase: "answer-composition", label: "正在组织回答" });
|
||||
const retryOrigin = fullOutput;
|
||||
await consumeAttempt(controller, await options.retryForAnswer());
|
||||
if (options.pass4Mode) await finishPass4(controller, retryOrigin, findings, false);
|
||||
}
|
||||
}
|
||||
if (!/\S/.test(fullOutput)) throw new Error("empty_answer");
|
||||
settling = true;
|
||||
|
||||
Reference in New Issue
Block a user