Skip to content

feat(runtime): add turn-scoped tool_search activation - #3765

Merged
likun666661 merged 4 commits into
mainfrom
feat/tool-search-lazy-loading
Aug 25, 2026
Merged

feat(runtime): add turn-scoped tool_search activation#3765
likun666661 merged 4 commits into
mainfrom
feat/tool-search-lazy-loading

Conversation

@likun666661

Copy link
Copy Markdown
Member

Summary

  • replace economy/load_tools availability with a provider-independent tool_search contract
  • cache a MiniSearch index per backend while keeping activation in TurnScope.activeTools
  • expose activated schemas on the next provider step and preserve the same-step execution guard
  • keep search bounded by result count and schema bytes, with repeated and parallel searches unioned per turn
  • migrate catalog, Host composition, telemetry, UI, bundled skill guidance, docs, tests, and third-party notices
  • retain historical load_tools decoding only for transcript compatibility, never for cross-turn activation

This implements the first slice agreed in Discussion #3621.

Closes #3752.

Testing

  • npm run format:check
  • npm run build:test
  • npm run typecheck
  • focused Runtime tool availability, activation, pruning, capacity, and retry suites
  • npm run check:third-party-notices
  • npm run check:cli-third-party-notices
  • npm audit --omit=dev --audit-level=moderate
  • npm audit signatures

The complete local workspace test runner still encounters existing platform-sensitive failures in the macOS Bash executable-root assertion and Desktop deadline tests. The affected tool-search suites pass independently.

AI assistance disclosure

Maka helped implement the agreed design, migrate tests and documentation, and run validation. I reviewed the resulting behavior and diff.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I reviewed this head and found blocking issues.

[P2] Client Capability search loses human meaning

Tool offer label/description are stored but not indexed; searching "schedule a calendar meeting" fails to activate an opaque remote/invoke tool whose capability is only in those fields, leaving it invisible to both search and model.

[P2] Per-search 64 KiB limit allows unbounded Turn-owned activation

The 64 KiB schema cap resets per search, but results are unioned into a persistent activeTools set. Three ~40k activations reach 120k, eventually exhausting context budget without compaction. The budget should be tracked cumulatively per Turn scope.

Checks on ca04b00d9f are test/audit/package: success.

简体中文存在搜索信息与预算两项阻断。

@likun666661

Copy link
Copy Markdown
Member Author

On the second point: the 64 KiB ceiling is intentionally per search, not cumulative per turn. Discussion #3621 and issue #3752 explicitly settled on monotonic turn-scoped accumulation with no turn-wide activation budget or unload operation; the model may make repeated searches to expand the active set, while each individual search is bounded by result count and schema bytes. The binding ceiling remains the final upper bound.

A cumulative cap would introduce a new ordering-dependent failure mode: an earlier approximate search could consume the budget and prevent a later, more precise search from activating the required tool. That is a different contract from the agreed first slice. Context capacity is still measured on every provider projection and handled by the existing capacity/compaction path.

The first point is valid. I will update Client Capability search documents to include the offer label/description while keeping the initial model-facing inventory limited to group ids and canonical tool names.

@likun666661

Copy link
Copy Markdown
Member Author

The first point is fixed in 838516e.

Search documents now include the owning group id, label, and description in addition to the canonical tool name and callable description. The initial tool_search inventory remains intentionally limited to group ids and canonical names.

Coverage includes:

  • a Runtime unit case where an opaque remote_invoke tool is found only through Team calendar / Schedule a calendar meeting group metadata
  • the Client Capability UDS path searching an opaque provider tool through offer.description

Build, typecheck, both focused tests, formatting, and diff checks pass.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Update on 838516e22d:

The previous search-semantics P2 is now closed (offer label/description only enter the per-member document, not initial inventory). The remaining P2 persists: per-search schemaChars resets while Turn-scoped activation is monotonic — exercising the limit three times reaches 120k and would exhaust budget without per-Turn accounting.

No new P0/P1; two P3 candidates remain for Map<MakaTool>Set<string> and duplicated direct/searchable authority, plus a pending decision on MiniSearch value vs cost.

Checks on 838516e22d are still running — code is NO-GO due to remaining P2.

简体中文一处语义已闭合,预算阻断仍在。

@likun666661

Copy link
Copy Markdown
Member Author

The remaining points do not identify implementation defects. They mix the accepted product contract, state representation, and executable authority into one review concern.

1. Per-search schema budget

