Skip to content

fix(core): streamsOutput 工具不受屏障,越过屏障照常按顺序执行 - #374

Merged
Phantivia merged 1 commit into
mainfrom
fix/streaming-tools-barrier
Nov 4, 2026
Merged

Phantivia merged 1 commit into
mainfrom
fix/streaming-tools-barrier

Conversation

@Phantivia

@Phantivia Phantivia commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

Fixes #367

中文

ToolDef 加一个可选标记 streamsOutput,表示输出接收器(outputTap)在模型还在输出时就对外执行这个工具。主循环的屏障不跳过它:同一条回复里排在 barrierAfter 工具后面、标了 streamsOutput 的调用,在屏障工具执行完之后照常按顺序执行,回执是它自己的真实结果。没标的调用照旧跳过,回执仍是 [not executed: review the preceding tool result first];只要同一条回复里有流式工具越过屏障执行过,这些回执末尾再加一行 [streaming tools in this response ran as usual: <工具名>]。EagerDispatch 仍在屏障处停下,interrupt 照旧跳过本轮尚未开始的调用;fork 没有输出接收器,屏障不变。

为什么:接收器在函数调用开头就开演说话类工具,屏障却在响应结束后才把它记成未执行,模型看到回执重说一遍,观众听两遍。标记只按工具声明认:不按 speak 标签认,因为确认发送的工具也是 speak,屏障本来就是为拦它设的;也不按接收器 externalizes 的运行时报告认。

感谢 @Lantichr 报告并给出复现用例和补丁。新测试沿用他的用例(屏障工具 stage 加普通工具 after,带 outputTap),加一个流式工具后改了断言。没有照补丁的做法(屏障后的调用不交给接收器):说话不是要先看前面结果才能做的动作,规则应当是流式工具不归屏障管,不该把这句台词拦下不播。

怎么验的:新用例在去掉 src/core/loop.ts 的改动后失败,改动后通过;原有的屏障用例里被跳过的工具加上 speak 标签,确认没标的照旧跳过。没验的:没在真实直播上跑。扩展要在 vtuber_act 上标 streamsOutput 才生效,在 Pal-AI-Lab/cortico-world-vtuber#14,要等这个 PR 合并发版后再合。

English

ToolDef gains an optional streamsOutput flag: the output tap executes this tool externally while the model is still producing output. The main loop's barrier does not skip such calls. A streamsOutput call placed after a barrierAfter tool in the same response runs in order once the barrier tool has finished, and its receipt is its own result. Unflagged calls are still skipped with [not executed: review the preceding tool result first]; when any streaming tool ran past the barrier, those receipts get one more line, [streaming tools in this response ran as usual: <names>]. EagerDispatch still stops at the barrier, an interrupt still skips calls that have not started, and forks keep their barrier because they have no output tap.

Why: the tap starts performing a speech tool when the call opens, but the barrier marked that call as not executed after the response ended, so the model repeated itself and the audience heard the line twice. The flag is the only signal. The speak tag is not used, because the send-confirm tool is also speak and is what the barrier exists for; the tap's runtime externalizes report is not used either.

Thanks to @Lantichr for the report, the reproduction and the patch. The new test is built on his fixture (barrier tool stage, plain tool after, an outputTap), with a streaming tool added and the assertions changed. The patch's approach, withholding calls after the barrier from the tap, was not taken: speaking does not depend on the preceding result, so the rule is that streaming tools are outside the barrier, and the line should not be held back.

Verified: the new test fails with the src/core/loop.ts change removed and passes with it; the existing barrier test now tags its skipped tool speak to show that unflagged calls are still skipped. Not verified: no live stream run. The extension needs streamsOutput on vtuber_act; that is Pal-AI-Lab/cortico-world-vtuber#14, to be merged after this one is merged and released.

Checks

  • pnpm test (289 files, 5227 passed, 3 skipped)
  • pnpm run typecheck
  • pnpm typecheck:web
  • A bug fix carries a test that fails before the fix / 修 bug 附带修复前会失败的测试

🤖 Generated with Claude Code

输出接收器在函数调用开头就对外执行说话这类工具,屏障却在响应结束后才把同一条回复里排在
屏障工具后面的调用记成未执行;模型看到未执行回执重说一遍,观众听到两遍。

- ToolDef 加 streamsOutput:标了的调用在屏障工具执行完之后照常按顺序执行,回执是真实结果。
  没标的照旧跳过;被跳过的回执末尾用 streamedPastBarrier 列出越过屏障执行过的流式工具名。
- 只按这个标记认:带 speak 标签的确认发送工具正是屏障要拦的,接收器 externalizes 的运行时
  报告也不作数。EagerDispatch 仍在屏障处停下,interrupt 照旧跳过本轮尚未开始的调用;fork
  没有输出接收器,屏障不变。
- 没采用「屏障后的调用不交给接收器」:说话不是要先看前面结果才能做的动作,规则放在工具上。
- 新用例在去掉 loop.ts 的改动后失败;原屏障用例里被跳过的工具加上 speak 标签。

Co-authored-by: Lantichr <333946430+Lantichr@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread src/core/loop.ts
if (barrierHit) {
const def = this.toolDefs.find((t) => t.name === call.name);
const pastBarrier = barrierHit;
if (pastBarrier && !def?.streamsOutput) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs/sessions.md 把 src/core/loop.ts 列为 owner,本 PR 改了这支文件里何时跳过后续调用,但没有改这一页。

AGENTS.md §8:「Every page under docs/ and every README of a code unit names, in its first line, the file or interface it shadows: its owner. The commit that changes the owner changes the page.」

此处把跳过条件改成 pastBarrier && !def?.streamsOutput。barrierHit 也会在同一轮前一个调用 status !== 'completed' 时置位。docs/sessions.md 写的是非 completed 的调用之后「同一轮排在它后面的调用也不执行」。这一句对 streamsOutput 不再成立。

请把 docs/sessions.md 改到与现行规则一致,或在 PR 说明里写明为什么这一页可以不改。

— Cortico PR Gate via Cursor Cloud Agent

@Phantivia
Phantivia merged commit cd05b82 into main Nov 4, 2026
4 checks passed
@Phantivia
Phantivia deleted the fix/streaming-tools-barrier branch November 4, 2026 19:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

outputTap 在屏障拒绝前就外化,同一次调用产生两次副作用

1 participant