Uh oh!
There was an error while loading. Please reload this page.
Python: refactor FoundryHostedAgentHistoryProvider onto Foundry SDK - #5637
Conversation
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 82%
✓ Correctness
The new _shared.py module extracts and consolidates conversion helpers from _responses.py. The code is structurally sound and consistent with the existing implementation. The _collect_unknown_keys / _inject_extras pair is symmetric, ensuring correct round-trip semantics. The test coverage is thorough, including end-to-end round-trip tests through InMemoryResponseProvider. No blocking correctness issues were found.
✓ Security Reliability
No actionable issues found in this dimension.
✓ Test Coverage
No actionable issues found in this dimension.
✗ Design Approach
The consolidation into
_shared.pyis directionally good, but two design issues in the new persistence path would corrupt history semantics. First,mcp_approval_responseis reconstructed with the response record id instead of the approval request id, which breaks the framework’s own approval-response contract and can cause replayed history to target the wrong request. Second, the text-only fallback now concatenates adjacent text fragments without separators, so persisted messages can come back with different user-visible text than the originalMessagecarried.
Flagged Issues
- MCP approval responses are loaded with
Content.id = mcp_resp.id, but the framework and OpenAI adapter both treatContent.idas theapproval_request_id(_types.py:1289-1292,_chat_client.py:1645-1649). Replaying history can therefore approve the wrong MCP request.
Automated review by eavanvalkenburg's agents
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
93dc145 to
d920d13Compare1a4caf6 to
25692a1Compare42a8849 to
a58ad96Compare… azure.ai.agentserver SDK
Rebuilds the Foundry hosted-agent history provider on top of
``azure.ai.agentserver``'s ``FoundryStorageProvider`` instead of the
in-house ``_HttpStorageBackend``. Splits the monolithic ``_responses.py``
into focused modules:
- ``_history_provider.py`` — new ``FoundryHostedAgentHistoryProvider``
that talks to the SDK's ``FoundryStorageProvider``, threads
``response_id`` / ``previous_response_id`` through ``ContextVar``s via
``bind_request_context``, and lifts host-bound isolation keys
(``x-agent-{user,chat}-isolation-key``) from the optional
``agent_framework_hosting`` package into a provider-local
``IsolationContext`` so the storage layer carries the correct
partition keys without channels having to know about them.
- ``_shared.py`` — extracts all SDK ``Item`` / ``OutputItem`` ↔
framework ``Message`` conversion helpers into one place so both
``_responses.py`` and the new history provider can share them.
Restores ``_convert_file_data`` for inline ``input_file`` payloads,
and the hosted-MCP routing for ``custom_tool_call_output`` items
whose ``call_id`` carries the ``mcp_*`` prefix.
- ``_ids.py`` — shared id helpers.
- ``_responses.py`` — shrinks ~700 lines, re-exports converters for
back-compat with existing tests.
- ``tests/test_history_provider.py`` — exercises the new provider
against a fake SDK backend; the host-isolation test is gated on the
optional ``agent_framework_hosting`` import.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>Adds an optional `local_storage_root: str | Path | None` parameter to
`FoundryHostedAgentHistoryProvider`. When set and the provider is
running outside a Foundry Hosted Agent container, conversations are
persisted to JSONL files via `agent_framework.FileHistoryProvider`
laid out as:
{root}/{user_key or '~none'}/{chat_key or '~none'}/{session_id}.jsonl
Hosted mode (FOUNDRY_HOSTING_ENVIRONMENT set) ignores the option with a
one-time INFO log so Foundry storage always wins on the platform. The
in-memory fallback is unchanged when the option is omitted.
Path safety: isolation segments are validated against the same character
allowlist FileHistoryProvider uses for session-id stems and
base64-url-encoded with a reserved "~iso-" prefix when unsafe. "~none"
sentinel for missing keys can never collide with a real isolation key
(real keys starting with "~" are encoded). The resolved target dir is
also re-checked to be inside the configured root.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>- _shared.py:_capture_raw narrows `except Exception` to `except TypeError` and emits a WARNING with traceback so the lossy fallback to a synthesized round-trip is observable. Mirrors the reviewer suggestion. - _history_provider.py:save_messages narrows `except Exception` to `except FoundryStorageError` so only storage-validation failures (4xx/5xx, opaque server errors) are swallowed. Network / TLS / auth / payload-builder bugs propagate so the caller can retry / alert. Adds an instance-level `failed_writes` counter operators can poll for silent-drop visibility. - _history_provider.py id-stamping loop: drops the `contextlib.suppress(AttributeError, TypeError)` around `item.id = new_id` so SDK contract changes surface in the test suite instead of silently corrupting the chain (the storage backend rejects the entire `create_response` with HTTP 500 when synthetic prefix-based ids leak through). `import contextlib` removed. - tests: * Unit-cover `foundry_response_id` / `foundry_response_id_factory` / `foundry_item_id` so SDK `IdGenerator` contract changes are caught locally. * Cover the `save_messages` wire payload: required-by-storage fields (`background`, `parallel_tool_calls`, `instructions`, `agent_reference`), env-var-driven stamping (`FOUNDRY_AGENT_NAME` / `FOUNDRY_AGENT_VERSION` / `FOUNDRY_AGENT_SESSION_ID` / `MODEL_DEPLOYMENT_NAME` with `AZURE_AI_MODEL_DEPLOYMENT_NAME` fallback), and the rule that `model` / `agent_session_id` / `agent_reference.version` are omitted (not stamped to `None`) when their env vars are unset. * Cover the `FOUNDRY_AGENT_SESSION_ID` last-resort chain anchor on both the get and save paths, including the prefix gate that blocks non-`caresp_*`/`resp_*` values from reaching storage, and the precedence rule that a host binding wins over the env. * Replace the old `test_save_messages_swallows_backend_errors` with two tests asserting the new contract: storage errors are swallowed and bump `failed_writes`; everything else propagates and leaves the counter at zero. 141 unit tests pass; mypy + pyright + ruff clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Hosted detection now delegates to AgentConfig.from_env().is_hosted so a future Foundry SDK rename of FOUNDRY_HOSTING_ENVIRONMENT propagates automatically; drop the local _ENV_FOUNDRY_HOSTING_ENVIRONMENT constant. - Drop the FOUNDRY_AGENT_SESSION_ID fallback in both get_messages and save_messages: per the SDK it identifies the *container instance*, not the conversation, so chaining off it would silently merge unrelated conversations across container restarts. The host-bound previous_response_id (set by ResponsesChannel) is the only authoritative anchor; the env value is still stamped into the persisted envelope's agent_session_id for operator correlation. - Update module docstring + replace TestFoundryAgentSessionIdAnchor with assertions for the new contract (env var ignored as anchor, still stamped onto persisted envelope, host binding wins). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…, microsoft#5666) Brings the FoundryHostedAgentHistoryProvider refactor branch back into sync with the foundry_hosting changes that have landed on upstream main since PR-1 was opened: * microsoft#5851 (path traversal in checkpoint storage, CWE-22). The workflow-host code in ``_responses.py`` builds a ``FileCheckpointStorage`` from a caller-controlled ``context_id`` (``previous_response_id`` / ``conversation_id`` / ``response_id``). Switch both call sites to route through ``_checkpoint_storage_for_context``, which rejects separators, NUL bytes, drive letters, absolute paths, and all-dot segments, and enforces ``is_relative_to(root)`` before any directory is created. * microsoft#5666 (function approval flow). Make the SDK-Item → AF-Message conversion helpers in ``_shared.py`` async and accept an optional ``approval_storage`` keyword: - ``_items_to_messages`` / ``_item_to_message`` / ``_item_to_message_inner`` - ``_output_items_to_messages`` / ``_output_item_to_message`` / ``_output_item_to_message_inner`` For ``mcp_approval_request`` / ``mcp_approval_response`` items the helpers now load the original function-call Content from the approval storage (via ``ApprovalStorage.load_approval_request``) instead of synthesising a placeholder. This matches upstream semantics and lets approval round-trips reconstruct the real payload. The ``ApprovalStorage`` Protocol moves to ``_shared.py`` so the conversion helpers can reference it without pulling in ``_responses.py`` (which would create a circular import). The concrete ``InMemoryFunctionApprovalStorage`` and ``FileBasedFunctionApprovalStorage`` stay in ``_responses.py`` next to the host that owns them, and re-export ``ApprovalStorage`` from ``_shared`` for compatibility. The workflow-host streaming path passes its own ``self._approval_storage`` into ``_to_outputs`` so approval requests are saved at emit time. * Bump ``_history_provider.FoundryHostedAgentHistoryProvider.get_messages`` to ``await`` the now-async ``_output_items_to_messages`` call. No public API change beyond the new keyword-only ``approval_storage`` parameter on the four conversion entry points. Validation: - uv run poe check-packages -P foundry_hosting (lint + pyright clean) - uv run poe mypy -P foundry_hosting (clean) - uv run poe test -P foundry_hosting (183 passed, 1 skipped) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
a58ad96 to
35b310eCompare4c317eb
into
microsoft:feature/python-hostingUh oh!
There was an error while loading. Please reload this page.
…5637) * refactor(foundry_hosting): build FoundryHostedAgentHistoryProvider on azure.ai.agentserver SDK Rebuilds the Foundry hosted-agent history provider on top of ``azure.ai.agentserver``'s ``FoundryStorageProvider`` instead of the in-house ``_HttpStorageBackend``. Splits the monolithic ``_responses.py`` into focused modules: - ``_history_provider.py`` — new ``FoundryHostedAgentHistoryProvider`` that talks to the SDK's ``FoundryStorageProvider``, threads ``response_id`` / ``previous_response_id`` through ``ContextVar``s via ``bind_request_context``, and lifts host-bound isolation keys (``x-agent-{user,chat}-isolation-key``) from the optional ``agent_framework_hosting`` package into a provider-local ``IsolationContext`` so the storage layer carries the correct partition keys without channels having to know about them. - ``_shared.py`` — extracts all SDK ``Item`` / ``OutputItem`` ↔ framework ``Message`` conversion helpers into one place so both ``_responses.py`` and the new history provider can share them. Restores ``_convert_file_data`` for inline ``input_file`` payloads, and the hosted-MCP routing for ``custom_tool_call_output`` items whose ``call_id`` carries the ``mcp_*`` prefix. - ``_ids.py`` — shared id helpers. - ``_responses.py`` — shrinks ~700 lines, re-exports converters for back-compat with existing tests. - ``tests/test_history_provider.py`` — exercises the new provider against a fake SDK backend; the host-isolation test is gated on the optional ``agent_framework_hosting`` import. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(foundry_hosting): add local_storage_root for file-based dev history Adds an optional `local_storage_root: str | Path | None` parameter to `FoundryHostedAgentHistoryProvider`. When set and the provider is running outside a Foundry Hosted Agent container, conversations are persisted to JSONL files via `agent_framework.FileHistoryProvider` laid out as: {root}/{user_key or '~none'}/{chat_key or '~none'}/{session_id}.jsonl Hosted mode (FOUNDRY_HOSTING_ENVIRONMENT set) ignores the option with a one-time INFO log so Foundry storage always wins on the platform. The in-memory fallback is unchanged when the option is omitted. Path safety: isolation segments are validated against the same character allowlist FileHistoryProvider uses for session-id stems and base64-url-encoded with a reserved "~iso-" prefix when unsafe. "~none" sentinel for missing keys can never collide with a real isolation key (real keys starting with "~" are encoded). The resolved target dir is also re-checked to be inside the configured root. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(foundry_hosting): address PR-1 review comments - _shared.py:_capture_raw narrows `except Exception` to `except TypeError` and emits a WARNING with traceback so the lossy fallback to a synthesized round-trip is observable. Mirrors the reviewer suggestion. - _history_provider.py:save_messages narrows `except Exception` to `except FoundryStorageError` so only storage-validation failures (4xx/5xx, opaque server errors) are swallowed. Network / TLS / auth / payload-builder bugs propagate so the caller can retry / alert. Adds an instance-level `failed_writes` counter operators can poll for silent-drop visibility. - _history_provider.py id-stamping loop: drops the `contextlib.suppress(AttributeError, TypeError)` around `item.id = new_id` so SDK contract changes surface in the test suite instead of silently corrupting the chain (the storage backend rejects the entire `create_response` with HTTP 500 when synthetic prefix-based ids leak through). `import contextlib` removed. - tests: * Unit-cover `foundry_response_id` / `foundry_response_id_factory` / `foundry_item_id` so SDK `IdGenerator` contract changes are caught locally. * Cover the `save_messages` wire payload: required-by-storage fields (`background`, `parallel_tool_calls`, `instructions`, `agent_reference`), env-var-driven stamping (`FOUNDRY_AGENT_NAME` / `FOUNDRY_AGENT_VERSION` / `FOUNDRY_AGENT_SESSION_ID` / `MODEL_DEPLOYMENT_NAME` with `AZURE_AI_MODEL_DEPLOYMENT_NAME` fallback), and the rule that `model` / `agent_session_id` / `agent_reference.version` are omitted (not stamped to `None`) when their env vars are unset. * Cover the `FOUNDRY_AGENT_SESSION_ID` last-resort chain anchor on both the get and save paths, including the prefix gate that blocks non-`caresp_*`/`resp_*` values from reaching storage, and the precedence rule that a host binding wins over the env. * Replace the old `test_save_messages_swallows_backend_errors` with two tests asserting the new contract: storage errors are swallowed and bump `failed_writes`; everything else propagates and leaves the counter at zero. 141 unit tests pass; mypy + pyright + ruff clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(foundry_hosting): address PR-1 round-2 review comments - Hosted detection now delegates to AgentConfig.from_env().is_hosted so a future Foundry SDK rename of FOUNDRY_HOSTING_ENVIRONMENT propagates automatically; drop the local _ENV_FOUNDRY_HOSTING_ENVIRONMENT constant. - Drop the FOUNDRY_AGENT_SESSION_ID fallback in both get_messages and save_messages: per the SDK it identifies the *container instance*, not the conversation, so chaining off it would silently merge unrelated conversations across container restarts. The host-bound previous_response_id (set by ResponsesChannel) is the only authoritative anchor; the env value is still stamped into the persisted envelope's agent_session_id for operator correlation. - Update module docstring + replace TestFoundryAgentSessionIdAnchor with assertions for the new contract (env var ignored as anchor, still stamped onto persisted envelope, host binding wins). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(foundry_hosting): reconcile with upstream main (#5851, #5666) Brings the FoundryHostedAgentHistoryProvider refactor branch back into sync with the foundry_hosting changes that have landed on upstream main since PR-1 was opened: * #5851 (path traversal in checkpoint storage, CWE-22). The workflow-host code in ``_responses.py`` builds a ``FileCheckpointStorage`` from a caller-controlled ``context_id`` (``previous_response_id`` / ``conversation_id`` / ``response_id``). Switch both call sites to route through ``_checkpoint_storage_for_context``, which rejects separators, NUL bytes, drive letters, absolute paths, and all-dot segments, and enforces ``is_relative_to(root)`` before any directory is created. * #5666 (function approval flow). Make the SDK-Item → AF-Message conversion helpers in ``_shared.py`` async and accept an optional ``approval_storage`` keyword: - ``_items_to_messages`` / ``_item_to_message`` / ``_item_to_message_inner`` - ``_output_items_to_messages`` / ``_output_item_to_message`` / ``_output_item_to_message_inner`` For ``mcp_approval_request`` / ``mcp_approval_response`` items the helpers now load the original function-call Content from the approval storage (via ``ApprovalStorage.load_approval_request``) instead of synthesising a placeholder. This matches upstream semantics and lets approval round-trips reconstruct the real payload. The ``ApprovalStorage`` Protocol moves to ``_shared.py`` so the conversion helpers can reference it without pulling in ``_responses.py`` (which would create a circular import). The concrete ``InMemoryFunctionApprovalStorage`` and ``FileBasedFunctionApprovalStorage`` stay in ``_responses.py`` next to the host that owns them, and re-export ``ApprovalStorage`` from ``_shared`` for compatibility. The workflow-host streaming path passes its own ``self._approval_storage`` into ``_to_outputs`` so approval requests are saved at emit time. * Bump ``_history_provider.FoundryHostedAgentHistoryProvider.get_messages`` to ``await`` the now-async ``_output_items_to_messages`` call. No public API change beyond the new keyword-only ``approval_storage`` parameter on the four conversion entry points. Validation: - uv run poe check-packages -P foundry_hosting (lint + pyright clean) - uv run poe mypy -P foundry_hosting (clean) - uv run poe test -P foundry_hosting (183 passed, 1 skipped) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Motivation and Context
This PR is the first piece of the Python hosting Channels work that landed as ADR / spec in #5549 and is being merged into the
feature/python-hostingintegration branch. It refactorsagent-framework-foundry-hostingso the rest of the hosting-Channels stack can build on top of a clean shared layer.Description
_responses.pyinto focused modules:_history_provider.py—FoundryHostedAgentHistoryProviderexposed as the public history-provider entry point._shared.py/_ids.py— internal building blocks reused across Responses and the upcoming hosting Channels.__init__.pyexports to surface the new history-provider type and prepare for the hosting integration.tests/test_history_provider.py, expandedtests/test_responses.py).Stack
This is the first PR (PR-1 of 9) in the hosting-Channels stack, all targeting
feature/python-hosting:refactor/foundry-hosted-agent-history-providerfeat/hosting-corefeat/hosting-samplesPR-1 is independent of all the others — can merge in any order relative to PR-2.
Contribution Checklist
FoundryHostedAgentHistoryProviderre-export.