The absence of a cumulative Turn budget is intentional and explicitly specified in #3621/#3752. Activation is monotonic within the Turn, there is no unload operation, and each search is independently bounded. The model is allowed to refine or expand the active surface until the binding ceiling.

A cumulative cap would be a semantic change, not a safety fix. It makes activation order-dependent: an approximate early search could consume the Turn allowance and prevent a later precise search from activating the required tool. That is exactly why the agreed contract uses per-call limits plus the binding ceiling.

The claim that this exhausts context “without compaction” also does not match the implementation. Every provider projection measures the complete currently visible schema surface, and the existing mid-turn capacity path includes schema growth from tool_search. packages/runtime/src/tests/mid-turn-capacity-backend.test.ts has a regression case named “the trigger counts same-turn tool-schema growth from tool_search”. If activation reaches the complete bound surface, that is the intentional ceiling, not unbounded state.

Changing this contract requires reopening the design decision with evidence from evaluation; it is not a P2 implementation bug in this PR.

2. Map<string, MakaTool> versus Set

These types encode different state. A Set records only membership. The Map records the Turn-owned resolved binding associated with each activated name. Replacing it with a Set discards that information and requires the projection path to resolve names back through a backend-owned registry.

The current Map is also the exact state shape agreed in #3752. There is no demonstrated correctness, memory, or performance problem here. A representation-only rewrite that throws away the resolved value is not required for this PR.

3. Direct and searchable are not duplicate executable authorities

There is one executable authority: the tools actually bound to the run.

  • boundTools answers whether a tool can exist or execute
  • the direct baseline answers whether a bound tool is projected initially
  • searchable group metadata answers how the remaining bound tools are discovered

ToolAvailabilityRuntime intersects every group with the known bound-tool set, ignores unknown members, prevents group metadata from deferring the fixed direct baseline, and can never create a binding. These are policy projections over one authority, not competing authorities. Removing either visibility classification would not simplify authority; it would remove required policy.

4. MiniSearch value and dependency cost

MiniSearch is not an arbitrary fuzzy-search dependency. It implements BM25+ scoring, which is the retrieval model selected by OpenAI Codex for its current tool_search implementation.

Codex main at 9c9675d3d0:

  • codex-rs/core/src/tools/handlers/tool_search.rs builds a bm25 SearchEngine over deferred tool search text
  • ToolSearchHandlerCache reuses that handler/index and rebuilds it when dynamic metadata changes
  • codex-rs/Cargo.toml pins bm25 = 2.3.2

Reference: https://github.com/openai/codex/blob/9c9675d3d0/codex-rs/core/src/tools/handlers/tool_search.rs

Maka follows the same architecture in TypeScript: backend-scoped cached index plus Turn-scoped activation. MiniSearch 7.2.0 provides BM25+, has zero runtime dependencies, is MIT licensed, is exact-pinned, has generated ASF notices, and passes dependency audit/signature checks. Reimplementing BM25 ranking and indexing locally would add code and maintenance risk without removing the underlying algorithmic cost.

The Client Capability P2 was valid and is fixed. The remaining cumulative-budget objection contradicts the accepted contract, while the Map, classification, and MiniSearch points do not establish defects. Please close the remaining P2 unless there is new evidence that the implemented behavior differs from #3752.

@Astro-Han

Copy link
Copy Markdown
Contributor

Agreed on all four — closing the remaining P2.

The cumulative-budget point was mine to withdraw. mid-turn-capacity-backend.test.ts does carry the schema-growth trigger over from load_tools, so activation degrades into the existing capacity path rather than blowing the window. The Map, classification and MiniSearch points were reopening #3621/#3752 rather than reporting defects, and the Codex bm25 reference settles the retrieval choice.

One thing I want recorded but not fixed here: when activation grows the surface enough to trigger compaction, the cost is paid in transcript, not in a failed search. That follows from the agreed contract rather than from anything in this PR — I will track it as something to measure once tool_search is live.

Thanks for the sourced replies, and for closing the search-document gap.

简体中文

四条我都同意,剩下那条 P2 关掉。

累计预算那条该由我撤回:mid-turn-capacity-backend.test.ts 确实把 schema 增长的触发从 load_tools 承接了过来,所以激活会退化到既有容量路径,不会撑爆窗口。Map、分类和 MiniSearch 三条是在重开 #3621/#3752,而不是报告缺陷;Codex 的 bm25 引用也把检索选型说清楚了。

