Python: Give a hosted Foundry agent a single source of conversation history - #7957
Python: Give a hosted Foundry agent a single source of conversation history#7957Atharva Vichare (atty57) wants to merge 2 commits into
Conversation
…istory A hosted `/responses` agent fed the model the conversation transcript more than once per request, and the duplication compounded every turn: by the third turn the model saw the first turn three times. `_handle_inner_agent` gives `agent.run(...)` the full platform transcript from `context.get_history()` as `messages`, and separately the session loaded from the session store. The existing guard pops the transient history buffer out of `session.state`, but the session's `service_session_id` survives, and core resumes it. The run therefore continues a service-side thread that already holds the whole transcript while `messages` carries that transcript again, and the service appends the duplicated request to the thread, so each turn grows superlinearly. This is rarely seen because the default session store's reads currently fail and return None, so the service thread is never resumed. Any working session store exposes it. Turn server-side storage off for the run instead, so the platform record is the only history in play, mirroring the .NET behaviour from microsoft#7525 and microsoft#7572: - Set `store=False` on the run's chat options when hosting manages history. The chat client then keeps nothing of its own and reports no conversation id, so no second thread exists to resume. - If a service session id lands on the session anyway, the client stored the turn despite that setting and a second unreconciled record now exists. Fail the request and leave the session unsaved, so later turns cannot resume onto the duplicated thread. - Add `allow_stored_output_enabled` to `ResponsesHostServer` for containers that want the chat client left exactly as they configured it. Nothing is then overridden or checked, and reconciling the two records is theirs to own. Fixes microsoft#7955
There was a problem hiding this comment.
Pull request overview
Makes hosted Foundry agents rely exclusively on platform-managed conversation history.
Changes:
- Disables downstream response storage by default.
- Detects clients that store despite configuration.
- Adds an opt-in and regression tests.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
_responses.py |
Adds storage policy and violation handling. |
test_responses.py |
Tests storage, failure, and opt-in behavior. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
| # `context.get_history()` above. Letting the agent's own service store it too would | ||
| # give the model the same transcript twice -- once as input, once as the resumed | ||
| # service thread -- compounding every turn. | ||
| chat_options["store"] = False |
| if self._uses_hosted_responses_history and not self._allow_stored_output_enabled: | ||
| # The platform records this conversation and serves it back through | ||
| # `context.get_history()` above. Letting the agent's own service store it too would | ||
| # give the model the same transcript twice -- once as input, once as the resumed | ||
| # service thread -- compounding every turn. | ||
| chat_options["store"] = False |
|
Reporter of #7955 here — thanks for picking this up so quickly, and the direction question you pose at the end is exactly the right one. I ran my repro bench from the issue against this branch (plus an upgrade simulation), so here are verification results rather than opinions. ✅ Verified: the fix works for fresh conversations. With a fake service that honours
(If the real service instead rejects Suggested remedy, which I think also answers your own review question: clear
My repro/upgrade-sim scripts are self-contained (no Azure needed) — glad to share them here or test iterations of this branch. If it's easier, I'm also happy to push a follow-up implementing neutralize-on-load: I have it working on top of this branch against my bench (fresh conversations stay clean, and the pre-fix-conversation case above completes cleanly instead of failing). |
A conversation that began before storage was disabled persisted a session naming the service thread that already holds its transcript. Core resumes that thread ahead of anything in the run options, so the model still saw the transcript twice, and the storage check then failed the turn even though the client had honoured store=False. Because a failed turn is not saved, the same stale session loaded again on every later turn. Clearing service_session_id where the transient history buffer is popped heals those conversations on their first post-upgrade turn, and makes the check exact: an id present afterwards can only have been set by this run.
|
Thanks — this is a genuinely useful report, and Finding 1 is real. Fixed in 7dcf9cb. Finding 1. Confirmed exactly as you describe. Your neutralize-on-load remedy is also what makes that check exact instead of heuristic, which is why I took it as written: Added a regression test for the upgrade path: turn 1, then the stored session mutated to carry I kept it in this PR rather than a follow-up since the repo asks for one open PR per issue — but I'd still like to run your upgrade-sim against it, since your bench covers the real chain and mine simulates the pre-fix state. Please do paste the scripts or link a gist. Finding 2 is a maintainer call, and your measurement is a stronger signal than #7487's "not reproduced as consistent" — it's the exact population this change affects: hosted Python agents on the Foundry project endpoint. Worth being explicit that the two halves of this PR are separable:
So if the +5 s reproduces on a live endpoint, the safe shape is neutralize-on-load as the default with @microsoft/agent-framework-python — the ask is: (a) keep |
|
Ran the bench against
Your regression test's simulated pre-fix state matches what the real chain produces, for what it's worth — the mutation you describe is exactly the session shape my released-version run persists. Bench script below as requested — it's the upgrade-sim variant of the repro already in #7955 ( On the measurement: I'll re-run the
|
|
I'm going to close this PR, we are having some design discussions on how we want to approach this, and I will create the PR to address the issue once that has played out, thanks Atharva Vichare (@atty57) |
Motivation & Context
A hosted
/responsesagent feeds the model the conversation transcript more than once per request, and the duplication compounds every turn. With a real model the agent visibly repeats its replies, getting worse as the conversation grows, plus the matching token overspend. In the reporter's repro, by turn 3 the model sees turn 1 three times (23 messages instead of 9)._handle_inner_agentgivesagent.run(...)two sources of history at once:run_kwargs["messages"], the full platform transcript fromcontext.get_history()plus the new input, andrun_kwargs["session"], the session loaded from the session store. The existing guard pops the transient history buffer (_HOSTED_RESPONSES_HISTORY_SOURCE_ID) out ofsession.state, but the session's top-levelservice_session_idsurvives and core resumes it. The run therefore continues a service-side thread that already holds the whole transcript whilemessagescarries that transcript again. The service appends the duplicated request to the thread, so the next turn's thread contains it twice, hence the superlinear growth.This is rarely seen because the default
FoundryAgentSessionStore's reads currently fail and returnNone, so the service thread is never resumed. Any working session store exposes it..NET fixed this class of bug in #7525 and refined it in #7572; this is the Python counterpart.
Description & Review Guide
What are the major changes?
Server-side storage is turned off for the run, so the platform record is the only history in play:
chat_options["store"] = Falsewhen hosting manages history. The chat client then keeps nothing of its own and reports no conversation id, so no second thread exists to resume. This removes the cause rather than cleaning up after it, and avoids creating a downstream thread per turn.allow_stored_output_enabledkeyword onResponsesHostServer, defaulting toFalse. Setting it toTrueleaves the chat client exactly as the container configured it; nothing is overridden or checked, and reconciling the two records is the container's responsibility.Three regression tests in
TestAgentSessionPersistencecover the duplicated transcript, the client that stores despitestore=False, and the opt-in. Each fails without the source change.What is the impact of these changes?
Hosted agents whose chat client stores conversation state service-side (the default for
FoundryChatClientand OpenAI Responses whenstoreis notFalse) now send each message to the model exactly once.Behaviour changes for those agents: the downstream service is asked not to store, and an agent that stores anyway now fails loudly instead of silently duplicating.
allow_stored_output_enabled=Truerestores the previous hands-off behaviour. The constructor keyword is additive and defaulted, so no signature breaks.Note that Python: [Bug]: Foundry Hosted agent - delay before final response when streaming #7487, the ~5s streaming delay seen with the Foundry project endpoint and
store: False, was closed as not caused by the SDK and not reproduced as consistent. This change makesstore: Falsethe default path for hosted agents, so if that delay is real it would become more visible;allow_stored_output_enabled=Trueis the escape hatch.What do you want reviewers to focus on?
Whether disabling downstream storage by default is the direction you want for Python, matching .NET: Add Options for Hosted Agent to Allow Backend Storage #7572, versus neutralizing
service_session_idon the hosted session. Also whether the misconfigured-client check belongs in thefinallyblock as written, and whether a readiness-time probe like the .NET one is wanted here as a follow-up.Related Issue
Fixes #7955
Contribution Checklist
breaking changelabel (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.