fix(consult): 降级正文过 Pass 4,去重并停止空跑 compose(BUG-959/960/961)
降级交付复用 releasePass4Sentences;每次 attempt 清空 uncontractedText;成功降级后跳过 compose。
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-09-18 — 没跑完计算时,保证性结论仍不会发出
|
||||
|
||||
合同没跑完但模型写了字时,那段回答会先按句子过一遍合规门:保证性结论整句拿掉,其余留下,并注明这次没跑完星盘计算。不会只丢一句说明、上面空空的。同一轮重试过的旧稿不会再拼进这次回答。Skill 版本不变。
|
||||
|
||||
## 2026-09-18 — 申报时段咨询重新能回答
|
||||
|
||||
只知道一段出生范围时,问事业 / 运势会在几十毫秒内失败,提示「未完成必要的方法与计算步骤」,从八月下旬起一直如此。根因是这条路线的助手装了方法校验、却没把方法写进系统提示,模型一次都没被叫到。现在方法已经装上;没有跑完计算但模型写过字时,会把那段留下并注明「没跑完星盘计算」,不再整轮空白。Skill 版本不变。
|
||||
|
||||
@@ -12600,6 +12600,54 @@
|
||||
- 复发自:无
|
||||
- 修复版本:—
|
||||
|
||||
## BUG-959 | 降级正文绕过 Pass 4,保证性结论原样送达
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-18
|
||||
- 最近更新:2026-09-18
|
||||
- 影响面:`stream-agent-response.ts` 合同未绿时的降级交付
|
||||
- 用户现象:没跑完星盘计算时,模型自己写的「我保证你一定会升职」一类句子会进正文。
|
||||
- 触发条件:`requireTool: true`、零次成功计算、有非空正文、`pass4Mode` 为有盘模式。
|
||||
- 根因:BUG-956 的降级分支直接 `send(uncontractedText)`,没有走 `releasePass4Sentences` / `classifyPass4`。降级正文恰恰是没有任何计算依据时写的,保证句和个人盘断言最容易出现在这里。
|
||||
- 修复:降级正文复用 `releasePass4Sentences` 按句过门,`reject` 整句丢弃,`observe` 照记。`CONTRACT_DEGRADED_NOTE` 是服务端文案,不过 Pass 4,贴在末尾。全部句子被丢弃时:`general_no_birth_time` 发拒绝句再附说明;其余模式维持 `runtime_contract_incomplete`,不发只有降级说明的空回答。不在句内改写。
|
||||
- 验证:假流降级 + `verified_chart` + 保证句与安全句 → 保证句不出现在任何 `answer.delta`,回执同时有 `contract-degraded` 与 `pass4-reject:guarantee`;全丢不发 note-only;一般模式全丢发拒绝句 + 说明。
|
||||
- 防复发:降级路径必须复用 `releasePass4Sentences`,不得另写分句或句内替换。Pass 4 是门不是刀。
|
||||
- 关联记录:BUG-956、BUG-948、BUG-950、BUG-951
|
||||
- 复发自:BUG-956(降级路径漏接 Pass 4)
|
||||
- 修复版本:`ae2e46a6`
|
||||
|
||||
## BUG-960 | 两次 attempt 的正文被拼起来一起送出
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-18
|
||||
- 最近更新:2026-09-18
|
||||
- 影响面:`stream-agent-response.ts` `uncontractedText`
|
||||
- 用户现象:合同重试后降级交付把第一稿和重试稿拼成一段,同一轮内容说两遍。
|
||||
- 触发条件:第一次 attempt 写过字,合同仍红,retry 再写一段不同的字。
|
||||
- 根因:`uncontractedText += text` 只在 `contractReady()` 为真时清零。合同一直红就一路累加,attempt 之间没有边界。
|
||||
- 修复:每次 `consumeAttempt` 开始时清空 `uncontractedText`。降级只交付最后一次 attempt 的正文。
|
||||
- 验证:假流 attempt1「第一段。」、attempt2「第二段。」、无工具成功 → 降级正文含第二段、不含第一段。
|
||||
- 防复发:未绿缓冲按 attempt 隔离,不得跨 attempt 累加。
|
||||
- 关联记录:BUG-956、BUG-959
|
||||
- 复发自:BUG-956
|
||||
- 修复版本:`ae2e46a6`
|
||||
|
||||
## BUG-961 | 降级之后还会空跑一轮 compose
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-18
|
||||
- 最近更新:2026-09-18
|
||||
- 影响面:`stream-agent-response.ts` 降级后的收口
|
||||
- 用户现象:无(用户看不到 compose 产出)。本命形状的事件序列里出现 `phase.started{phase:"compose"}`,白花一次模型调用与一段墙钟。
|
||||
- 触发条件:降级交付成功,且该路径传入了 `composeAnswer`(本命线)。
|
||||
- 根因:降级分支执行后流程继续走到 `publishFindings` / `composeOnce`。
|
||||
- 修复:成功降级后设 `deliveredDegraded`,跳过 Pass 2 / Pass 3 / compose / answer-retry,直接 `run.completed`。全丢的非一般模式在 compose 之前抛 `runtime_contract_incomplete`。
|
||||
- 验证:降级路径 `composeAnswer` 调用次数为 0,事件无 `phase.started{phase:"compose"}`。
|
||||
- 防复发:降级是终态交付,不得再进 compose。
|
||||
- 关联记录:BUG-956、BUG-959
|
||||
- 复发自:BUG-956
|
||||
- 修复版本:`ae2e46a6`
|
||||
|
||||
## BUG-958 | 窗口指令「必须调工具」与「不得给应期」未写清,模型可能跳过计算或拒答
|
||||
|
||||
- 状态:resolved
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# PROGRESS · 降级交付路径补 Pass 4 与去重(2026-09-18)
|
||||
|
||||
工作树:`.worktrees/contract-degraded-pass4-20260918`
|
||||
分支:`codex/contract-degraded-pass4-20260918`
|
||||
基线:`origin/staging` @ `1061514f`(任务书写 `877128ce`,其后远端已快进)
|
||||
任务书:`docs/tasks/TASK-contract-degraded-pass4-20260918.md`
|
||||
|
||||
## 结论
|
||||
|
||||
降级正文现在按句过 Pass 4:保证句整句丢掉,其余留下,服务端说明贴在末尾。两次 attempt 不再拼在一起。降级之后不再空跑 compose。合同门 `contractReady()` 没放宽。
|
||||
|
||||
## 任务状态
|
||||
|
||||
| 任务 | 状态 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| BUG-959 降级正文过 Pass 4 | 完成 | 复用 `releasePass4Sentences`;全丢时一般模式发拒绝句,其余模式仍 `runtime_contract_incomplete` |
|
||||
| BUG-960 每次 attempt 清空缓冲 | 完成 | 降级只交付最后一次 attempt |
|
||||
| BUG-961 降级后跳过 compose | 完成 | `composeAnswer` 零次调用,事件无 `phase.started{compose}` |
|
||||
| BUG-957 窗口预跑 | 本文件不覆盖 | 见 `PROGRESS-window-consult-precompute-20260918.md` |
|
||||
|
||||
## 断言三栏(既有测试)
|
||||
|
||||
| 测试 | 原值 | 新值 | 原因 |
|
||||
| --- | --- | --- | --- |
|
||||
| BUG-956 有正文降级 | 未传 `pass4Mode` | `pass4Mode: "verified_chart"` | 任务书:e32ce624 之后的既有降级测试补上该参数;正文「不能保存」不含 reject 句,行为不变 |
|
||||
| BUG-956 无正文 | 未传 `pass4Mode` | 同上 | 同上,空正文仍走 `runtime_contract_incomplete` |
|
||||
|
||||
## 测试
|
||||
|
||||
| 项 | 结果 |
|
||||
| --- | --- |
|
||||
| `tsc --noEmit` | 0 错 |
|
||||
| `npm run lint` | 0 error(120 warning,与基线同量级,未顺手修) |
|
||||
| `consultation-agentic-runtime.test.ts` + `timing-output-guard.test.ts` | 88 pass / 1 fail。失败是本机 Windows `EPERM` 无法为 live skill 建 symlink(`personal Agent exposes the Jyotish Skill…`),与基线缺口同类,不是本单引入 |
|
||||
| 本单新增 | 降级+保证句不出现在任何 `answer.delta`,回执同时有 `contract-degraded` 与 `pass4-reject:guarantee`;全丢不发只有降级说明的空回答;两次 attempt 只留第二段;降级路径 `composeAnswer` 调用次数为 0 |
|
||||
|
||||
Linux 门禁才是验收数字:目标 fail 回到 31,总数不低于基线 3501。本机无登录态、无 Chrome、Windows 无 symlink 权限,不写成通过。
|
||||
@@ -139,7 +139,7 @@
|
||||
| `TASK-consult-three-channels-fix-20260918.md` | `PROGRESS-consult-three-channels-fix-20260918.md` | 验收修复单:领域上限解耦(945)、31s 预算断言(946)、校正思考分片门(947)、Pass 4 按模式分流(948)、容量算术(949) | 已验收通过(Claude:tsc 0 / lint 0 error / npm test 3491 条 31 红且与基线 `742ffbc6` 同一组、原 12 条红全绿 / `/` 仍 Static / 首屏 js gzip 468,388 B 较基线 +0.02%);review 另出 BUG-950~953 见下一行 | `e32ce624` |
|
||||
| `TASK-consult-pass4-streaming-20260918.md` | `PROGRESS-consult-pass4-streaming-20260918.md` | 验收 review 三轮:Pass 4 一 hold 正文就整段蹦出、逐字流式消失(BUG-950 产品拍板按句放行);无出生分钟模式整段被一句拒绝顶掉、一般知识句一起丢(951 改按句丢弃);该模式下日期不留痕(952);校正流 token 级 thinking 是死链,按 P2 删除并把测试翻转成否定合同(953)。基线 `1e553976` | 待验收 | `cd4775ae` |
|
||||
| `TASK-window-consult-contract-20260918.md` | `PROGRESS-window-consult-contract-20260918.md` | **P0 线上**:申报时段模式**自 2026-08-21 起每轮秒败**(69ms、0 token、模型从未被调用)。服务端日志实证根因:窗口 Agent attach 了 `jyotishSkillBinding()`,但 `windowJyotishInstructions` 从来不含方法块 marker,输入处理器直接 abort(BUG-954)。abort 与「模型没调工具」同码,是它藏四周的原因(955);另含合同未绿不得丢正文(956)、计算不该由模型触发(957,须等 954 上线后另轮)、窗口指令应期冲突(958)。基线 `9cdcf96b` | 已验收通过(Claude:tsc 0 / lint 0 error / npm test 3501 条 31 红同基线 / `/` Static / gzip 无变化;实跑确认窗口 Agent 指令已含 marker);review 另出 BUG-959~961 见下一行 | `5b6abc23` |
|
||||
| `TASK-contract-degraded-pass4-20260918.md` | — | 验收 review:BUG-956 新增的降级交付路径绕过 Pass 4,保证句原样送达(BUG-959);`uncontractedText` 跨 attempt 不清零,同一轮正文说两遍(960);降级后还空跑一轮 compose(961)。基线 `877128ce` | 待领取 | — |
|
||||
| `TASK-contract-degraded-pass4-20260918.md` | `PROGRESS-contract-degraded-pass4-20260918.md` | 验收 review:BUG-956 新增的降级交付路径绕过 Pass 4,保证句原样送达(BUG-959);`uncontractedText` 跨 attempt 不清零,同一轮正文说两遍(960);降级后还空跑一轮 compose(961)。基线 `877128ce` | 待验收 | `ae2e46a6` |
|
||||
| `TASK-chat-markdown-list-20260918.md` | — | 真机排版反馈:聊天正文列表**没有项目符号**(Tailwind v4 preflight 清了 `list-style`,`.markdown-list` 没恢复,BUG-962);`promoteDefinitionLists` 把四标题口语体的散文误判成并列项,三段正文被改写成列表(BUG-963,判据太松,收紧而非删除——BUG-356 的场景要留);思考条与正文之间实测 56px=报告 grid gap 24 + 首标题 margin-top 32,后者是被特指度压掉的「首元素清零」规则(BUG-964)。基线 `ea0280c1` | 待领取 | — |
|
||||
| `TASK-secondary-page-entry-20260918.md` | — | 真机反馈:星盘 / 星历 / 报告进入时抖一下——三页都是「矮的等待文案块 → 高的正文块」一次推挤,且 `use-chart-page` 无缓存所以每次进入都抖(BUG-966)。产品已拍板方案一:统一外壳 + 缓存 + 预取消灭中间态,**不加 spinner**,红线不动。另含 BUG-967:标签页跨过部署后客户端导航静默失效(BUG-965 已复现确认,刷新即恢复),要按 `NEXT_PUBLIC_GIT_COMMIT` 与 `/api/health` 比对自愈。基线 `41902067` | 待领取 | — |
|
||||
| `TASK-account-dialog-inert-20260918.md` | — | 真机:账户弹窗打开后整个弹窗点不动、退出登录做不了,刷新依旧——`e4e73f56` 把 `SidebarInset` 搬进 layout 后 `inert={modalOpen}` 罩住了没有 portal 的 `AccountDialogOverlay`(BUG-968,P0,代码级确认)。另含 BUG-969:校正「换一件事问」后无下文,服务端已出下一题且快照重算完整,客户端停在开场状态,GET 路由与客户端两端静默,本单只做可观测 + 不静默 + 题干进正文。基线 `1061514f` | 待领取 | — |
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1279,6 +1279,7 @@ test("incomplete runtime contract with body is delivered degraded instead of dis
|
||||
}
|
||||
const response = streamAgentResponse({
|
||||
runId: "run", requestId: "req", state, stream: chunks(), requireTool: true,
|
||||
pass4Mode: "verified_chart",
|
||||
toolStatus: () => "blocked", receipt: () => ({ ...receipt(state), steps: publicConsultationRuntimeSteps(state) }),
|
||||
onComplete: () => { completed += 1; },
|
||||
onError: () => { failed += 1; },
|
||||
@@ -1310,6 +1311,7 @@ test("incomplete runtime contract without body still fails closed (BUG-956)", as
|
||||
async function* chunks() {}
|
||||
const response = streamAgentResponse({
|
||||
runId: "run", requestId: "req", state, stream: chunks(), requireTool: true,
|
||||
pass4Mode: "verified_chart",
|
||||
toolStatus: () => "blocked", receipt: () => ({ ...receipt(state), steps: publicConsultationRuntimeSteps(state) }),
|
||||
onComplete: () => { completed += 1; },
|
||||
onError: () => { failed += 1; },
|
||||
@@ -1329,6 +1331,157 @@ test("incomplete runtime contract without body still fails closed (BUG-956)", as
|
||||
step.kind === "validation" && step.name === "runtime-contract-incomplete" && step.status === "failed"));
|
||||
});
|
||||
|
||||
test("degraded delivery drops guarantee sentences through Pass 4 (BUG-959)", async () => {
|
||||
const state = createConsultationRuntimeState();
|
||||
let completed = 0;
|
||||
async function* chunks() {
|
||||
yield { type: "text-delta", payload: { text: "我保证你一定会升职。" } };
|
||||
yield { type: "text-delta", payload: { text: "方向上可以推进。" } };
|
||||
}
|
||||
const response = streamAgentResponse({
|
||||
runId: "run", requestId: "req", state, stream: chunks(), requireTool: true,
|
||||
pass4Mode: "verified_chart",
|
||||
toolStatus: () => "blocked", receipt: () => ({ ...receipt(state), steps: publicConsultationRuntimeSteps(state) }),
|
||||
onComplete: () => { completed += 1; },
|
||||
});
|
||||
const events: unknown[] = [];
|
||||
const parser = createNdjsonParser((event) => events.push(event));
|
||||
parser.finish(await response.text());
|
||||
assert.equal(completed, 1);
|
||||
const answers = events
|
||||
.filter((event): event is { type: string; text: string } => (event as { type?: string }).type === "answer.delta")
|
||||
.map((event) => event.text);
|
||||
assert.equal(answers.some((text) => /一定会升职|我保证/.test(text)), false);
|
||||
assert.match(answers.join(""), /方向上可以推进/);
|
||||
assert.equal(answers.join("").includes(CONTRACT_DEGRADED_NOTE), true);
|
||||
const completedEvent = events.find((event) => (event as { type?: string }).type === "run.completed") as {
|
||||
receipt?: { steps: Array<{ kind: string; name: string; status: string }> };
|
||||
};
|
||||
assert.ok(completedEvent.receipt?.steps.some((step) =>
|
||||
step.kind === "validation" && step.name === "contract-degraded" && step.status === "failed"));
|
||||
assert.ok(completedEvent.receipt?.steps.some((step) =>
|
||||
step.kind === "validation" && step.name === "pass4-reject:guarantee" && step.status === "failed"));
|
||||
});
|
||||
|
||||
test("degraded delivery that Pass 4 empties stays incomplete instead of a note-only answer (BUG-959)", async () => {
|
||||
const state = createConsultationRuntimeState();
|
||||
let completed = 0;
|
||||
let failed = 0;
|
||||
async function* chunks() {
|
||||
yield { type: "text-delta", payload: { text: "我保证你一定会升职。" } };
|
||||
}
|
||||
const response = streamAgentResponse({
|
||||
runId: "run", requestId: "req", state, stream: chunks(), requireTool: true,
|
||||
pass4Mode: "verified_chart",
|
||||
toolStatus: () => "blocked", receipt: () => ({ ...receipt(state), steps: publicConsultationRuntimeSteps(state) }),
|
||||
onComplete: () => { completed += 1; },
|
||||
onError: () => { failed += 1; },
|
||||
});
|
||||
const events: unknown[] = [];
|
||||
const parser = createNdjsonParser((event) => events.push(event));
|
||||
parser.finish(await response.text());
|
||||
assert.equal(completed, 0);
|
||||
assert.equal(failed, 1);
|
||||
const answer = events
|
||||
.filter((event): event is { type: string; text: string } => (event as { type?: string }).type === "answer.delta")
|
||||
.map((event) => event.text)
|
||||
.join("");
|
||||
assert.equal(answer.includes(CONTRACT_DEGRADED_NOTE), false);
|
||||
const failure = events.find((event) => (event as { type?: string }).type === "run.failed") as {
|
||||
code: string;
|
||||
receipt?: { steps: Array<{ name: string }> };
|
||||
};
|
||||
assert.equal(failure.code, "runtime_contract_incomplete");
|
||||
assert.ok(failure.receipt?.steps.some((step) => step.name === "pass4-reject:guarantee"));
|
||||
assert.ok(failure.receipt?.steps.some((step) => step.name === "runtime-contract-incomplete"));
|
||||
});
|
||||
|
||||
test("degraded delivery uses the refusal when Pass 4 drops every general-mode sentence (BUG-959)", async () => {
|
||||
const state = createConsultationRuntimeState();
|
||||
async function* chunks() {
|
||||
yield { type: "text-delta", payload: { text: "你的上升是巨蟹座。" } };
|
||||
}
|
||||
const response = streamAgentResponse({
|
||||
runId: "run", requestId: "req", state, stream: chunks(), requireTool: true,
|
||||
pass4Mode: "general_no_birth_time",
|
||||
toolStatus: () => "blocked", receipt: () => ({ ...receipt(state), steps: publicConsultationRuntimeSteps(state) }),
|
||||
});
|
||||
const events: unknown[] = [];
|
||||
const parser = createNdjsonParser((event) => events.push(event));
|
||||
parser.finish(await response.text());
|
||||
const answer = events
|
||||
.filter((event): event is { type: string; text: string } => (event as { type?: string }).type === "answer.delta")
|
||||
.map((event) => event.text)
|
||||
.join("");
|
||||
assert.match(answer, new RegExp(GENERAL_NO_BIRTH_TIME_REFUSAL));
|
||||
assert.equal(answer.includes(CONTRACT_DEGRADED_NOTE), true);
|
||||
assert.doesNotMatch(answer, /你的上升是巨蟹座/);
|
||||
assert.equal(events.filter((event) => (event as { type?: string }).type === "run.completed").length, 1);
|
||||
});
|
||||
|
||||
test("degraded delivery keeps only the last attempt body (BUG-960)", async () => {
|
||||
const state = createConsultationRuntimeState();
|
||||
async function* first() {
|
||||
yield { type: "text-delta", payload: { text: "第一段。" } };
|
||||
}
|
||||
async function* second() {
|
||||
yield { type: "text-delta", payload: { text: "第二段。" } };
|
||||
}
|
||||
const response = streamAgentResponse({
|
||||
runId: "run", requestId: "req", state, stream: first(), requireTool: true,
|
||||
pass4Mode: "verified_chart",
|
||||
retry: async () => second(),
|
||||
toolStatus: () => "blocked", receipt: () => ({ ...receipt(state), steps: publicConsultationRuntimeSteps(state) }),
|
||||
});
|
||||
const events: unknown[] = [];
|
||||
const parser = createNdjsonParser((event) => events.push(event));
|
||||
parser.finish(await response.text());
|
||||
const answer = events
|
||||
.filter((event): event is { type: string; text: string } => (event as { type?: string }).type === "answer.delta")
|
||||
.map((event) => event.text)
|
||||
.join("");
|
||||
assert.match(answer, /第二段/);
|
||||
assert.doesNotMatch(answer, /第一段/);
|
||||
assert.equal(answer.includes(CONTRACT_DEGRADED_NOTE), true);
|
||||
assert.equal(events.filter((event) => (event as { type?: string }).type === "run.completed").length, 1);
|
||||
});
|
||||
|
||||
test("degraded delivery does not start a compose pass (BUG-961)", async () => {
|
||||
const state = createConsultationRuntimeState();
|
||||
let composeCalls = 0;
|
||||
async function* chunks() {
|
||||
yield { type: "text-delta", payload: { text: "方向上可以推进。" } };
|
||||
}
|
||||
const response = streamAgentResponse({
|
||||
runId: "run", requestId: "req", state, stream: chunks(), requireTool: true,
|
||||
pass4Mode: "verified_chart",
|
||||
toolStatus: () => "blocked", receipt: () => ({ ...receipt(state), steps: publicConsultationRuntimeSteps(state) }),
|
||||
composeAnswer: async () => {
|
||||
composeCalls += 1;
|
||||
async function* composed() {
|
||||
yield { type: "text-delta", payload: { text: "不该出现的 compose。" } };
|
||||
yield { type: "finish", payload: { stepResult: { reason: "stop" }, output: { usage: {}, steps: [{}] } } };
|
||||
}
|
||||
return composed();
|
||||
},
|
||||
});
|
||||
const events: unknown[] = [];
|
||||
const parser = createNdjsonParser((event) => events.push(event));
|
||||
parser.finish(await response.text());
|
||||
assert.equal(composeCalls, 0);
|
||||
assert.equal(events.some((event) => {
|
||||
const item = event as { type?: string; phase?: string };
|
||||
return item.type === "phase.started" && item.phase === "compose";
|
||||
}), false);
|
||||
const answer = events
|
||||
.filter((event): event is { type: string; text: string } => (event as { type?: string }).type === "answer.delta")
|
||||
.map((event) => event.text)
|
||||
.join("");
|
||||
assert.match(answer, /方向上可以推进/);
|
||||
assert.doesNotMatch(answer, /不该出现的 compose/);
|
||||
assert.equal(events.filter((event) => (event as { type?: string }).type === "run.completed").length, 1);
|
||||
});
|
||||
|
||||
test("skill-binding abort is a distinct receipt step from a missing tool call (BUG-955)", async () => {
|
||||
const bindingState = createConsultationRuntimeState();
|
||||
let bindingError = "";
|
||||
|
||||
Reference in New Issue
Block a user