有一点我想记下来但不在这里改:当激活确实把可见面撑到触发压缩时,代价是付在转录上,而不是一次失败的搜索。这来自已定的契约,不是本 PR 的问题——等 tool_search 上线后我会把它作为需要度量的项跟踪。

谢谢你带出处的回复,也谢谢补上搜索文档那个缺口。

@Astro-Han

Copy link
Copy Markdown
Contributor

I read the full diff this time rather than only the thread. One thing I would like fixed before approving, plus a few deletions that are cheap to take while this area is open.

[P2] Already-active tools consume the next search's limit and byte budget

In the connector impl, both the limit check and the schemaChars accumulation happen before anything consults activeToolsnewlyActivated is only computed after the loop:

for (const name of ranked) {
  if (activated.length >= limit) break;
  const tool = this.toolsByName.get(name);
  if (!tool || !this.searchableNames.has(name)) continue;
  const chars = toolSchemaCharsForDiagnostics([tool], [tool.name]);
  if (schemaChars + chars > TOOL_SEARCH_MAX_SCHEMA_CHARS) continue;
  activated.push(name);
  schemaChars += chars;
}
const newlyActivated = activated.filter((name) => !activeTools.has(name));

When the model refines a search, the default limit of 8 can be filled entirely by tools the previous search already activated. The call returns activated with 8 entries, newlyActivated empty, and nothing new becomes visible. The model only sees { activated: [...] }, so it cannot distinguish "no further matches" from "the slots went to results I already have" — and since activation is monotonic with no unload, refining again makes it worse, which is exactly what the inventory text invites: "Search again to expand the active set."

One line closes it: skip already-active names before the limit and budget checks, or union them into the response without charging them.

This is not the cumulative-Turn-budget point from the earlier thread — I accepted that one. This is the existing per-search budget, and the two remaining notes below are on the same branch.

[P3] A single tool larger than 64 KiB can never be activated

schemaChars starts at zero, so any tool whose own schema exceeds TOOL_SEARCH_MAX_SCHEMA_CHARS fails schemaChars + chars > MAX on every search regardless of rank. It stays bound, stays searchable, and is never callable, with no signal to the model. A large MCP tool schema over 64 KiB is not exotic. At minimum, activated being empty while ranked is not should be distinguishable in the result.

[P3] Relevance order is silently broken by size

The budget check uses continue, not break, so a lower-ranked smaller tool is activated while the top match is skipped. I cannot tell from the code whether that is deliberate. Either way, activated no longer reflects ranking, and the model has no other source for it.

These three share one cause: TOOL_SEARCH_MAX_SCHEMA_CHARS appears exactly twice in the diff — its definition and its single use. No test exercises the branch. repeated and parallel searches union and deduplicate turn activation uses limit: 1 and asserts the union, which passes straight over it.

Deletions still available in this area

[P1] onSearch and ToolSearchTrace have no production consumer. prepare's third parameter is never passed by the only production call site — ai-sdk-backend.ts calls prepare(scope.activeTools, requiredOrchestrationTools). The single caller that passes it is a test. The same payload already leaves through context.emitRunTrace('tool_searched', …), which run-trace.ts gained a type for in this PR. Dropping the exported interface, the third parameter, and the intermediate trace constant changes no telemetry; the test can assert the run trace instead.

[P2] ProductToolSurfacePolicy is now an empty object. With economy gone, the only production call site is policy: {}. The remaining field, disabledSurfaceIds, never had a producer — the pre-change call site passed only economy. That leaves ProductToolSurfacePolicy, NormalizedProductToolSurfacePolicy, the policy parameter, the normalize/lookup/Unknown product-tool surface branch at tool-catalog-derive.ts:113-119, and the disabledSurfaceIds field on EffectiveProductToolSurface as a path production has never taken.

[P2] CatalogSurfaceDef.availability is an inert discriminator, and a second answer to one question. Its type is the single literal 'searchable' and all four surfaces declare it, so if (surface.availability !== 'searchable') continue can never be true. What actually decides direct versus searchable is the hardcoded DIRECT_TOOL_NAMES in tool-availability.ts. This predates the PR as economy: 'deferred'; it was carried across under a new name.

One thing worth deciding, not fixing here

buildDiagnostic still treats a group as enabled when every member is active. Under load_tools(group) that matched a real event. Under per-tool activation with a default limit of 8, a group with more members than the limit can never be reported enabled. request-shape.ts:559 depends on it for the tool_source_enabled change reason — the PR correctly widened the mode check, so this is not a defect, but the branch is close to unreachable in search mode and the reason degrades to tool_source_state_changed.

