Skip to content

[agentserver-responses] Exclude failed response inputs from replayable conversation history - #49009

Open
prasanna164-code wants to merge 3 commits into
Azure:mainfrom
prasanna164-code:agentserver-failed-history
Open

prasanna164-code wants to merge 3 commits into
Azure:mainfrom
prasanna164-code:agentserver-failed-history

Conversation

@prasanna164-code

@prasanna164-code prasanna164-code commented Sep 15, 2026

Copy link
Copy Markdown

Description

Part of #48929 (provider-side fix; the hosted Foundry storage endpoint is tracked separately, see below).

When a turn fails, AgentServer already holds a stored response for it together with its input_items. get_history_item_ids() then keeps returning those input IDs for every later request in the same conversation_id, and for any request chained through previous_response_id. The input that made the turn fail (for example a function_call_output with no matching call) is replayed into each subsequent request, which fails the same way, so one bad request poisons the conversation indefinitely. The Agent Framework workaround (microsoft/agent-framework#7637) was closed in favour of fixing this in the provider layer, where the issue notes the behaviour is defined.

Change

The provider invariant suggested in the issue is now defined on ResponseProviderProtocol.get_history_item_ids and enforced by the in-memory and file providers:

  • A response whose stored status is failed contributes only the history it inherited. Its own input items and output items are excluded from replayable history. The filter is applied while the chain is resolved, so it runs before the limit truncation and failed items never displace replayable ones.
  • Every other status (completed, incomplete, cancelled, in-flight, or unknown) is unchanged.
  • The failed response and its input items stay stored and retrievable through GET /responses/{id} and GET /responses/{id}/input_items for diagnostics.
  • The rule lives in one place, store/_history.py (is_replayable_status), so both providers stay in lock-step.

Both providers read the status from the stored response envelope, which is the single source of truth for it. For FileResponseStore that means one extra small JSON read per response in the chain, and no second copy of the status that could drift from the envelope after a crash between two writes. Existing on-disk stores need no migration.

Output items are excluded along with the input because a failed turn may have produced a function_call that never received an output; replaying it would fail the next turn for the mirror-image reason.

Known limitation

The rule is evaluated against the status of the response that contributes the items. History that a response captured while the previous turn was still running is a frozen snapshot: if a client chains previous_response_id to a response that has not finished yet (or, outside resilient mode, starts a second turn in a conversation while the first is in flight) and that earlier turn later fails, the snapshot still carries its input. Any status-based filter has this window, because the outcome of an in-flight turn is unknown when its input is read. Closing it means rejecting chaining to a non-terminal response or locking the conversation in the orchestrator, which is a separate behaviour decision; happy to follow up if that is wanted.

Hosted Foundry storage

FoundryStorageProvider delegates history resolution to the hosted history/item_ids endpoint, and hosted mode auto-selects that provider. The rule cannot be enforced client-side there: the storage API exposes per-response GET, batch item retrieval and history/item_ids (bare item IDs), with no conversation listing and no item ownership, so the client cannot tell which returned IDs belong to a failed response. The invariant is therefore defined on ResponseProviderProtocol and called out on FoundryStorageProvider.get_history_item_ids as the contract the hosted endpoint needs to implement (or expose a server-side filter for), as the issue suggests. This PR does not close #48929 on its own; the hosted part stays open with the service team.

Testing

tests/unit/test_failed_response_history.py runs every scenario against both InMemoryResponseProvider and FileResponseStore and asserts identical results:

  • failed turn excluded from conversation history, earlier and later successful turns kept in order;
  • a response created as in_progress and later updated to failed (the orchestrator's create-then-terminal-update path);
  • previous_response_id chaining from a failed response yields only its inherited history, and a successful response chained after it stays clean;
  • exclusion happens before limit truncation;
  • get_input_items on the failed response still returns its items;
  • file store reads the status from the envelope, so a stale or tampered indexes.json cannot make a failed turn replayable again;
  • enum status members are normalised to their wire value.

tests/contract/test_failed_response_history.py drives ResponsesAgentServerHost end to end with the reproduction from the issue (a function_call_output for a call that does not exist): the next valid turn in the conversation succeeds and its handler sees no history from the failed turn, successful turns before and after the failure are kept, chaining through the failed response via previous_response_id is clean, and the same holds for streaming and background requests. The failed response and its input items are still retrievable.

With the status rule neutralised (old behaviour), 13 of the 22 new tests fail, including all end-to-end conversation, chaining, streaming and background cases. The existing unit and contract suites pass with the change. black, pylint with the repository guidelines checker, mypy and cspell are clean on the changed files.

All SDK Contribution checklist:

  • The pull request does not introduce [breaking changes]
  • CHANGELOG is updated for new features, bug fixes or other significant changes.
  • I have read the contribution guidelines.

General Guidelines and Best Practices

  • Title of the pull request is clear and informative.
  • There are a small number of commits, each of which have an informative message. This means that previously merged commits do not appear in the history of the PR. For more information on cleaning up the commits in your PR, see this page.

Testing Guidelines

  • Pull request includes test coverage for the included changes.

AgentServer stores a response and its input items when it processes the
initial response.created event. When the handler later fails the response,
get_history_item_ids() still returned that response's input item IDs for
every later request in the same conversation, and for requests chained
through previous_response_id. The input that made the turn fail (for
example a function_call_output with no matching call) was replayed into each
subsequent request, which then failed the same way.

Define the invariant on ResponseProviderProtocol and enforce it in the
in-memory and file providers: a failed response contributes only the history
it inherited; its own input and output items are excluded, before the
history limit is applied. The failed response and its input items remain
stored and retrievable through the response and input_items endpoints.

The file store now records the response status in indexes.json on create and
update so history resolution does not open every envelope; stores written
before this change fall back to the envelope status.

Fixes Azure#48929
Copilot AI balanced review requested due to automatic review settings September 15, 2026 01:31
@github-actions github-actions Bot added Community Contribution Community members are working on the issue customer-reported Issues that are reported by GitHub users external to the Azure organization. Hosted Agents sdk/agentserver/* labels Sep 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thank you for your contribution prasanna164-code! We will review the pull request and get back to you soon.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
7 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

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.

🟡 Changes recommended

File-store status indexes can become stale or diverge from response envelopes, causing incorrect history replay.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Prevents failed response items from poisoning subsequent replayable history.

Changes:

  • Defines shared failed-status replay rules.
  • Applies filtering to memory and file stores, with persisted status indexes.
  • Adds unit, contract, and changelog coverage.
File summaries
File Description
CHANGELOG.md Documents the history fix.
store/_base.py Defines the provider invariant.
store/_history.py Adds shared status normalization and filtering.
store/_memory.py Filters failed response items in memory.
store/_file.py Filters failed items using indexed status.
tests/unit/test_failed_response_history.py Tests provider behavior and compatibility.
tests/contract/test_failed_response_history.py Tests synchronous, streaming, and background flows.
Review details

Suppressed comments (1)

sdk/agentserver/azure-ai-agentserver-responses/azure/ai/agentserver/responses/store/_file.py:744

  • None is both the default sentinel and a valid unknown status, so an update that omits/clears status leaves the previous indexed value intact. After updating a failed response without a status, the in-memory provider treats it as replayable, but the file provider remains non-replayable. Always rewrite the indexed status on an envelope update so it stays aligned with the source envelope.
        if status is not None:
            current["status"] = normalize_status(status)
  • Files reviewed: 7/7 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Keeping a copy of the status in indexes.json created a second source of
truth that could disagree with the envelope if the process stopped between
the two atomic writes, and an update that omitted the status left the old
indexed value in place. Read the status from the persisted envelope instead,
which is written atomically, and drop the indexed copy.
Copilot AI review requested due to automatic review settings September 15, 2026 02:05

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.

🟡 Changes recommended

The hosted Foundry provider still delegates to an endpoint that does not demonstrably enforce the new invariant.

Get a fresh assessment by requesting another Copilot review.

Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Copilot AI review requested due to automatic review settings September 15, 2026 02:17

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.

🟢 Approval recommended

The scoped provider fix is consistent, preserves diagnostics, and has comprehensive regression coverage.

Review details
  • Files reviewed: 8/8 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Community Contribution Community members are working on the issue customer-reported Issues that are reported by GitHub users external to the Azure organization. Hosted Agents sdk/agentserver/*

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[azure-ai-agentserver-responses] Failed response inputs are replayed in conversation history

2 participants