Harden replay-proxy "Available tools" normalization against built-in tool set changes#2013
Merged
stephentoub merged 1 commit intoJul 17, 2026
Merged
Conversation
When a model calls a nonexistent tool, the runtime replies with
"Available tools that can be called are <list>." The E2E replay proxy
embedded that literal enumeration in ~54 snapshots, so any change to the
built-in tool set (e.g. the new write_agent tool) broke snapshot matching
and caused the copilot-agent-runtime C# SDK canary to retry forever until
the 45m timeout.
Collapse the entire enumeration to a stable ${available_tools} placeholder
on both the live-request side (normalizeAvailableToolNames) and the stored
snapshot side (new normalizeStoredToolMessages applied at load time), so
snapshots keep matching as the built-in tool set evolves across runtime
versions and platforms.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: fa431f41-c7dc-4580-9cba-274170ee64df
Contributor
There was a problem hiding this comment.
Pull request overview
Hardens replay matching against runtime built-in tool-set changes.
Changes:
- Normalizes available-tool enumerations in live and stored messages.
- Removes obsolete shell-family normalization.
- Adds recording and legacy-snapshot regression tests.
Show a summary per file
| File | Description |
|---|---|
test/harness/replayingCapiProxy.ts |
Adds stable available-tools normalization. |
test/harness/replayingCapiProxy.test.ts |
Tests normalization and replay compatibility. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 0
- Review effort level: Medium
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The Copilot SDK C# tests leg in
github/copilot-agent-runtime(a non-blocking canary that replays these E2E snapshots against runtimeHEAD) started timing out at the 45-minute cap, e.g. this run. Every request loggedNo cached response found for POST /chat/completionsand retried forever.Root cause
When a model calls a tool that doesn't exist (the harness deliberately triggers this via a fake
report_intenttool), the runtime replies with:The replay proxy previously only normalized the platform-specific shell tool names inside that list and froze the rest of the enumeration verbatim in ~54 snapshots. The runtime graduated multi-turn subagents to always-on and added a new default built-in tool,
write_agent, to that list. The stored snapshots (..., read_agent, list_agents, grep, glob, task.) no longer matched the live request (..., read_agent, list_agents, write_agent, grep, glob, task.), so no snapshot matched, each request retried until it exhausted the suite budget, and the whole leg timed out.This is a by-design runtime change (multi-turn subagents GA); the brittleness lives in this harness, which shouldn't re-record on every built-in tool set change.
Fix
Collapse the entire enumeration after
Available tools that can be called are(up to the terminating.) to a stable${available_tools}placeholder:normalizeAvailableToolNamesnow replaces the whole list on the live-request side.normalizeStoredToolMessages, applied inloadStoredData, re-normalizes the same string in stored snapshots at load time (the result normalizers otherwise only run against live requests, so the stored side would keep its stale list).Both sides collapse to
${available_tools}and match without re-recording, and stay matched as the built-in tool set evolves across runtime versions and platforms. Removed the now-unused shell-tool-family normalizer that only served the old list-preserving path.Testing
cd test/harness && npm test→ all 37 harness tests pass.write_agent) still matches a live request whose tool result includeswrite_agent. Verified it fails (HTTP 500 / no match) without the fix and passes with it.Generated by Copilot