The fields cannot simply go, since telemetry-file-schema.ts still reads historical records. What needs deciding is what search mode writes: keep group-shaped diagnostics and accept that enabledSourceIds is usually empty, or add tool-shaped fields and let the group ones serve history only. Happy to take that to the issue rather than hold this PR.

What this gets right

The persisted surfaces are handled properly: mode widens to a union instead of being replaced, and telemetry-file-schema.ts accepts both values, so historical telemetry stays readable. load_tools decoding survives for transcripts but not for cross-turn activation, and MAKA_DISABLE_DEFERRED_TOOLS is gone from both its reader and the macOS verify script. The backend-scoped index versus turn-scoped activation split is clean — prepare(activeTools) keeps the mutable state outside the runtime, and step.active is replaced rather than mutated so an in-flight step keeps its own snapshot. And it is a real net deletion: economy mode, ledger re-seeding, SEED_CONNECTOR_NAMES, and the multi-key extractGroupId guessing all go.

Fix the P2 with a test on that branch and I am happy to approve.

简体中文

这次我完整读了 diff,不只是跟着讨论走。有一处希望在 approve 之前修掉,另外趁这块打开顺手可以删几处。

[P2] 已激活的工具会吃掉下一次搜索的名额和字节预算

连接器 impl 的循环里,limit 判断和 schemaChars 累加都发生在任何「是否已激活」检查之前——newlyActivated 是循环结束之后才算的。

模型细化搜索时,默认 limit = 8 可能被上一次已经激活的工具全部占满:返回的 activated 有 8 项,newlyActivated 为空,实际没有任何新工具可见。模型只看到 { activated: [...] },无法区分「没有更多匹配」和「名额都给了我已经有的结果」。而激活是单调的、没有 unload,再细化只会更糟——恰恰 inventory 文案写的就是 "Search again to expand the active set"。

一行即可:在 limit 与预算判断之前跳过已激活的名字,或者把它们并入返回但不计名额与预算。

这不是之前那条「跨搜索累计预算」——那条我已经接受了。这里说的是现有的 per-search 预算,下面两条也在同一个分支上。

[P3] 单个超过 64 KiB 的工具永远激活不了

schemaChars 从 0 起,所以自身 schema 超过 TOOL_SEARCH_MAX_SCHEMA_CHARS 的工具,在任何一次搜索里都必然被跳过,排名再高也一样。它 bound 了、搜得到、却永远不可调用,而且模型收不到任何信号。大型 MCP 工具的 schema 超过 64 KiB 并不罕见。至少应当让「ranked 非空但 activated 为空」在结果里可区分。

[P3] 相关性顺序被体积静默打破

预算判断用的是 continue 而不是 break,所以排名更低但体积更小的工具会被激活,而 top 匹配被跳过。从代码看不出这是刻意还是顺手。无论哪种,activated 都不再反映排序,而模型没有别的信息源。

这三条同一个根因:TOOL_SEARCH_MAX_SCHEMA_CHARS 在整个 diff 里只出现两次——定义和唯一使用处,没有任何测试走这条分支。repeated and parallel searches union and deduplicate turn activation 用的是 limit: 1 并且只断言并集,正好从上面绕了过去。

这块还能继续删的部分

[P1] onSearchToolSearchTrace 没有生产消费者。 prepare 的第三参在唯一的生产调用点没有被传——ai-sdk-backend.ts 调的是 prepare(scope.activeTools, requiredOrchestrationTools),唯一传它的是一处测试。同样的负载已经通过 context.emitRunTrace('tool_searched', …) 发出,run-trace.ts 也在本 PR 里为它加了类型。删掉导出的接口、第三参和中间的 trace 常量,遥测毫无变化;那条测试改为断言 run trace 即可。

[P2] ProductToolSurfacePolicy 现在是空对象。 economy 去掉之后,唯一的生产调用点变成 policy: {}。剩下的 disabledSurfaceIds 从来没有过生产者——改动之前的调用点也只传 economy。于是 ProductToolSurfacePolicyNormalizedProductToolSurfacePolicypolicy 参数、tool-catalog-derive.ts:113-119 的归一化/查表/Unknown product-tool surface 分支,以及 EffectiveProductToolSurface.disabledSurfaceIds,都是生产从未走过的路径。

