Skip to content

feat(runtime): provider-native Tool Search discovery policy + lowering contract (#1382 slice 1) - #1394

Closed
sunheyi6 wants to merge 6 commits into
apache:mainfrom
sunheyi6:feat/provider-native-tool-search-1382
Closed

feat(runtime): provider-native Tool Search discovery policy + lowering contract (#1382 slice 1)#1394
sunheyi6 wants to merge 6 commits into
apache:mainfrom
sunheyi6:feat/provider-native-tool-search-1382

Conversation

@sunheyi6

@sunheyi6 sunheyi6 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

What

First slice of #1382 (RFC: Provider-native Tool Search for large tool catalogs). Establishes a Maka-owned tool discovery policy on the catalog plus a provider-native lowering contract that the ModelAdapter seam (#1381 / #1390) lowers to Anthropic / OpenAI native Tool Search, with a deterministic load_tools fallback for unsupported models.

This is the contract only, not the live streamText wiring — mirrors how #1381 was sliced (establish the seam first, wire behavior in follow-ups).

Why

A session connected to several MCP servers sends dozens/hundreds of tool schemas on every provider request even when the model needs one tool. Both Anthropic and OpenAI now ship native deferred loading + Tool Search. #1382 says: reuse the protocol capability rather than build a generic search service first. This slice lands the Maka-owned policy + lowering contract that later slices consume.

Changes

  • packages/runtime/src/tool-discovery.ts (new, pure, provider-package-free):
    • ToolDiscovery policy (direct | deferred + namespace), extending the catalog model rather than duplicating it.
    • buildToolDiscoveryPolicy — derives policy from product tools + deferred catalog surfaces + MCP tools grouped per server (MCP tools default deferred — the exact pain point).
    • resolveProviderToolSearchCapability (anthropic / openai / none by adapter kind).
    • lowerToolsForProvider — the lowering contract: native modes mark deferred tools with deferLoading (OpenAI also carries namespace), exclude them from the initial activeTools, and add a tool_search descriptor kept active; none is an identity no-op so today's full-surface behavior and the existing load_tools economy are unchanged.
  • packages/runtime/src/__tests__/tool-discovery.test.ts (new) — 14 tests.
  • packages/runtime/package.json — expose the contract via the ./tool-discovery subpath export (the runtime barrel index.ts was retired upstream in refactor: remove duplicate runtime architecture #2742).

Safety / correctness (per the RFC)

  • Tool Search changes visibility, not authorization; loaded tools still cross ToolRuntime.
  • One authoritative catalog; a search result only selects catalog entries — never grants permission, never introduces an unknown schema.
  • Unsupported models keep the current deterministic behavior (no silent tool loss).
  • An unclassified tool defaults to direct (never silently hidden).

Verification

  • 14 unit tests pass (capability resolution, policy building, anthropic / openai / fallback lowering paths, catalog-authority boundaries).
  • New code adds zero type errors; the 178 pre-existing errors on the base branch are untouched.
  • biome lint / format clean.

Stacked

Rebased onto current main (c78f850). The #1381 seam is already merged upstream and has evolved there, so this diff contains only this slice — the feat(runtime): add provider-native Tool Search discovery policy + lowering contract commit plus its review fixes. History is linear; no merge commits.

Out of scope (follow-up slices)

  • Wire lowerToolsForProvider into the backend tool-assembly point; expand NativeSearchToolDescriptor into @ai-sdk/anthropic.toolSearchBm25_20251119 / @ai-sdk/openai.toolSearch.
  • Adjust the execute-boundary visibility guard so a provider-loaded (search-result) tool isn't rejected by the load_tools-driven activeNames guard.
  • Durable replay of tool-search call / output / tool_reference items for stateless continuation / process restart.
  • Telemetry (searched / loaded / called / unused tools) and provider E2E.

Refs #1382

@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.

Two P2 issues remain in addition to the inline P1:

  • [P2] resolveProviderToolSearchCapability accepts modelId but ignores it at packages/runtime/src/tool-discovery.ts:142. The openai adapter also serves Chat Completions models such as gpt-4o, while native tool search requires the Responses API on GPT-5.4 or later. Older Anthropic models have the same problem. Once this contract is wired, those models will select the native path instead of the required load_tools fallback. Capability resolution needs to include the actual model and wire support, with unsupported-model tests.
  • [P2] lowerToolsForProvider discards namespaceDescription at packages/runtime/src/tool-discovery.ts:264. The OpenAI SDK expects providerOptions.openai.namespace to contain both name and description, and groups tools using that pair. The lowering result carries only the namespace name, so the adapter cannot construct the promised namespace payload. Preserve the description and test the complete provider option shape.

The inline P1 prevents the native lowering contract from working. Please revise before merging or wiring this contract.

Comment thread packages/runtime/src/tool-discovery.ts
sunheyi6 pushed a commit to sunheyi6/maka-agent that referenced this pull request Jul 24, 2026
Address all three review issues from Astro-Han on PR apache#1394:

[P1] Keep deferred tools in activeTools.
AI SDK 7 treats activeTools as an allowlist: any tool omitted is stripped from
the tools dict entirely, so the provider adapter never receives deferred schemas
or their deferLoading flags. Fix: keep deferred entries in activeTools (unless
neverAdvertise) and rely on the provider's deferLoading option to control initial
visibility. Update the LoweredProviderToolPayload doc + the unit test that locked
in the broken shape.

[P2] resolveProviderToolSearchCapability now gates on modelId.
Previously the function accepted modelId but ignored it, returning 'anthropic' /
'openai' for all models of that adapter kind. This would route models without
native Tool Search (e.g. gpt-4o Chat Completions, older Claude 3.x) onto the native
path, causing silent tool loss. Fix: add supportsAnthropicToolSearch (Opus 4.5+ /
Sonnet 4.5+) and supportsOpenaiToolSearch (GPT-5.4+ Responses API) model-id checks.
Unsupported models fall back to 'none' (the deterministic load_tools economy).
Add tests for supported and unsupported models on both providers.

[P2] Preserve namespaceDescription in LoweredToolEntry.
The OpenAI SDK expects providerOptions.openai.namespace to carry both name and
description for namespace grouping. The lowering result previously carried only
the namespace name, so the adapter could not construct the full payload. Fix: add
namespaceDescription to LoweredToolEntry and populate it during lowering.
Add a test asserting the complete provider option shape.

Tests: 17 pass (14 original updated + 3 new). biome lint/format clean.

Refs apache#1382

@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.

The earlier activeTools and namespace issues are fixed. I still found two P1 issues, left inline, plus two lower-priority gaps:

[P2] supportsAnthropicToolSearch only recognizes Opus and Sonnet. Anthropic's current compatibility table also includes claude-haiku-4-5 and claude-fable-5, and both models already exist in this repository's catalog. They currently resolve to none, so supported models never use this path. Please derive this from a declared capability table and cover the supported IDs instead of relying on the current family regex.

[P2] buildToolDiscoveryPolicy says the first surface claim wins, but each later surface overwrites an existing deferred entry. Putting the same bound tool in surfaces A and B produces B, unlike ToolAvailabilityRuntime, which skips an already claimed tool. Please either reject overlapping surface membership as a catalog invariant or preserve the first deferred claim, with a regression test.

The 17 tests pass, but they only verify the intermediate objects returned by this module. They do not exercise the existing availability plan, a real resolved provider/wire, or either SDK request serializer. Provider-boundary request tests can land with the wiring slice, but the contract-level P1s need to be fixed first.

Because the inline findings affect fallback correctness and capability selection, I am leaving comments rather than approving.

Anthropic compatibility reference:
https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool

Comment thread packages/runtime/src/tool-discovery.ts
Comment thread packages/runtime/src/tool-discovery.ts
@Astro-Han

Copy link
Copy Markdown
Contributor

Hi — this PR conflicts with current main and cannot be merged as-is.

I tested a rebase onto current main locally (in a throwaway worktree — your branch was not touched). It stops on these files:

  • packages/cli/src/runtime-bootstrap.ts
  • packages/runtime/src/__tests__/active-full-compact.test.ts
  • packages/runtime/src/__tests__/active-tool-result-prune.test.ts
  • …(more)

These are real source conflicts, so they need your judgement rather than a mechanical rebase — please rebase onto current main and resolve them yourself, then push. Once the branch is conflict-free and CI is green on the new head, I will pick it up for review.

git fetch upstream && git rebase upstream/main
# resolve, then
git push --force-with-lease

Thanks for the contribution — happy to help if any conflict is unclear.


AI-assisted maintenance note, not a review. It does not count as the required human review under CONTRIBUTING.md §Review.

sunheyi6 and others added 5 commits August 26, 2026 09:22
…ering contract (apache#1382 slice 1)

First increment of apache#1382: establish a Maka-owned tool
discovery policy on the catalog plus a provider-native lowering contract that
the ModelAdapter (apache#1381 seam) lowers to Anthropic / OpenAI native Tool Search,
with a deterministic `load_tools` fallback for unsupported models.

Add packages/runtime/src/tool-discovery.ts as a pure, provider-package-free
module owning:
- `ToolDiscovery` policy (`direct` | `deferred` + namespace), extending the
  catalog model rather than duplicating it;
- `buildToolDiscoveryPolicy`, deriving policy from product tools + deferred
  catalog surfaces + MCP tools grouped per server (MCP tools default deferred —
  the exact pain point apache#1382 targets: dozens/hundreds of MCP schemas sent every
  request even when the model needs one tool);
- `resolveProviderToolSearchCapability` (anthropic / openai / none by adapter
  kind);
- `lowerToolsForProvider`, the lowering contract: native modes mark deferred
  tools with `deferLoading` (OpenAI also carries `namespace`), exclude them from
  the initial `activeTools`, and add a `tool_search` descriptor kept active;
  `none` is an identity no-op so today's full-surface behavior and the existing
  `load_tools` economy are unchanged.

This is the contract only, not the live `streamText` wiring. Per the RFC:
- Tool Search changes visibility, not authorization; loaded tools still cross
  `ToolRuntime`.
- One authoritative catalog; a search result only selects catalog entries.
- Unsupported models keep the current deterministic behavior (no silent tool
  loss).
- An unclassified tool defaults to `direct` (never silently hidden).

Rebased onto current main: the apache#1381 seam commit was dropped (merged upstream,
evolved); the runtime barrel `index.ts` was retired upstream (apache#2742), so the
contract is exposed via a `./tool-discovery` subpath export in package.json.
Address all three review issues from Astro-Han on PR apache#1394:

[P1] Keep deferred tools in activeTools.
AI SDK 7 treats activeTools as an allowlist: any tool omitted is stripped from
the tools dict entirely, so the provider adapter never receives deferred schemas
or their deferLoading flags. Fix: keep deferred entries in activeTools (unless
neverAdvertise) and rely on the provider's deferLoading option to control initial
visibility. Update the LoweredProviderToolPayload doc + the unit test that locked
in the broken shape.

[P2] resolveProviderToolSearchCapability now gates on modelId.
Previously the function accepted modelId but ignored it, returning 'anthropic' /
'openai' for all models of that adapter kind. This would route models without
native Tool Search (e.g. gpt-4o Chat Completions, older Claude 3.x) onto the native
path, causing silent tool loss. Fix: add supportsAnthropicToolSearch (Opus 4.5+ /
Sonnet 4.5+) and supportsOpenaiToolSearch (GPT-5.4+ Responses API) model-id checks.
Unsupported models fall back to 'none' (the deterministic load_tools economy).
Add tests for supported and unsupported models on both providers.

[P2] Preserve namespaceDescription in LoweredToolEntry.
The OpenAI SDK expects providerOptions.openai.namespace to carry both name and
description for namespace grouping. The lowering result previously carried only
the namespace name, so the adapter could not construct the full payload. Fix: add
namespaceDescription to LoweredToolEntry and populate it during lowering.
Add a test asserting the complete provider option shape.

Tests: 17 pass (14 original updated + 3 new). biome lint/format clean.

Refs apache#1382
@sunheyi6
sunheyi6 force-pushed the feat/provider-native-tool-search-1382 branch from 100a5cb to 5fe86c5 Compare August 26, 2026 01:40
@sunheyi6

Copy link
Copy Markdown
Contributor Author

@Astro-Han I would like to confirm the intended scope before expanding this PR further.

While reviewing the provider-native Tool Search contract, I found an important distinction:

  • Native defer_loading reduces the tool schemas initially visible to the model, so it can reduce model-context/token pressure.
  • However, the deferred tool definitions still cross the provider API boundary in each request so that the provider can search them. It does not, by itself, reduce the client-to-provider request payload containing hundreds of MCP schemas.

To literally avoid transmitting all MCP tool definitions on every request, the likely follow-up is a provider-independent local discovery path:

  1. Keep the complete MCP catalog and schemas inside Maka.
  2. Group MCP tools by authoritative server metadata and make those groups searchable through ToolAvailabilityRuntime.
  3. Initially send only direct tools plus the local tool_search / load_tools connector.
  4. After a local search selects a bounded set of tools, include only those selected schemas in the next provider request.
  5. Preserve the existing catalog authority, permission checks, schema-size limits, and execution-boundary gating.

This is a different optimization from provider-native Tool Search: it reduces request bytes as well as model-visible schema volume, while native deferral primarily reduces model-visible context.

Would you prefer this local MCP discovery / request-payload reduction work to be implemented as a separate follow-up PR under #1382, keeping #1394 as the provider-native contract slice? Or should part of that behavior be included in this PR before it is reviewed?


中文

@Astro-Han 想在继续扩大这个 PR 的范围之前确认一下方向。

这里有一个重要区别:

  • Provider 原生的 defer_loading 可以减少模型一开始看到的工具定义,从而降低上下文和 token 压力。
  • 但是这些延迟工具的完整定义仍然会随每次 API 请求发送给 Provider,供 Provider 搜索。因此,它本身不能减少“每次请求传输几百个 MCP schema”的请求体大小。

如果要真正避免每次传输全部 MCP 工具定义,后续可能需要实现与 Provider 无关的本地发现流程:

  1. 完整 MCP catalog 和 schema 保留在 Maka 本地。
  2. 根据可信的 MCP Server 元数据分组,并接入 ToolAvailabilityRuntime
  3. 第一次只发送常用工具和本地 tool_search / load_tools
  4. 本地搜索选中少量工具后,下一次请求只加入这些工具的 schema。
  5. 继续保留 catalog 权威、权限检查、schema 大小限制和执行边界保护。

这个方案与 Provider 原生 Tool Search 是两种不同的优化:本地发现同时减少请求字节和模型可见 schema;原生延迟加载主要减少模型可见上下文。

请问这部分“本地 MCP 工具发现 / 减少请求体”的工作,是否应该在 #1382 下单独新建一个后续 PR,让 #1394 继续只负责 Provider 原生契约?还是希望在正式 review #1394 之前,把其中一部分实现也放进当前 PR?

@sunheyi6

Copy link
Copy Markdown
Contributor Author

Closing this PR as superseded by the provider-independent design agreed in Discussion #3621 and implemented by merged PR #3765.

The project now has one Maka-owned, turn-scoped tool_search contract: deferred schemas stay off the initial provider request, bounded search results become visible on the next provider step, and activation is cleared at turn completion. Provider-native Tool Search, if added later, should remain an optimization behind that contract rather than introduce a second semantic path.

Thank you for the review and design feedback.


中文

关闭此 PR,因为 Discussion #3621 已经达成了新的 Provider 无关方案,并由已合并的 PR #3765 完成实现。

项目现在采用一个由 Maka 管理、仅在当前 turn 生效的 tool_search 契约:延迟工具的完整 schema 不进入首次 Provider 请求;搜索得到的有限结果在下一步变为可见;turn 完成后清除激活状态。以后如果加入 Provider 原生 Tool Search,也应作为这一统一契约背后的优化,而不是再引入第二套语义。

感谢大家的 review 和设计反馈。

@sunheyi6 sunheyi6 closed this Aug 26, 2026
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.

2 participants