Skip to content

fix(core): cite web search sources as bare URLs - #11677

Merged
qqqys merged 2 commits into
QwenLM:mainfrom
qqqys:fix/web-search-cite-bare-urls
Sep 12, 2026
Merged

fix(core): cite web search sources as bare URLs#11677
qqqys merged 2 commits into
QwenLM:mainfrom
qqqys:fix/web-search-cite-bare-urls

Conversation

@qqqys

@qqqys qqqys commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

The web_search tool now asks the model to cite sources as bare URLs, one per line, instead of markdown links. Both places that tell the model how to cite change together: the citation policy appended to every search result, and the citation requirement and example in the tool description. Both now say not to add titles or link text, and why: the result's page lists give URLs only, so a title in the answer cannot be verified — including one repeated from the search side model's narrated findings. Cite-order rules (opened pages first, candidates only when they directly support a claim, say so when attribution cannot be established) are unchanged.

Why it's needed

Asking for markdown links without supplying titles makes the model write link text itself. In practice the link text is a URL path, or a title the model reconstructs from the URL slug — which reads to the user as the page's real title. #11564 first tried to fix this by extracting real titles from the search side model's answer; that implementation (#11616) was withdrawn after two review rounds showed that turning page- or model-written text into trusted link text is an open adversarial surface (see the decision in #11564). Citing bare URLs removes the invented titles without adding a title channel to secure. The CLI (on terminals that support OSC 8 hyperlinks) and the web UI already render a bare URL as a link whose visible text is the URL itself, so the destination the user sees is the destination they get.

Reviewer Test Plan

How to verify

  1. cd packages/core && npx vitest run src/tools/web-search.test.ts — the new WebSearchTool citations tests pin the policy wording in the tool result and the bare-URL example in the tool description.
  2. With a ModelStudio Standard or Token Plan key, run qwen -p "Search the web for the current Node.js Active LTS version, then answer with sources." --approval-mode yolo --output-format stream-json.
  3. Expected: the final answer ends with a Sources: section of bare URLs, one per line, with no [title](url) links, and every URL appears in the web_search tool result.

Evidence (Before & After)

Unit tests: npx vitest run src/tools/web-search.test.ts → 105 passed. Reverting each of the three wording changes (policy, description bullet, description example) makes the new WebSearchTool citations tests fail (3/3 caught). eslint, prettier --check, and tsc --noEmit for packages/core are clean.

Live runs, same prompt and isolated HOME (Token Plan qwen3.6-plus, web search on by default). The Sources: sections below are copied from each run's final answer; provenance is checked against the web_search result recorded in the session transcript.

Before (main wording, 2 runs) — the result's policy says "as markdown links"; every citation is a markdown link, with link text the model wrote:

Sources:
- [nodejs.org/en/about/previous-releases](https://nodejs.org/en/about/previous-releases) (opened evidence)
- [Node.js End of Life Dates - HeroDevs](https://www.herodevs.com/blog-posts/node-js-end-of-life-dates-you-should-be-aware-of)
- [EndofLife.date — Node.js](https://endoflife.date/nodejs)
Sources:
- [nodejs.org/en/about/previous-releases](https://nodejs.org/en/about/previous-releases)
- [endoflife.date/nodejs](https://endoflife.date/nodejs)

The page lists in these runs' web_search results carry URLs only (8 and 16). In the first run, the search side model's narration inside the result did give its own link text for two of these pages — "Node.js Version Support: EOL Dates and Latest Releases" for HeroDevs and "Node.js" for endoflife.date — and the answer used neither: "Node.js End of Life Dates - HeroDevs" is rebuilt from the URL slug, and "EndofLife.date — Node.js" does not appear in the result.

After (this branch at b26bf0689b, 3 runs) — the result's policy says "as bare URLs"; 9 of 9 citations are bare URLs, 0 markdown links:

Sources:
- https://endoflife.date/nodejs
- https://nodejs.org/en/about/previous-releases
- https://nodejs.org/en/blog/announcements/evolving-the-nodejs-release-schedule
Sources:
- https://nodejs.org/en/about/previous-releases
- https://endoflife.date/nodejs
Sources:
- https://nodejs.org/en/about/previous-releases
- https://nodejs.org/en/blog/announcements/evolving-the-nodejs-release-schedule
- https://endoflife.date/nodejs
- https://www.pkgpulse.com/guides/nodejs-22-vs-nodejs-24-2026

In every run, each cited URL appears in that run's web_search result (13, 12, and 9 URLs for the After runs). Three earlier runs on the first revision of the wording (95ca4c60d3) gave the same result: 11 of 11 bare URLs, all from the result.

Tested on

OS Status
🍏 macOS ⚠️
🪟 Windows ⚠️
🐧 Linux

Environment (optional)

Local build (packages/core and packages/cli dist) run with an isolated HOME whose settings declare one ModelStudio Token Plan modelProviders entry (qwen3.6-plus, key via BAILIAN_TOKEN_PLAN_API_KEY) and no tools.webSearch, so the default opt-out path is exercised.

Risk & Scope

  • Main risk or tradeoff: the model's final answer is still free text; the policy is guidance, not enforcement, so a model can still write markdown links. Bare URLs are also less readable than titled links in long source lists — the tradeoff accepted in web_search: design page titles for cited sources (split from #11490) #11564 in exchange for never showing invented titles.
  • Not validated / out of scope: other model families behind the search tool and other providers' primary models were not run; producing real page titles is out of scope (withdrawn in web_search: design page titles for cited sources (split from #11490) #11564, implementation kept on feat/web-search-titles-readonly).
  • Breaking changes / migration notes: none. Only prompt text changes; the tool's parameters, result sections, and settings are unchanged.

Linked Issues

Closes #11564. Supersedes #11616 (closed). Follows #11490.

中文说明

这个 PR 做了什么

web_search 工具现在要求模型以裸 URL(每行一个)引用来源,而不是 markdown 链接。告诉模型如何引用的两处同时修改:附加在每次搜索结果后的引用策略,以及工具描述中的引用要求和示例。两处都说明不要添加标题或链接文字,并说明原因:结果中的页面列表只提供 URL,因此答案中的标题无法核实——包括从搜索侧模型的叙述中照搬的标题。引用顺序规则(优先引用已打开的页面,候选页面仅在直接支持论断时引用,无法确定出处时如实说明)保持不变。

为什么需要

要求 markdown 链接却不提供标题,会让模型自己写链接文字。实际效果是链接文字为 URL 路径,或者是模型根据 URL 路径片段拼出来的标题——在用户看来就像页面的真实标题。#11564 最初尝试从搜索侧模型的答案中提取真实标题来解决;该实现(#11616)在两轮评审后撤回,因为评审表明把网页或模型写下的文字变成可信的链接文字本身就是一个开放的对抗面(决定见 #11564)。改用裸 URL 引用消除了编造的标题,而且不会新增需要防护的标题通道。CLI(在支持 OSC 8 超链接的终端上)和 Web 界面本来就会把裸 URL 渲染为可见文字就是 URL 本身的链接,用户看到的地址就是实际打开的地址。

评审测试计划

如何验证

  1. cd packages/core && npx vitest run src/tools/web-search.test.ts —— 新增的 WebSearchTool citations 测试固定了工具结果中的策略文案和工具描述中的裸 URL 示例。
  2. 使用 ModelStudio Standard 或 Token Plan 的 key,运行 qwen -p "Search the web for the current Node.js Active LTS version, then answer with sources." --approval-mode yolo --output-format stream-json
  3. 预期:最终答案以 Sources: 小节结尾,其中是每行一个的裸 URL,没有 [标题](url) 链接,且每个 URL 都出现在 web_search 工具结果中。

证据(改动前后)

单元测试:npx vitest run src/tools/web-search.test.ts → 105 个通过。分别还原三处文案改动(策略、描述要点、描述示例),新增的 WebSearchTool citations 测试都会失败(3/3 被捕获)。packages/coreeslintprettier --checktsc --noEmit 均无问题。

实机运行使用相同提示词和隔离的 HOME(Token Plan qwen3.6-plus,web search 默认开启)。下面的 Sources: 小节复制自每次运行的最终答案;出处依据会话记录中保存的 web_search 结果核对。

改动前main 的文案,2 次运行)——结果中的策略写的是 "as markdown links";每条引用都是 markdown 链接,链接文字由模型自己编写:

Sources:
- [nodejs.org/en/about/previous-releases](https://nodejs.org/en/about/previous-releases) (opened evidence)
- [Node.js End of Life Dates - HeroDevs](https://www.herodevs.com/blog-posts/node-js-end-of-life-dates-you-should-be-aware-of)
- [EndofLife.date — Node.js](https://endoflife.date/nodejs)
Sources:
- [nodejs.org/en/about/previous-releases](https://nodejs.org/en/about/previous-releases)
- [endoflife.date/nodejs](https://endoflife.date/nodejs)

这两次运行的 web_search 结果中,页面列表只含 URL(分别为 8 个和 16 个)。第一次运行时,结果里搜索侧模型的叙述确实为其中两个页面给出了自己的链接文字——HeroDevs 为 "Node.js Version Support: EOL Dates and Latest Releases",endoflife.date 为 "Node.js"——但答案两者都没用:"Node.js End of Life Dates - HeroDevs" 是根据 URL 路径片段拼出来的,"EndofLife.date — Node.js" 在结果中并不存在。

改动后(本分支 b26bf0689b,3 次运行)——结果中的策略写的是 "as bare URLs";9 条引用全部是裸 URL,markdown 链接为 0:

Sources:
- https://endoflife.date/nodejs
- https://nodejs.org/en/about/previous-releases
- https://nodejs.org/en/blog/announcements/evolving-the-nodejs-release-schedule
Sources:
- https://nodejs.org/en/about/previous-releases
- https://endoflife.date/nodejs
Sources:
- https://nodejs.org/en/about/previous-releases
- https://nodejs.org/en/blog/announcements/evolving-the-nodejs-release-schedule
- https://endoflife.date/nodejs
- https://www.pkgpulse.com/guides/nodejs-22-vs-nodejs-24-2026

每次运行中,每条被引用的 URL 都出现在该次运行的 web_search 结果中(改动后三次运行的结果分别列出 13、12、9 个 URL)。在文案第一版(95ca4c60d3)上另跑的三次结果相同:11 条引用全部是裸 URL,且都来自结果。

测试平台

Linux 已测试;macOS 与 Windows 未测试。

环境

本地构建(packages/corepackages/cli 的 dist),使用隔离的 HOME,其设置只声明一个 ModelStudio Token Plan modelProviders 条目(qwen3.6-plus,key 通过 BAILIAN_TOKEN_PLAN_API_KEY 提供),不设置 tools.webSearch,从而走默认开启(opt-out)路径。

风险与范围

  • 主要风险或取舍:模型的最终答案仍是自由文本;策略是引导而非强制,模型仍可能写出 markdown 链接。在较长的来源列表中,裸 URL 的可读性也不如带标题的链接——这是 web_search: design page titles for cited sources (split from #11490) #11564 中为了不再展示编造标题而接受的取舍。
  • 未验证 / 不在范围内:未测试搜索工具背后的其他模型系列,也未测试其他服务商的主模型;生成真实页面标题不在本 PR 范围内(已在 web_search: design page titles for cited sources (split from #11490) #11564 中撤回,实现保留在 feat/web-search-titles-readonly 分支)。
  • 破坏性变更 / 迁移说明:无。仅修改提示文案;工具参数、结果小节和设置均不变。

关联 Issue

Closes #11564。取代 #11616(已关闭)。承接 #11490

The citation policy and tool description asked for markdown links, but the
search result lists pages by URL only, so the model wrote link text itself:
URL paths or plausible titles for pages it never saw. Ask for bare URLs, one
per line, without titles or link text, and say why. Cite-order rules are
unchanged.

The title-extraction alternative (QwenLM#11616) is withdrawn; see QwenLM#11564.
The search side model's narrated findings can carry its own link text, so
"page titles are not part of this result" is not always true. State the
accurate reason: the page lists give URLs only, so a title in the answer,
even one repeated from the narration, cannot be verified.
@github-actions github-actions Bot added the review/self-reported The linked issue was opened by the PR author (self-reported) label Sep 11, 2026
@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR — and for writing up the withdrawal of #11616 rather than just quietly closing it. That comment is what makes this one reviewable.

Template looks good ✓

Problem: observed, not theoretical, and I confirmed it statically against main rather than taking the description's word. The shipped prompt contract asks the model for data the pipeline never supplies: formatLlmContent builds both evidence tiers as bare - ${source.url} lines, and nothing on the response path carries a title, yet CITATION_POLICY and the description bullet both demand "markdown links" and the description demonstrates a titled one (- [cms.gov transmittal R12951CP](…)). So on a surface #11348 turned on by default, the model is told to produce [title](url) and handed only url — it either invents a title or degrades to a URL slug as link text, which reads to the user as the page's real title. That is an internal inconsistency in shipped behaviour. Your before/after runs corroborate it; the code inconsistency alone already would.

Direction: aligned. This is decision 2's third option, the one #11564 itself enumerates ("or no titles at all with the citation policy asking for bare URLs"), and the prior triage on that issue argued it was the only zero-parser-surface choice. I re-verified the two rendering claims that make the tradeoff survivable, since everything rests on them: BARE_URL_PATTERN (osc8.ts:208) is part of the CLI's combined inline token pattern (InlineMarkdownRenderer.tsx:42), so a bare-URL Sources: list is already OSC 8-wrapped and clickable; Web Shell gets the same via remarkGfm autolink literals (Markdown.tsx:1029). The genuine loss is the human-readable label, which is exactly what #11564 says.

One question, raised as a question and not a block: #11564 still carries need-discussion, and the prior triage there explicitly reserved decision 2 for a maintainer — "Nothing should go back into code before 1 and 2 are settled." No maintainer has commented on that issue; you settled it in a comment and this PR would auto-close it. Withdrawing #11616 after two diverging rounds and a human-corroborated Critical is well-evidenced and I'm not second-guessing it. But trading citation readability for citation honesty, on a default-on surface, is a product call rather than a code call — worth a maintainer's explicit yes on the issue before this merges. @yiliang114 is already assigned, which is the right person by the owner map's path routing for packages/core/src/tools/.

Size: core paths touched (packages/*/src/tools/**), title type fix — so no Tier 1 hard block and no size escalation. Breakdown: 6 production logic lines (web-search.ts +3/−3), 21 test lines (web-search.test.ts +21), and 27 lines of e2e-plan markdown under .qwen/e2e-tests/. Far under both the 500 and 1000 thresholds. Every downstream consumer is nameable, which is what lets me proceed under Tier 2 rather than escalating on the core-path rule alone — the full list is in the code review below.

Approach: nothing to cut. I went looking for a third site that still says "markdown links" and there isn't one — repo-wide, the string appears exactly twice and you changed both, and the only ](https:// in the file is the example you replaced. That completeness is the whole job here; a half-applied wording change would have left the description and the result policy contradicting each other, which is the bug you're fixing. No drive-by edits, no scope creep. The e2e-plan file lands where 27 tracked precedents already live, so it follows practice even though .qwen/* is gitignored.

Risk: no elevated risk signals — none of the changed files match the revert-correlated path list. The one thing I checked that could actually have broken something is the result envelope: the policy grows ~156 chars and is appended after the body is bounded to MAX_RESULT_SIZE_CHARS, so a longer footer could in principle get bisected by the generic truncator and take SAFETY_FOOTER with it. It can't — envelope is now ~750 chars against RESULT_ENVELOPE_HEADROOM_CHARS = 2_000. Details below.

Moving on to code review. 🔍

中文说明

感谢贡献——也感谢你把 #11616 的撤回过程写清楚,而不是直接关掉。正是那段说明让这个 PR 变得可评审。

模板完整 ✓

问题: 是已观测到的问题,不是理论性加固;我没有只信 PR 描述,而是对 main 做了静态核对。当前上线的提示词契约向模型索要一条流水线从不提供的数据:formatLlmContent 把两个证据层级都输出为裸 - ${source.url},整条响应路径上没有任何 title,但 CITATION_POLICY 与描述条目都要求 "markdown links",描述里的示范还是带标题的(- [cms.gov transmittal R12951CP](…))。于是在 #11348 已默认开启的链路上,模型被要求产出 [标题](链接),拿到的却只有链接——它要么编一个标题,要么退化成用 URL 路径当链接文字,而用户会把它读成页面的真实标题。这是已上线行为的自相矛盾。你的 before/after 运行佐证了这一点;但仅凭代码里的这处矛盾就已成立。

方向: 对齐。这正是 #11564 自己列出的第 2 项决策的第三个选项("干脆不要标题、让引用策略要求裸 URL"),该 issue 上一次分诊也论证过这是唯一解析面为零的选择。这个取舍能否成立完全取决于两条渲染事实,我重新核对过:BARE_URL_PATTERNosc8.ts:208)在 CLI 合并后的行内 token 正则里(InlineMarkdownRenderer.tsx:42),所以裸 URL 的 Sources: 清单本来就会被 OSC 8 包装、可点击;Web Shell 通过 remarkGfm 的 autolink literals 得到同样效果(Markdown.tsx:1029)。真正的损失只有可读标签,这也正是 #11564 写明的。

有一个问题,作为提问而非阻断提出:#11564 仍带 need-discussion 标签,且上一次分诊明确把第 2 项决策留给 maintainer——"在第 1、2 项定下来之前不应再写代码"。该 issue 下没有 maintainer 回复过;你在评论里自行定了,而本 PR 会自动关闭它。#11616 在两轮发散评审和一条人工确认的 Critical 之后撤回,证据充分,这点我不质疑。但用引用可读性换引用真实性、且作用在默认开启的链路上,属于产品决策而非代码决策——合并前值得 maintainer 在 issue 里明确点头。@yiliang114 已被指派,按 owner map 对 packages/core/src/tools/ 的路径路由,正是合适的人选。

规模: 触及核心路径(packages/*/src/tools/**),标题类型为 fix——因此不触发 Tier 1 硬阻断,也不因规模上升。明细:6 行生产逻辑web-search.ts +3/−3)、21 行测试(web-search.test.ts +21)、27 行 .qwen/e2e-tests/ 下的 e2e 计划 markdown。远低于 500 与 1000 两个阈值。每一个下游消费方都能点名,这也是我能在 Tier 2 下继续、而不必仅因核心路径规则就上升的原因——完整清单见下方代码评审。

方案: 无可裁剪。我专门去找是否还有第三处写着 "markdown links",没有——全仓库该字符串只出现两次,你都改了;文件里唯一的 ](https:// 就是你替换掉的示范。这里的活儿正是这份完整性:文案只改一半会让描述与结果里的策略互相矛盾,而那恰恰是你要修的 bug。无夹带改动、无范围蔓延。e2e 计划文件落在已有 27 个同类被跟踪文件的位置,因此即便 .qwen/* 被 gitignore,它也符合既有实践。

风险: 无升级风险信号——改动文件均不匹配与 revert 相关的路径清单。我唯一认真查过、且真可能出问题的地方是结果信封:策略变长约 156 字符,而它是在 body 被限制到 MAX_RESULT_SIZE_CHARS 之后追加的,所以更长的尾注理论上可能被通用截断器拦腰截断,连带切掉 SAFETY_FOOTER。实际不会——信封现在约 750 字符,而 RESULT_ENVELOPE_HEADROOM_CHARS = 2_000。详见下方。

进入代码审查 🔍

Qwen Code · qwen3.8-max-2026-09-02

Reviewed at b26bf0689b4b3ecdc5b1a2e6753e15e7e3a0f87c · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Code review

Small diff, so I read the whole surface around it rather than just the three changed lines. No Criticals and no AGENTS.md violations. What follows is what I verified, because for a prompt-wording change the interesting question isn't "is the string well-formed" but "did you get all of it, and does anything downstream assume the old shape".

The change is complete. CITATION_POLICY has exactly one consumer — formatLlmContent's return body + CITATION_POLICY + SAFETY_FOOTER (web-search.ts:710) — and getWebSearchToolDescription() has exactly two, both inside the same file (the constructor at :879 and the schema getter at :910). Nothing outside web-search.ts reads either. Repo-wide, "as markdown links" appears at precisely the two sites you changed, and the file's only ](https:// is the example you replaced. So there is no third site left contradicting the new policy, and no cross-package consumer, snapshot, or docs page pinning the old wording — cms.gov appears nowhere else in the repo, and no user-facing doc describes the web_search citation format. That is the complete downstream-consumer list, which is why this passes the core-path confidence bar instead of escalating on it.

Your two negative assertions are non-vacuous. This is worth calling out because expect(x).not.toContain(…) is the easy way to write a test that passes for the wrong reason. Here neither does: since '](https://' occurs exactly once in the description and 'as markdown links' exactly twice in the file, both negatives fail if the wording regresses, and the paired positives ('as bare URLs, one per line', 'cannot be verified', the bare-URL example) pin the replacement rather than merely the absence. Your claim that reverting each of the three wording changes fails a test is consistent with what the assertions actually reach.

The new describe is wired correctly. It's appended at top level, outside describe('WebSearchTool execute'), which is usually where this goes wrong. It doesn't here: every helper it uses (mockCreate, makeStream, completedEvents, SEARCH_ITEM, EXTRACTOR_ITEM, MESSAGE_ITEM, runSearch, makeConfig) is module-level (web-search.test.ts:28–200), and so are the beforeEach/afterEach that reset mockCreate and TEST_ENV_KEY (:208/:213). The new block therefore inherits the same mock hygiene as the existing ones — no leakage from a preceding test's queued mockResolvedValueOnce, and no environment key left set. The fixtures contain no link syntax or the phrase "markdown links", so neither negative assertion collides with fixture text.

The policy's factual premise is true, not aspirational. "The page lists above give URLs only" is the load-bearing justification for refusing titles, so I checked it rather than trusting it: both evidence tiers are built by mapping each source to a bare - ${source.url} bullet (:666 for opened pages, :675 for unopened candidates). URLs only. The model genuinely has no verified title available, so the instruction tells the truth about its own inputs — which matters, because a citation policy that misdescribes the result is how you get a model improvising.

The result envelope still fits. This is the one way a longer footer could have broken something real. formatLlmContent bounds the body to MAX_RESULT_SIZE_CHARS (100 000) and only then appends the policy, the optional truncation note, and SAFETY_FOOTER; maxOutputChars is MAX_RESULT_SIZE_CHARS + RESULT_ENVELOPE_HEADROOM_CHARS (:871), and the comment at :41–46 states the headroom exists specifically so a max-size result doesn't get its footers bisected by the generic truncator. Your policy grows ~156 chars (344 → 501 line chars) and SAFETY_FOOTER is ~190, so the envelope lands around 750 chars of 2 000. Comfortable. Had it been tight, the casualty would have been the prompt-injection guard at the very end of the result — the worst possible line to lose — so I'm glad to be able to say it isn't close. The description bullet also grows ~124 chars (93 → 217), but that's schema text, not envelope, and carries no cap.

Two non-blocking observations, neither needing action:

  • This constrains the final Sources: section only. The side model's narration inside the result can still contain a titled markdown link, and the main model's prose can still emit one — your Risk section says exactly this, so it's disclosed rather than hidden. Worth being clear-eyed that the change narrows the invented-title surface (the authoritative source list is now title-free) rather than closing it.
  • Changing the description busts the prompt cache once on deploy, since the schema getter recomputes the string. That's inherent to any wording change and the getter's month-boundary comment already documents the stability contract. No action — just don't be surprised by it.

Test evidence — the PR's own CI

Unattended CI run, so under the static-review rule I built nothing and executed nothing: no vitest, no npm, no PR code. The evidence below is this PR's own CI read off the API at the reviewed commit, plus static verification of the assertions above. Nothing is red. The unit suite, lint, and integration tests were still running when I fetched — I did not poll or guess their outcome, so the rows below say in_progress and the finalize job will rewrite the table once CI settles.

No failing job, so there is no log excerpt to quote. The macOS and Windows Test legs and the sandboxed CLI integration leg are skipped by the workflow's own matrix and trigger conditions on this commit, not by anything in the diff — which does mean the only platform coverage in flight is Linux, matching your Tested-on table.

Final CI results for b26bf06 (auto-updated by the triage finalize job after CI completed):

Check Conclusion
Classify PR ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Integration Tests (no-AK, No Sandbox) ✅ success
Lint & Static (ubuntu-latest, Node 22.x) ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

Not verified: your 105-passing unit run, the 3/3 revert-catch claim, the clean eslint / prettier --check / tsc --noEmit, and every live-run number — the 9-of-9 bare URLs across three After runs, the 11-of-11 on the first wording revision, the 2 Before runs, and the per-run URL provenance counts. Those are the author's measurements against a live Token Plan endpoint on Linux with qwen3.6-plus; I quote them as your claims, not as evidence I reproduced. I could not have reproduced them here even in principle: no PR code is executed on this path, and model compliance isn't a static property.

Sandboxed verification would settle this: @qwen-code /tmux — the central claim is that the reworded policy actually changes what the model emits, and the new tests pin the strings, not compliance, so the suite passes identically whether or not any model obeys. A real session would show the Sources: section as bare URLs and, on an OSC 8 terminal, that they still render clickable — which is the whole justification for accepting the readability loss. @qwen-code /verify would add the A/B against the base build, confirming the wording delta is what carries the change. You have write access, so neither needs a sponsor.

Real-scenario testing (2c) is not applicable here: this is an unattended CI run, which never drives tmux. The live-behaviour gap is the /tmux line above, not a skipped local run.

中文说明

代码评审

改动很小,所以我读的是它周围的整片面,而不只是那三行。没有 Critical,也没有违反 AGENTS.md。下面写的是我实际核对过的内容——对一处提示词文案改动来说,有意思的问题不是"字符串写得对不对",而是"你改全了没有,以及下游有没有东西依赖旧形状"。

改动是完整的。 CITATION_POLICY 只有一个消费方——formatLlmContentreturn body + CITATION_POLICY + SAFETY_FOOTERweb-search.ts:710);getWebSearchToolDescription() 只有两个,且都在同一文件内(构造函数 :879schema getter :910)。web-search.ts 之外没有任何地方读取这两者。全仓库范围内,"as markdown links" 恰好只出现在你改的两处,文件里唯一的 ](https:// 就是你替换掉的示范。因此不存在第三处仍在与新策略矛盾的地方,也没有跨包消费方、快照或文档钉住旧文案——cms.gov 在仓库其他任何地方都不出现,也没有面向用户的文档描述 web_search 的引用格式。这就是完整的下游消费方清单,也是本 PR 能通过核心路径的置信度门槛、而不必因此上升的原因。

你的两条否定断言不是空断言。 值得专门点出来,因为 expect(x).not.toContain(…) 正是最容易写出"因错误原因而通过"的测试的地方。这里两条都不是:既然 '](https://' 在描述中只出现一次、'as markdown links' 在文件中只出现两次,一旦文案回退,两条否定断言都会失败;而配对的正向断言('as bare URLs, one per line''cannot be verified'、裸 URL 示范)钉住的是替换后的内容,而不只是"某个东西不在了"。你说回退三处文案改动各自都会让测试失败,这与断言实际能覆盖的范围是一致的。

新增 describe 的接线是对的。 它被追加在顶层、位于 describe('WebSearchTool execute') 之外,而这通常正是出问题的地方。这里没有:它用到的每一个辅助函数(mockCreatemakeStreamcompletedEventsSEARCH_ITEMEXTRACTOR_ITEMMESSAGE_ITEMrunSearchmakeConfig)都是模块级的(web-search.test.ts:28–200),重置 mockCreateTEST_ENV_KEYbeforeEach/afterEach 也是模块级的(:208/:213)。所以新块继承了与既有块相同的 mock 卫生条件——不会被前一个测试排队的 mockResolvedValueOnce 泄漏影响,也不会残留环境变量。fixture 中不含链接语法或 "markdown links" 字样,因此两条否定断言都不会与 fixture 文本相撞。

策略里的事实前提为真,不是一厢情愿。 "the page lists above give URLs only" 是拒绝标题的关键论据,所以我核对了它而不是直接相信:两个证据层级都是把每个来源映射为一条裸 - ${source.url} 条目(:666 为已打开页面,:675 为未打开候选)。只有 URL。模型确实拿不到任何已校验的标题,所以这条指令如实描述了它自己的输入——这点很重要,因为一份与结果内容不符的引用策略,正是模型开始自行编造的起点。

结果信封仍然放得下。 这是"更长的尾注"唯一可能真正弄坏东西的途径。formatLlmContent 先把 body 限制到 MAX_RESULT_SIZE_CHARS(100 000),之后才追加策略、可选的截断提示和 SAFETY_FOOTERmaxOutputCharsMAX_RESULT_SIZE_CHARS + RESULT_ENVELOPE_HEADROOM_CHARS:871),而 :41–46 的注释写明这段余量存在的目的正是防止满尺寸结果的尾注被通用截断器拦腰截断。你的策略变长约 156 字符(行长 344 → 501),SAFETY_FOOTER 约 190,所以信封总量约 750 字符,上限 2 000。余量充足。如果这里很紧,被牺牲的会是结果最末尾那道防提示注入的护栏——那是最不该丢的一行——所以能明确说"离得很远"是好事。描述条目也变长约 124 字符(93 → 217),但那是 schema 文本,不属于信封,也没有长度上限。

两条不阻断的观察,均无需处理:

  • 本改动只约束最终的 Sources: 小节。结果里侧模型的旁白仍可能包含带标题的 markdown 链接,主模型的正文也仍可能写出一个——你的风险小节已如实写明,所以这是已披露而非被掩盖。需要清醒的是:本改动收窄了编造标题的面(权威来源清单现在不含标题),而不是彻底关闭它。
  • 修改描述会在部署时打破一次 prompt cache,因为 schema getter 会重算该字符串。这对任何文案改动都是固有的,且 getter 关于月份边界的注释已经写明了这份稳定性契约。无需处理——只是别被它惊到。

测试证据 —— 本 PR 自己的 CI

这是无人值守的 CI 运行,因此按静态评审规则我没有构建、也没有执行任何东西:没有 vitest、没有 npm、没有 PR 代码。下面的证据是在被评审的 commit 上通过 API 读到的本 PR 自己的 CI,加上上文对断言的静态核对。没有红色项。 我抓取时单元测试、lint 与集成测试仍在运行——我没有轮询、也没有猜测结果,所以下表如实写 in_progress;CI 落定后 finalize 作业会重写这张表。

没有失败的作业,因此没有日志摘录可引。macOS 与 Windows 的 Test 分支以及带沙箱的 CLI 集成分支,是工作流自身的 matrix 与触发条件在这个 commit 上判为 skipped 的,与 diff 无关——这确实意味着在跑的只有 Linux 平台覆盖,与你的 Tested-on 表一致。

未验证: 你说的 105 个单测通过、3/3 的回退捕获、eslint / prettier --check / tsc --noEmit 干净,以及所有实测数字——改动后三次运行的 9/9 裸 URL、第一版文案上的 11/11、改动前的 2 次运行、每次运行的 URL 溯源计数。这些都是你在真实 Token Plan 端点、Linux、qwen3.6-plus 上的实测数据;此处按你的主张转述,不是我复现出的证据。即使不考虑规则,我在这条路径上也无法复现:不执行 PR 代码,而模型是否遵守指令本身就不是静态属性。

沙箱化验证可以定这件事:@qwen-code /tmux —— 本 PR 的核心主张是"改写后的策略确实改变了模型的输出",而新增测试钉住的是字符串、不是遵守情况,所以无论模型是否遵守,测试套件都会同样通过。一次真实会话能展示 Sources: 小节是裸 URL,并且在支持 OSC 8 的终端上它们仍可点击——而这正是接受可读性损失的全部理由。@qwen-code /verify 可以补上与 base build 的 A/B,确认承载这处改动的是文案增量本身。你有写权限,两者都不需要 sponsor。

真实场景测试(2c)在此不适用:这是无人值守的 CI 运行,从不驱动 tmux。上述 /tmux 一行就是这条实时行为缺口,而不是被跳过的本地运行。

Qwen Code · qwen3.8-max-2026-09-02

Reviewed at b26bf0689b4b3ecdc5b1a2e6753e15e7e3a0f87c · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — the diff itself I'd merge without hesitation; what I can't settle from the diff is whether this product decision was ours to make yet.

Before reading the diff I asked what I'd do about a model inventing page titles when the pipeline hands it only URLs. There are two shapes of answer: supply real titles, or stop asking for them. The first was attempted twice in this repo — #11490 went 16 findings to 29, #11616 went 16 to 29 plus a human-corroborated Critical, and in both cases most new findings were induced by the previous round's fixes. That's the signature of a mechanism whose surface grows faster than it can be secured. The second shape is three lines. This PR is the second shape, so it matches my independent proposal and lands on the convergent branch of a fork that had already diverged twice. I did not find a simpler path it missed.

The thing I'd want to say out loud, because it's easy to read a chain of four related PRs as volume and miss that this one is different in kind: the earlier three added machinery — a list parser, a title cleaner, URL normalization, a destination guard. This one deletes a requirement. It doesn't secure the title channel, it removes it, and the review divergence on #11490 and #11616 is evidence for it rather than against it. Judged on its own merits it's the right call, and I don't think I'm being worn down into accepting it.

It also solves something users actually feel. A fabricated title in a citation isn't cosmetic: the user reads a page name that doesn't exist and attributes it to the publisher, and the URL underneath is the only honest part. That was happening on a surface #11348 turned on by default.

If I were maintaining this in six months I'd thank whoever wrote it, for a reason that isn't the fix itself. The new wording carries its own justification — the page lists above give URLs only, so a title cannot be verified — which means the next person tempted to re-add titles has to argue with a stated reason instead of silently reverting a string. That's the difference between a wording change and a decision that survives.

So the code is not what I'm deferring on. Two things are, and I want to be precise about both because neither is resolvable from the diff, the tests, or the PR body:

1. The need-discussion decision was never ratified. #11564 still carries that label, and the triage on that issue explicitly reserved decision 2 for a maintainer — "A maintainer call on decision 2 … Nothing should go back into code before 1 and 2 are settled." No maintainer has commented there; the decision was recorded in an author comment, and this PR's Closes #11564 would shut the issue on that basis. I want to be fair about how much weight that carries: the option chosen is literally the third branch #11564 enumerates, and the prior triage argued for pricing it seriously, having verified that the usual objection doesn't hold in this codebase. I re-verified that myself — BARE_URL_PATTERN is in the CLI's combined inline token pattern, and Web Shell gets bare-URL links from remarkGfm autolink literals, so the citations stay clickable and the only real loss is the human-readable label. But "the analysis points this way" and "a maintainer decided this" are different things, and trading citation readability for citation honesty on a default-on surface is the second kind of question. That's a human call, not a review finding.

2. The behavioural claim is the author's, and I can't close the gap from here. Everything about whether models actually comply — 9 of 9 bare URLs across three After runs, 11 of 11 on the first wording revision, no markdown links — is author-reported, Linux only, one model family. The new unit tests pin the wording, which is the right thing for them to pin and I confirmed both negative assertions genuinely bite, but a prompt-wording change is exactly the case where a green suite proves the strings changed and nothing about whether the behaviour did. On this path I execute no PR code, so @qwen-code /tmux in the comment above is the lane that would settle it, not anything I can substitute for.

Neither of those is a defect in the PR. Escalating rather than approving, because the gate's job here is to get a maintainer to own the product decision, and approving would let it merge on my read of an argument a maintainer never weighed in on.

@yiliang114 — you're already assigned, which is the right routing by the owner map's path rules for packages/core/src/tools/. The call I'd like from you is narrow: is losing the readable label on the default web_search path acceptable, and should this PR close #11564 or should the issue stay open to record the decision? If yes to both, the code needs nothing further from my side — it's complete, the envelope math is safe, and the only thing outstanding is CI, which was still running when I reviewed (unit, lint, and integration legs in_progress; nothing red).

中文说明

Confidence: 3/5 —— 这份 diff 本身我会毫不犹豫合并;我无法从 diff 判断的是,这个产品决定是否已经轮到我们拍板。

在读 diff 之前我先问自己:模型在只拿到 URL 的情况下编造页面标题,我会怎么处理。答案只有两种形状:提供真实标题,或者不再索要标题。第一种在本仓库已经试过两次——#11490 从 16 条意见涨到 29 条,#11616 从 16 条涨到 29 条、外加一条人工确认的 Critical,而且两次的新增意见大多由上一轮修复引出。这正是"机制的表面积增长快于可被加固的速度"的典型特征。第二种形状是三行代码。本 PR 就是第二种,因此它与我的独立方案一致,并且落在这个已经两次发散的岔路的收敛分支上。我没有找到它漏掉的更简路径。

有一点我想明确说出来,因为把连着四个相关 PR 读成"数量压力"、从而看不出这一个在性质上不同,是很容易的:前三个都在增加机制——清单解析器、标题清洗、URL 规范化、目的地校验。而这一个在删除一项要求。它不是去加固标题通道,而是把通道去掉,所以 #11490#11616 的评审发散是支持它的证据,而不是反对它的证据。按它自身的价值判断,这是正确的选择;我也不认为自己是被磨到接受它的。

它解决的也是用户真能感觉到的问题。引用里编造的标题不是表面瑕疵:用户读到一个并不存在的页面名,并把它归给该发布方,而下面那条 URL 是唯一诚实的部分。这发生在 #11348 已默认开启的链路上。

如果半年后由我来维护这段代码,我会感谢写下它的人,而且理由不在修复本身。新文案自带论证——the page lists above give URLs only, so a title cannot be verified——这意味着下一个想重新加回标题的人必须与一个写明理由的判断交锋,而不是悄悄改回一个字符串。这是"一次文案改动"与"一个能留下来的决策"之间的区别。

所以我不是在代码上暂缓。让我暂缓的是两件事,我想把两件都说准确,因为它们都无法从 diff、测试或 PR 描述里解决:

1. need-discussion 的决策从未被确认。 #11564 仍带该标签,而那个 issue 上的分诊明确把第 2 项决策留给 maintainer——"请 maintainer 就第 2 项拍板……在第 1、2 项定下来之前不应再写代码"。该 issue 下没有 maintainer 回复过;决策被记录在作者的一条评论里,而本 PR 的 Closes #11564 会据此关闭该 issue。关于这一点该占多大分量,我想说得公道:所选方案恰恰就是 #11564 自己列出的第三个分支,且上一次分诊是主张认真给这个选项定价的,因为它已核实过常见反对理由在本仓库不成立。我自己也重新核实了——BARE_URL_PATTERN 在 CLI 合并后的行内 token 正则里,Web Shell 通过 remarkGfm 的 autolink literals 得到裸 URL 链接,所以引用仍然可点击,真正损失的只有可读标签。但"分析指向这个方向"与"maintainer 做了这个决定"是两回事,而在默认开启的链路上用引用可读性换引用真实性,属于后一类问题。这是人的决策,不是评审发现。

2. 行为层面的主张来自作者,而我在这里无法补上这个缺口。 关于模型是否真的遵守——改动后三次运行的 9/9 裸 URL、第一版文案上的 11/11、零 markdown 链接——全部是作者报告的,仅 Linux、单一模型系列。新增单测钉住的是文案,这对它们来说是正确的职责,我也确认了两条否定断言确实有效;但提示词文案改动恰恰是那种"绿色套件只证明字符串变了、对行为是否改变毫无说明"的情形。在这条路径上我不执行 PR 代码,所以能定这件事的是上面评论里的 @qwen-code /tmux,而不是任何我能替代的东西。

这两点都不是 PR 的缺陷。我选择上升而不是批准,因为这里 gate 的职责是让 maintainer 来承担这个产品决策;而批准等于让它凭我对一个 maintainer 从未参与过的论证的解读就合并。

@yiliang114 —— 你已被指派,按 owner map 对 packages/core/src/tools/ 的路径规则,这是正确的路由。我想请你拍的这个决定很窄:在默认的 web_search 链路上失去可读标签是否可以接受,以及本 PR 是否应该关闭 #11564、还是让该 issue 保持打开以记录这个决策? 如果两者都是"是",代码这边不需要我再提任何意见——它是完整的,信封计算是安全的,唯一未落定的是 CI:我评审时单元测试、lint 与集成分支仍为 in_progress,没有红色项。

Qwen Code · qwen3.8-max-2026-09-02

Reviewed at b26bf0689b4b3ecdc5b1a2e6753e15e7e3a0f87c · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed. Suggestions are inline.

Not explored to full depth (tool budget reached): "agent 1c": none — all planned checks completed (~23 tool calls). I did not execute the vitest suite; the test-file conclusions above are from reading the helpers and liter….

Test Plan (not a blocker): src/tools/web-search.test.tsno such file or directory; 105 passed — this review observed 25539, 2054, 30788, 1016, 2000, 569, 7430 passed.

中文说明

已审查。 建议见行内评论。

未探索到全部深度(达到工具调用预算):"agent 1c"none — all planned checks completed (~23 tool calls). I did not execute the vitest suite; the test-file conclusions above are from reading the helpers and liter…

Test Plan(非阻断):src/tools/web-search.test.tsno such file or directory; 105 passed — this review observed 25539, 2054, 30788, 1016, 2000, 569, 7430 passed

— qwen3.8-max via Qwen Code /review (v0.23.3)

Comment thread packages/core/src/tools/web-search.ts
Comment thread packages/core/src/tools/web-search.ts
@qqqys
qqqys enabled auto-merge September 12, 2026 04:23

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

APPROVE — no Critical. CI green at b26bf068, three files, +3/-3 of production change. The two open threads are both Suggestion-severity and both forward-looking drift risks rather than defects at this commit.

The instruction now matches what the tool actually returns

WebSearchSource carries url and opened and nothing else, so there is no verified page title in the result for the model to cite. The old policy asked for "the relevant URLs from above as markdown links", which means link text — and the only title-shaped material in context is the search backend's narrated findings, i.e. untrusted external content. So the previous instruction did not merely permit an unverifiable title, it pointed the model at the one source of titles that SAFETY_FOOTER two lines above tells it to treat as data rather than as trustworthy. "Do not add titles or link text: the page lists above give URLs only, so a title (even one repeated from the narrated findings) cannot be verified" closes that, and naming the narrated-findings case explicitly is what makes it stick rather than reading as a style preference.

The example was part of the bug: it demonstrated - [cms.gov transmittal R12951CP](https://…), so a model following the example rather than the prose would produce exactly the titled markdown link the policy is trying to prevent. It is now the bare URL. Both copies of the rule — the per-result CITATION_POLICY footer at web-search.ts:632 and the CRITICAL REQUIREMENT bullet in the tool description at :838 — agree on the format and each states the reason in its own terms, and the three attribution rules that actually carry the safety weight (opened evidence pages first, an unopened candidate only when it directly supports the claim, never attach a URL that was not returned) are untouched.

The tests pin both sides with negative assertions as well as positive ones: not.toContain('as markdown links') on the result and not.toContain('](https://') on the description. That is what makes this safe to land — a partial revert that fixes one copy and not the other, or that restores the markdown example, goes red instead of shipping.

Non-blocking

R1-2 is the one I'd act on, and this PR is itself the evidence. The citation policy is hand-written twice with independently worded rationales, and the description records that the follow-up commit b26bf068 exists solely to re-word that rationale — which it had to apply to both copies in one change. The two are not equivalent in reach: the description copy sits under CRITICAL REQUIREMENT - You MUST follow this and is present on every request, while the footer rides only on a web_search result, so if they ever diverge the description wins and a relaxation applied only to the footer would be silently inert. Extracting one constant interpolated into both would remove the duplication and the asymmetry at once.

R1-1 is a fair guard but cheaper to satisfy than it sounds. Nothing pins that WebSearchSource stays {url, opened}, so the realistic regression is the withdrawn title-extraction direction (#11616): a backend author adds title, renders it into the evidence bullet, and from then on every result tells the model a title "cannot be verified" while titles sit in the list directly above that sentence. The bot is right that this is exposure rather than a present defect. A one-line assertion that the rendered page-list rows contain no title field would pin the premise the policy text asserts, and would fail loudly at the moment the premise stops holding instead of leaving the model with a contradiction.

The .qwen/e2e-tests/ note is honest about its baseline being a local build of main under an isolated HOME rather than an installed release, and names the artefact to inspect (functionResponse in ~/.qwen/projects/*/chats/*.jsonl) for step 3, which is the part a reader would otherwise have no way to check.

@qwen-code-dev-bot qwen-code-dev-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

APPROVE

已核对 head b26bf0689b4b3ecdc5b1a2e6753e15e7e3a0f87c(两个提交:95ca4c60d3 改引用形态,b26bf0689b 改理由措辞;vs 已合入的 main 28df8b8a78)。改动仅 3 行产代码 + 21 行用例 + 1 份 e2e 记录。

独立复查未发现 Critical

  • 提示词里那句「上面的页面清单只给 URL」在当前实现下是真的:buildBody 的两段列表都是 opened.map((source) => - ${source.url}) / candidates.map((source) => - ${source.url})web-search.ts:666:675),WebSearchSource 只带 urlopened,没有标题字段,所以要求模型不要补标题(补了也无法核验)比原先「写成 markdown 链接」更贴近事实,也消掉了模型给不存在的标题编 link text 的路径。
  • 两份引用规则(CITATION_POLICY 脚注与工具描述里的 CRITICAL REQUIREMENT 段)在本次改动里都被改写,且各自有独立见证。

本地验证src/tools/web-search.test.ts 在该 head Test Files 1 passed (1) / Tests 105 passed (105)。两侧分别回退做变异:只把脚注退回 as markdown links. Add titles:asks the model to cite bare URLs without titles 单条变红;只把描述里的示例退回 [cms.gov transmittal R12951CP](https://…)shows a bare URL citation example in the tool description 单条变红。即本 PR 改的两处文案都有能变红的钉法,不存在「改了一半没信号」。

CI:required 全部 success(Test (ubuntu-latest, Node 22.x)Lint & StaticIntegration Tests (no-AK, No Sandbox)web-shell E2E Smoke),无失败无 pending。

两条待回应的 Suggestion(不阻塞,记在这里以免静默丢失)

  • web-search.ts:632:钉的是文案,没钉住文案所依赖的事实。若后续有人给 WebSearchSourcetitle 并渲染进 :666/:675 的列表,提示词会变成假的而全绿。补一条列表形状断言即可闭合(例如取 llmContent 里那两段列表的每个条目行,断言形如 - https://… 整行、不含方括号链接文本或额外标题)。
  • web-search.ts:838:同一规则手抄两处,本次就改了两遍。可让描述段的规则文本由 CITATION_POLICY 生成,或加一条「两处都必须包含同一个格式短语」的一致性用例,避免以后只改一处。
    两条都是未来漂移防护,当前行为与用例均自洽,故按 Suggestion 记录。

@qqqys
qqqys added this pull request to the merge queue Sep 12, 2026
Merged via the queue into QwenLM:main with commit 4856066 Sep 12, 2026
152 checks passed
@chiga0

chiga0 commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

Post-merge review — published after merge (2026-09-12). No merge gate implication.


Verdict

No blocking findings. No approval blockers.


Scope and tier

Scan — pure prompt-wording change. No logic paths touched, no persisted formats, fully reversible.

Reviewed all three changed files in full. Cross-checked against the existing triage-bot review.


What I checked

Change completeness (Class 10 — stated intent vs code)
The PR description says "Both places that tell the model how to cite change together." Confirmed at head b26bf0689b:

  • CITATION_POLICY constant (web-search.ts:631): updated from "as markdown links" to "as bare URLs, one per line", with added explanation for why titles cannot be verified.
  • getWebSearchToolDescription() (web-search.ts:838): instruction line and example both updated; example changes from - [cms.gov transmittal R12951CP](https://...) to - https://www.cms.gov/files/document/r12951cp.pdf.

No third cite-instruction site exists in web-search.ts. Repo-wide grep ("as markdown links") confirmed by the existing triage review that the string appears at exactly the two changed locations.

Consumer coverage (Class 1 — contract asymmetry)
CITATION_POLICY has one consumer: formatLlmContent at web-search.ts:710. getWebSearchToolDescription() has two consumers, both inside the same file. Nothing outside web-search.ts parses or processes either. No downstream parser assumes markdown-link shape.

Test quality (Class 5)
Two new WebSearchTool citations tests:

  1. Pins both the new policy wording ("as bare URLs, one per line", "cannot be verified") and the absence of the old wording ("as markdown links").
  2. Pins the bare-URL example in the tool description and the new "do not wrap" instruction; .not.toContain("](https://") ensures no markdown-link syntax survives.

The PR description states that reverting each of the three wording changes makes the relevant test fail (3/3 caught) — consistent with the test structure above.

Cross-check outcome
The triage-bot stage-2 review independently confirmed change completeness and no external readers of either prompt string. No conflicting or unaddressed findings to carry forward.


Reviewed with AI assistance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/self-reported The linked issue was opened by the PR author (self-reported)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

web_search: design page titles for cited sources (split from #11490)

5 participants