[P2] CatalogSurfaceDef.availability 是惰性判别式,也是同一个问题的第二个答案。 它的类型是单值字面量 'searchable',四个 surface 全都这么声明,所以 if (surface.availability !== 'searchable') continue 永远不为真。真正决定 direct 还是 searchable 的是 tool-availability.ts 里硬编码的 DIRECT_TOOL_NAMES。这条在本 PR 之前就存在(原为 economy: 'deferred'),本次是改名沿用。

一件值得定、但不必在这里改的事

buildDiagnostic 仍然在「group 的每个成员都激活」时判定该 group 为 enabled。在 load_tools(group) 模型下这对应一个真实事件;在逐工具激活、默认 limit 为 8 的模型下,成员数超过 limit 的 group 永远不可能被报为 enabled。request-shape.ts:559 依赖它产出 tool_source_enabled 变更原因——PR 已经正确放宽了 mode 判断,所以这不是缺陷,但该分支在 search 模式下基本不可达,原因会退化成 tool_source_state_changed

字段本身不能直接删,telemetry-file-schema.ts 还要读历史记录。需要定的是 search 模式写什么:保留 group 单位并接受 enabledSourceIds 基本恒空,还是新增 tool 单位的字段、让 group 字段只服务历史。这条我更想放到 issue 上,不必卡住本 PR。

做得好的地方

持久化面处理得规范:mode 是放宽成联合而不是替换,telemetry-file-schema.ts 同步接受两个取值,历史遥测仍可读。load_tools 的解码为转录保留、但不用于跨轮激活;MAKA_DISABLE_DEFERRED_TOOLS 在读取端和 macOS 验证脚本两侧都清干净了。backend 级索引与 turn 级激活的切分很干净:prepare(activeTools) 把可变状态留在 runtime 之外,step.active 用替换而非原地修改,在途的 step 保有自己的快照。而且这是真正的净删除:economy 模式、ledger 重播种、SEED_CONNECTOR_NAMES,连多 key 猜测的 extractGroupId 一并消失。

把那条 P2 修掉并给这条分支补个测试,我这边就可以 approve。

@likun666661

Copy link
Copy Markdown
Member Author

Addressed in c28cb8d.

Search expansion and schema budget

  • already-active tools are filtered before the result-count slice and schema accounting, so repeated/refined searches spend both limits only on new candidates
  • the model-facing result now includes an optional blocked record with the candidate name, schema size, and either schema_too_large or schema_budget_exhausted
  • schema exhaustion now stops at the ranked prefix instead of continuing to a lower-ranked smaller tool
  • the tool_search description explains the blocked result

Added regression coverage for:

  • a second search expanding beyond a previously active 40 KiB match without charging its count or bytes again
  • one tool larger than 64 KiB producing a model-visible schema_too_large result
  • a ranked prefix stopping with schema_budget_exhausted instead of silently activating a lower-ranked tool

Deletions

  • removed the unused onSearch callback and exported ToolSearchTrace; tests now assert the production emitRunTrace path
  • removed ProductToolSurfacePolicy, disabledSurfaceIds, the unreachable lookup/error path, and the empty production policy argument
  • removed the single-value CatalogSurfaceDef.availability discriminator and its unreachable branch

I left the search-mode diagnostic shape unchanged as suggested; that deserves a separate measurement/design issue rather than expanding this PR.

Validation:

  • npm run format:check
  • npm run build:test
  • npm run typecheck
  • 193 focused Runtime/Host tests covering search, catalog derivation, backend activation, capacity, retry, composition, and Client Capability UDS
  • git diff --check

@Astro-Han

Astro-Han commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Thanks for the fast turnaround — I read c28cb8d3. The limit/budget accounting, the ordering break, onSearch, ProductToolSurfacePolicy, and availability are all resolved, and the three new tests land on exactly the branch that had none. One of the fixes introduced a new problem, and the deletions left a few loose ends.

[P2] schema_too_large should continue, not break

if (chars > TOOL_SEARCH_MAX_SCHEMA_CHARS) {
  blocked = { name, reason: 'schema_too_large', schemaChars: chars };
  break;
}

A tool larger than the whole budget can never be activated by any search, so it never enters activeTools — which means the new .filter((name) => !activeTools.has(name)) never removes it either. If it ranks at position k for some query, everything after k is now unreachable. Once the first k−1 matches get activated and filtered out, it rises to position 1, and from then on that query space returns nothing at all. The blocked payload tells the model what happened but gives it no way around.

Before this commit that tool was merely unusable; now it takes the rest of the results with it. The budget-exhausted break is right — it preserves relevance order and the next search resumes from there — but this branch is a different fact: this tool will never fit, so skipping it and spending the remaining budget is the only useful move. Recording blocked and continuing gets both.

