Skip to content

Add deferred tool loading and hosted tool search (OpenAI + Anthropic)#697

Draft
behzadsp wants to merge 3 commits into
laravel:0.xfrom
behzadsp:feature/deferred-tool-loading
Draft

Add deferred tool loading and hosted tool search (OpenAI + Anthropic)#697
behzadsp wants to merge 3 commits into
laravel:0.xfrom
behzadsp:feature/deferred-tool-loading

Conversation

@behzadsp

Copy link
Copy Markdown

Summary

Adds opt-in deferred tool loading backed by each provider's hosted tool search, so an agent's tool definitions can be loaded on demand instead of shipped in every request. This reduces input tokens and improves tool-selection accuracy for agents with many tools, while staying fully backward compatible.

The defer flag is identical across providers (defer_loading: true); only the search-tool entry differs. That seam is where the abstraction lives: a provider-neutral opt-in on the tool/agent, per-gateway emission of the search entry.

API

use Laravel\Ai\Attributes\Deferred;
use Laravel\Ai\Attributes\ToolSearch;

#[Deferred] // load this tool's definition on demand
class SearchInvoices implements Tool { /* ... */ }

#[ToolSearch]                    // enable hosted search; regex (default)
#[ToolSearch(strategy: 'bm25')]  // or bm25 (Anthropic only; OpenAI ignores it)
class SupportAgent implements Agent, HasTools { /* ... */ }
  • #[Deferred] marks a tool for on-demand loading (mirrors the existing #[Strict] attribute).
  • #[ToolSearch] on the agent enables the hosted search entry; its strategy selects Anthropic's regex/bm25 variant.
  • A new Contracts\Providers\SupportsToolSearch gates by provider + model.

Behavior

Provider Search entry Models
OpenAI {"type":"tool_search"} gpt-5.4+
Anthropic tool_search_tool_{regex,bm25}_20251119 (+ name) Sonnet/Opus 4.0+, Haiku 4.5+
  • Backward compatible: with no #[ToolSearch] / #[Deferred], request output is byte-identical to today.
  • Graceful degradation: unsupported provider or model silently emits tools as before (no search entry, no defer_loading).
  • Anthropic guard: the API returns 400 if all tools are deferred; the gateway throws a clear LogicException instead, and never defers the search entry itself.
  • Response parsing is untouched — tool_reference expansion is handled server-side by both providers.

Tests

  • Unit: #[Deferred] resolution, #[ToolSearch] strategy resolution, OpenAI + Anthropic tool mapping (defer flag, search entry, silent skip, all-deferred guard), provider model gating.
  • Feature (HTTP-faked, end-to-end): an agent with #[ToolSearch] + a #[Deferred] tool emits the correct request body on both providers.
  • Full suite green, PHPStan clean, Pint clean.

Out of scope

  • Client-side / custom tool search (embeddings, self-run BM25) — supported via tool_reference results; separate PR.
  • MCP mcp_toolset deferred loading.
  • Gemini / Groq / Mistral — no upstream API yet.

References

…opic

Lets agents defer tool definitions so supporting providers load them on
demand via hosted tool search, reducing input tokens and improving
tool-selection accuracy for agents with many tools.

- #[Deferred] marks a tool for on-demand loading
- #[ToolSearch(strategy:)] enables the hosted search entry on an agent
  (strategy selects Anthropic's regex or bm25 variant; OpenAI ignores it)
- SupportsToolSearch gates by provider and model (gpt-5.4+; Sonnet/Opus
  4.0+, Haiku 4.5+); unsupported providers and models silently emit tools
  as before
- Anthropic guards against an all-deferred toolset, which the provider
  rejects with a 400

Backward compatible: with no opt-in, request output is unchanged.
@github-actions

Copy link
Copy Markdown

Thanks for submitting a PR!

Note that draft PRs are not reviewed. If you would like a review, please mark your pull request as ready for review in the GitHub user interface.

Pull requests that are abandoned in draft may be closed due to inactivity.

@behzadsp behzadsp marked this pull request as ready for review June 10, 2026 09:34
@pushpak1300 pushpak1300 marked this pull request as draft June 15, 2026 17:17
@pushpak1300 pushpak1300 marked this pull request as ready for review June 15, 2026 17:19
@pushpak1300

Copy link
Copy Markdown
Member

Reworked the API around a ToolSearch provider tool

Instead of marking each tool with #[Deferred] and auto-enabling search, deferral is now expressed by wrapping the deferred tools in a ToolSearch provider tool, consistent with the WebSearch / WebFetch / FileSearch family.

Before:

#[Deferred]
class ArchiveCustomer implements Tool { /* ... */ }

public function tools(): iterable
{
    return [new GetWeather, new ArchiveCustomer]; // search auto-enabled
}

After:

public function tools(): iterable
{
    return [
        new GetWeather,                               // eager
        new ToolSearch(tools: [new ArchiveCustomer]), // deferred, discovered via search
    ];
}

Why the change

  • Consistency. Tool search is a server tool, so it should be a ProviderTool you configure at the call site like the others, not an attribute plus implicit activation.
  • Explicit grouping. The deferred set lives in one place instead of being scattered as attributes across N tool classes.
  • Per-agent flexibility. The same Tool can be deferred in one agent and eager in another. The attribute baked that decision into the class globally.
  • Vendor-neutral strategy. Anthropic's regex/bm25 is provider-specific, so it goes through withProviderOptions(Lab::Anthropic, ['strategy' => 'bm25']), following the convention from Migrate OpenAI web_search_preview to web_search with provider options #662. Default is regex; OpenAI ignores it.

Verified end-to-end against the live OpenAI API (the model searches, discovers the deferred tool, and calls it).

@pushpak1300 pushpak1300 requested a review from taylorotwell June 16, 2026 12:40
@taylorotwell

Copy link
Copy Markdown
Member

My agent's review.

Blocking

  • src/Gateway/OpenAi/Concerns/ParsesTextResponses.php:114 - OpenAI stateless tool loops drop hosted tool_search_call / tool_search_output items because only function calls are stored in AssistantMessage, so store=false follow-up requests cannot replay the full response output required for deferred-tool discovery.

Should fix

  • src/Gateway/OpenAi/Concerns/MapsTools.php:30 - AzureOpenAiGateway uses this shared trait, so ToolSearch emits OpenAI tool_search / defer_loading payloads for Azure even though the PR only documents OpenAI and Anthropic support and has no Azure support contract or tests.
  • src/Gateway/OpenAi/Concerns/BuildsTextRequests.php:30 - OpenAI mapping has no model guard, so ToolSearch on models older than gpt-5.4 sends an API-invalid tool_search request instead of rejecting or degrading as described.

Consider

  • tests/Feature/Providers/OpenAi/ToolSearchTest.php:17 - Add coverage for store=false and unsupported/Azure models so the deferred-tool loop and support boundaries are exercised.

@taylorotwell taylorotwell marked this pull request as draft June 21, 2026 15:43
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.

3 participants