Skip to content

Python: make FoundryToolbox.as_skills_provider() disable_caching effective#7135

Draft
giles17 wants to merge 2 commits into
microsoft:mainfrom
giles17:fix-toolbox-skills-caching
Draft

Python: make FoundryToolbox.as_skills_provider() disable_caching effective#7135
giles17 wants to merge 2 commits into
microsoft:mainfrom
giles17:fix-toolbox-skills-caching

Conversation

@giles17

@giles17 giles17 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Motivation & Context

FoundryToolbox.as_skills_provider() accepts a disable_caching parameter, but it had no effect: the toolbox's skill discovery was re-read from skill://index.json over MCP on every agent run, adding an avoidable round-trip per request for hosted agents.

The cause is that as_skills_provider() forwarded disable_caching to SkillsProvider, which deliberately does not auto-wrap a caller-supplied SkillsSource in caching (to avoid leaking skills across agents/tenants for context-aware sources). So the flag was silently ignored, and skills were never cached. The docstring also claimed "caching after the first discovery", which was inaccurate.

Surfaced during review of #7099.

Description & Review Guide

  • What are the major changes?
    • as_skills_provider() now composes caching itself, wrapping the source in DeduplicatingSkillsSource(CachingSkillsSource(...)) when caching is enabled, so disable_caching has a real, observable effect.
    • Stopped forwarding disable_caching to SkillsProvider (it was a no-op there for a caller-supplied source).
    • Added a cache_refresh_interval: timedelta | None parameter, threaded into CachingSkillsSource, so callers can re-query when a toolbox's attached skills change over the process lifetime.
    • Rewrote the docstring to describe the real caching behavior.
    • Added unit tests covering cached (index read once), disabled (read every run), and refresh-interval behavior.
  • What is the impact of these changes?
    • By default, toolbox skill discovery is now cached (skill://index.json read once instead of per run). This is safe because _FoundryToolboxSkillsSource is context-independent: its get_skills ignores the SkillsSourceContext and returns the same toolbox skills for every caller. No public API is removed; disable_caching keeps working and now actually does something.
  • What do you want reviewers to focus on?
    • The safety argument for composing caching here (context-independence of the source).

Related Issue

Fixes #7100

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

…ctive

as_skills_provider() forwarded disable_caching to SkillsProvider, which
ignores it for a caller-supplied SkillsSource, so it was a no-op and the
toolbox re-read skill://index.json on every agent run.

Compose caching in as_skills_provider() instead: wrap the context-independent
_FoundryToolboxSkillsSource in DeduplicatingSkillsSource(CachingSkillsSource(...)).
Add a cache_refresh_interval param, fix the docstring, and add tests covering
cached, disabled, and refresh-interval behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 84150ec4-6f7c-4ef8-b9fb-12fa11652773
Copilot AI review requested due to automatic review settings July 15, 2026 21:35
@giles17 giles17 added the python Usage: [Issues, PRs], Target: Python label Jul 15, 2026

Copilot AI 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.

Pull request overview

This PR fixes FoundryToolbox.as_skills_provider() in the Python agent-framework-foundry-hosting package so the existing disable_caching flag actually affects toolbox skill discovery, avoiding an extra skill://index.json MCP round-trip on every agent run for hosted agents. It does this by composing caching at the toolbox layer (since SkillsProvider intentionally does not auto-cache caller-supplied SkillsSources) and adds a refresh interval option plus targeted unit tests.

Changes:

  • Compose DeduplicatingSkillsSource(CachingSkillsSource(...)) inside FoundryToolbox.as_skills_provider() when caching is enabled, and stop forwarding disable_caching to SkillsProvider (it’s a no-op for caller-supplied sources).
  • Add cache_refresh_interval: timedelta | None to allow periodic re-discovery when toolbox-attached skills change over process lifetime.
  • Add unit tests validating default caching (read once), disabled caching (read every run), and refresh-interval behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
python/packages/foundry_hosting/agent_framework_foundry_hosting/_toolbox.py Implements effective discovery caching by wrapping the toolbox skills source in CachingSkillsSource (plus dedup), adds cache_refresh_interval, and updates the docstring to match real behavior.
python/packages/foundry_hosting/tests/test_toolbox.py Adds tests that stub MCPSkillsSource to count discovery calls and verify cached/uncached/refresh behaviors.

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/foundry_hosting/agent_framework_foundry_hosting
   _toolbox.py650100% 
TOTAL44461523688% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
8986 33 💤 0 ❌ 0 🔥 2m 27s ⏱️

@github-actions

Copy link
Copy Markdown
Contributor

Flagged issue

FoundryToolbox.as_skills_provider() now wraps _FoundryToolboxSkillsSource in a default shared CachingSkillsSource, but _ToolboxAuth injects get_request_context().platform_headers() on every outbound MCP request and documents that the forwarded per-request call ID lets the Foundry MCP proxy 'resolve the caller context server-side' (python/packages/foundry_hosting/agent_framework_foundry_hosting/_toolbox.py:76-80, :106-110). Core CachingSkillsSource uses one shared bucket unless given an isolation selector (python/packages/core/agent_framework/_skills.py:3775-3782, :3850-3856), so once one hosted request populates the cache, later callers silently skip re-discovery and can see the wrong skill set. The existing provider contract explicitly avoids auto-caching caller-suplied sources for exactly this cross-context leak risk (python/packages/core/agent_framework/_skills.py:2047-2054, python/packages/core/tests/core/test_skills.py:603-6052).


Source: automated DevFlow PR review

@github-actions github-actions Bot 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.

Automated Code Review

Reviewers: 5 | Confidence: 91%

✓ Correctness

The PR correctly fixes the ineffective disable_caching parameter by composing caching wrappers directly in as_skills_provider(). The API usage of CachingSkillsSource and DeduplicatingSkillsSource matches their actual constructors. Removing disable_caching from the SkillsProvider() call is correct since SkillsProvider explicitly does not auto-cache caller-supplied sources. The safety argument (context-independence of _FoundryToolboxSkillsSource) is valid — its get_skills ignores SkillsSourceContext and always returns the same toolbox skills. No correctness issues found.

✓ Security Reliability

This PR correctly fixes the ineffective disable_caching parameter by composing CachingSkillsSource and DeduplicatingSkillsSource directly in as_skills_provider(). The security argument is sound: _FoundryToolboxSkillsSource.get_skills() ignores the SkillsSourceContext (only reads self._toolbox.session), so a shared unkeyed cache cannot leak skills across agents/tenants. The concurrency model in CachingSkillsSource (per-key asyncio locks with double-check) is correct. No injection risks, resource leaks, or unhandled failure modes introduced.

✓ Test Coverage

The PR adds three well-targeted unit tests covering the three key behaviors: default caching (index read once), disabled caching (re-read every call), and cache_refresh_interval threading (zero interval causes re-reads). Tests correctly monkeypatch MCPSkillsSource with a counting stub to verify composition logic without network I/O. The test helper structure is clean and reusable. Coverage is adequate for the scope of the change — the core CachingSkillsSource and DeduplicatingSkillsSource classes have their own independent test suites, so the toolbox tests rightly focus on verifying correct composition/wiring rather than re-testing caching semantics.

✓ Failure Modes

The change correctly composes caching at the call site where it has effect, rather than relying on SkillsProvider which deliberately skips auto-caching for caller-supplied sources. Exception propagation through CachingSkillsSource and DeduplicatingSkillsSource is clean (no swallowed errors). The removed disable_caching argument to SkillsProvider was verified as a no-op for caller-supplied SkillsSource instances. No silent failure modes, lost errors, or operational hazards introduced.

✗ Design Approach

I found one blocking design issue: the new default cache is shared across all callers even though toolbox skill discovery still depends on Foundry’s per-request hosting context, so the first caller’s skill://index.json can be replayed to later callers instead of being re-evaluated for them.

Flagged Issues

  • FoundryToolbox.as_skills_provider() now wraps _FoundryToolboxSkillsSource in a default shared CachingSkillsSource, but _ToolboxAuth injects get_request_context().platform_headers() on every outbound MCP request and documents that the forwarded per-request call ID lets the Foundry MCP proxy 'resolve the caller context server-side' (python/packages/foundry_hosting/agent_framework_foundry_hosting/_toolbox.py:76-80, :106-110). Core CachingSkillsSource uses one shared bucket unless given an isolation selector (python/packages/core/agent_framework/_skills.py:3775-3782, :3850-3856), so once one hosted request populates the cache, later callers silently skip re-discovery and can see the wrong skill set. The existing provider contract explicitly avoids auto-caching caller-suplied sources for exactly this cross-context leak risk (python/packages/core/agent_framework/_skills.py:2047-2054, python/packages/core/tests/core/test_skills.py:603-6052).

Automated review by giles17's agents

Emphasize that the toolbox advertises the same skill set to every caller (the
per-request call-id governs execution/authorization, not which skills are
listed) rather than leaning on 'ignores SkillsSourceContext'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 84150ec4-6f7c-4ef8-b9fb-12fa11652773
@giles17
giles17 marked this pull request as ready for review July 15, 2026 22:12

@github-actions github-actions Bot 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.

Automated Code Review

Reviewers: 5 | Confidence: 93%

✓ Correctness

The PR correctly composes caching at the as_skills_provider() level by wrapping the source in DeduplicatingSkillsSource(CachingSkillsSource(...)) when caching is enabled. The constructor signatures of CachingSkillsSource (accepts refresh_interval: timedelta | None) and DeduplicatingSkillsSource (accepts inner_source: SkillsSource) match the PR's usage. Omitting disable_caching from the SkillsProvider call is safe since it defaults to False and is a no-op for caller-supplied sources anyway. No correctness issues found.

✓ Security Reliability

The PR correctly fixes the ineffective disable_caching parameter by composing caching directly in as_skills_provider(). The implementation is sound: CachingSkillsSource properly handles refresh_interval with monotonic timestamps and per-key async locks, DeduplicatingSkillsSource wraps it correctly, and the safety argument for shared caching (context-independence of _FoundryToolboxSkillsSource) is valid. The removal of disable_caching from the SkillsProvider call is correct since it was always a no-op for caller-supplied sources. No security or reliability issues identified.

✓ Test Coverage

The test coverage for this PR is good. Three new tests cover the main behavioral scenarios: default caching (index read once), disabled caching (read every call), and cache_refresh_interval wiring (zero interval forces re-reads). Assertions are meaningful (exact invocation counts), and the monkeypatch approach appropriately isolates the unit under test. The tests verify the wiring of as_skills_provider() without redundantly re-testing CachingSkillsSource internals.

✓ Failure Modes

The PR correctly composes caching at the call site where it can actually take effect, rather than forwarding a no-op parameter to SkillsProvider. The failure-mode surface is minimal: CachingSkillsSource handles refresh_interval=None (never-expire) correctly, the session-None check in _FoundryToolboxSkillsSource is preserved, and the removed disable_caching kwarg to SkillsProvider was always inert for caller-supplied sources. No silent failures, lost errors, or operational failure paths introduced.

✓ Design Approach

The new caching layer makes disable_caching observable, but it also caches MCPSkill objects that are bound to a specific MCP session. Because the MCP tool can reconnect by replacing tool.session, later runs may reuse cached skills that still point at the old closed session, breaking load_skill/read_skill_resource after reconnects.


Automated review by giles17's agents

# so we compose the caching ourselves.
source: SkillsSource = _FoundryToolboxSkillsSource(self)
if not disable_caching:
source = DeduplicatingSkillsSource(CachingSkillsSource(source, refresh_interval=cache_refresh_interval))

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.

This caches concrete MCPSkill objects that capture the MCP session at construction time and use that stored client for get_content/get_resource. The MCP tool supports reconnect by replacing tool.session with a new object, but after such a reconnect this cache will still hand out skills bound to the old closed session, causing load_skill or read_skill_resource to fail. Consider storing reconnect-safe discovery data instead, or rebuilding session-bound MCPSkill wrappers from the current toolbox session on each cache hit.

@giles17
giles17 marked this pull request as draft July 17, 2026 15:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: FoundryToolbox.as_skills_provider() disable_caching is a no-op; toolbox skills re-read every run

2 participants