reports a top-ranked tool whose schema exceeds the per-search ceiling has a single tool in the index, so it passes either way. A second smaller tool behind the oversized one would pin this down.

Loose ends from the deletions (P3, take them or leave them)

  • catalogSurfaceById (packages/core/src/tool-catalog.ts:231) has no consumer left now that the policy lookup is gone.
  • ProductToolSurfacePolicy took the rest of ProductToolSurfaceIdentity with it: the interface now holds only productToolNames, which EffectiveProductToolSurface already exposes at the top level. One fact in two places, and the only reader of the nested copy is a test assertion.
  • In the run trace, newlyActivated: activated is now identical to activated by construction — the distinction is what the activeTools filter removed.
  • .filter((name) => this.searchableNames.has(name)) is always true, since the index is built from claimed only. It also sits after .slice(0, TOOL_SEARCH_MAX_LIMIT), so if it ever stopped being true it would silently shorten the result instead of filtering before the cut.

None of this blocks the merge, so I am approving. I would still like the P2 taken, with a test that puts something behind the oversized tool.

简体中文

多谢这么快,我读了 c28cb8d3。名额与预算的计账、排序的 breakonSearchProductToolSurfacePolicyavailability 都解决了,三个新测试也正好落在此前完全没有覆盖的那条分支上。其中一处修复带出了一个新问题,删除之后还留了几个小尾巴。

[P2] schema_too_large 应该 continue 而不是 break

单个体积超过整个预算的工具,任何搜索都装不下,因此它永远进不了 activeTools——也就永远不会被新加的 .filter((name) => !activeTools.has(name)) 过滤掉。假设它对某个 query 排在第 k 位,那么第 k 位之后的匹配现在全都取不到;等前 k−1 个被激活并过滤走,它升到第 1 位,此后该 query 空间将永久零产出。blocked 负载能告诉模型发生了什么,却没有给它任何绕开的办法。

这个 commit 之前,这类工具只是自己不可用;现在它会把其余结果一起拖下水。预算耗尽那条 break 是对的——它保住了相关性顺序,下一次搜索从这里接着走;但这条分支是另一件事:这个工具永远装不下,跳过它、把剩余预算用掉才是唯一有意义的动作。记录 blocked 并继续,两件事都能拿到。

reports a top-ranked tool whose schema exceeds the per-search ceiling 的索引里只有一个工具,所以两种写法都能通过。在超大工具后面再放一个小工具就能钉住这一点。

删除留下的小尾巴(P3,可取可不取)

  • catalogSurfaceByIdpackages/core/src/tool-catalog.ts:231)在 policy 查表去掉之后已无消费者。
  • ProductToolSurfacePolicy 顺带掏空了 ProductToolSurfaceIdentity:该接口现在只剩 productToolNames,而 EffectiveProductToolSurface 顶层已经暴露了同一个字段。同一事实两份,嵌套那份的唯一读者是一处测试断言。
  • run trace 里的 newlyActivated: activated 现在按构造恒等于 activated——两者的区别恰好就是 activeTools 过滤掉的那部分。
  • .filter((name) => this.searchableNames.has(name)) 恒为真,因为索引只用 claimed 构建。而且它位于 .slice(0, TOOL_SEARCH_MAX_LIMIT) 之后,万一哪天不再恒真,它会静默截短结果,而不是在截断前完成过滤。

这些都不阻塞合并,所以我 approve 了。P2 还是希望顺手拿掉,测试在超大工具后面放点东西。

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approving. My comments above are non-blocking: the P2 on schema_too_large is worth taking as a follow-up, and the P3 items are opportunistic.

@likun666661

Copy link
Copy Markdown
Member Author

Took the follow-up P2 in da7259a.

schema_too_large now records the blocked candidate and continues, so a permanently oversized match cannot starve smaller later results. schema_budget_exhausted still breaks to preserve the ranked prefix for candidates that could fit in a fresh search.

The regression test now puts a smaller match behind the oversized top result and verifies that the result reports the oversized tool while activating the smaller one.

Validated with format check, build:test, typecheck, the complete tool-availability suite, and diff check.

@likun666661
likun666661 merged commit a27d59e into main Aug 25, 2026
3 checks passed
@likun666661
likun666661 deleted the feat/tool-search-lazy-loading branch August 25, 2026 09:33
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.

feat(runtime): implement turn-scoped tool_search activation

2 participants