Skip to content

.NET: [BREAKING] Add file_access_read_lines and move the line-numbering contract onto AgentFileStore - #7671

Open
Anton Sokolovskyi (antsok) wants to merge 17 commits into
microsoft:mainfrom
antsok:issue-7571-dotnet-file-access-read-lines
Open

.NET: [BREAKING] Add file_access_read_lines and move the line-numbering contract onto AgentFileStore#7671
Anton Sokolovskyi (antsok) wants to merge 17 commits into
microsoft:mainfrom
antsok:issue-7571-dotnet-file-access-read-lines

Conversation

@antsok

@antsok Anton Sokolovskyi (antsok) commented Aug 14, 2026

Copy link
Copy Markdown

Motivation & Context

Two defects, one of which only became visible while fixing the other.

The reading gap (#7571). The harness file tools are line-precise when editing — file_access_replace_lines takes 1-based line numbers and file_access_grep reports them — but all-or-nothing when reading. There is no way to see the lines around a match without reading the whole file, so agents either re-read entire files or edit by a line number they never looked at.

Grep and the editor did not agree on what a line is. The stores split on '\n' and stripped '\r'; FileEditor split on '\n', '\r\n' and a lone '\r' and kept terminators. That is not a cosmetic difference:

  • On content with lone '\r' terminators, grep's line 1 covers only part of what the model was shown, so editing by that number silently changes the wrong text.
  • On a newline-terminated file, grep can report a trailing line number the editor rejects as out of range.
  • Because grep stripped terminators while NewLine is written verbatim, feeding a grepped line back into replace_lines joined it to the next line.

The same hole, one level down — and this is new scope since the first version of this PR. Unifying the splitter fixes the two stores in this package, but AgentFileStore.SearchAsync was abstract with no numbering contract at all, while read_lines and replace_lines re-count from ReadAsync. So a custom store could make "line 5" mean two different things, and replace_lines would edit the wrong line — in range, reporting success. FileMemoryProvider had the identical hole.

The previous revision of this PR documented that as a caveat and called it "a maintainer call". @moonbox3 made that call on the Python PR #7669: the rule should live on the contract, not in a doc comment. This PR now mirrors #7669 so both SDKs carry the same contract.

Description & Review Guide

  • What are the major changes?

    The reading gap:

    1. FileEditor.SplitLinesKeepEnds becomes internal and is the single definition of a line.
    2. FileSearchMatch.Line is reported verbatim, terminator included.
    3. New file_access_read_lines tool, rendering <n>\t<line> with everything after the tab verbatim — so a row is already a valid replace_lines new_line.

    The contract, now on the base class:
    4. SplitLines() publishes the split every line_number addresses. Per-SDK by design: it need not match Python, only be consistent here, because a line number never crosses runtimes.
    5. ScanContent() is the numbering primitive. Both shipped stores report through it, which also removes the scan loop that was duplicated between them.
    6. FindMatchingFilesAsync() is a new hook for narrowing to the files worth reading. The regex goes down as a hint with superset semantics — over-returning is harmless because the base re-scans, under-returning loses matches. A backend with a native index overrides it and narrows server-side. Its default is built from ListChildrenAsync, so a store implementing nothing beyond the mandatory members gets aligned numbers for free.
    7. SearchAsync is no longer abstract. It reads and numbers candidates itself, and re-applies the glob and the non-recursive rule, since the hook may over-return.
    8. Overriding SearchAsync stays first-class — a backend that can do the whole job natively should — but then it owns numbering, and nothing checks it at runtime. SearchAsync documents what is owed: LineNumber is a 1-based coordinate into SplitLines of the content ReadAsync returns, ScanContent produces that correctly, and getting it wrong fails silently — the search looks right and a later line edit lands on a line the caller never saw. See focus item 3.
    9. FileLineEdit.ExpectedLine — when supplied, the edit is refused unless the target line still says what the caller saw. Catches splitter drift, a stale line number, and the file changing between read and write.

    Added during review, after the items above:

    1. The tool descriptions now state how lines are counted. @westey-m asked for the case where a model reads a whole file with file_access_read and then edits by line number: it has to count the lines itself, and nothing told it the rule. read, read_lines, replace_lines, grep and the two memory tools now say that lines are terminated by \r\n, \n or a lone \r, that each line keeps its terminator, and that content ending in a terminator has no extra empty line after it. Taken from the cases FileEditorTests already pins rather than from reading the splitter. They also state that the numbers are 1-based, so a model counting lines from a whole-file read knows where to start.
    2. The runtime alignment check was removed. Earlier revisions had both providers re-read every matched file and refuse grep when a store's numbers disagreed with SplitLines. @westey-m's call: that is runtime cost for all users to police implementations that are not per spec, and a spec violation is the implementer's bug. SearchAlignment, BaseSearchResults, ReportsAlignedLineNumbers and DisableSearchAlignmentCheck are gone; the expectation now lives on the members an implementer overrides. A conformance test library is the better answer and is tracked separately as .NET: Add Conformance Test Library for Agent Framework #7931.
  • What is the impact of these changes?

    Breaking, in these ways:

    Change Who notices
    FileSearchMatch.Line includes terminators anyone consuming grep output
    Line numbers change on lone-'\r' content and on a trailing newline same
    The whole terminator is stripped before matching, so match$ now matches on a CRLF line where it could not before, and a pattern targeting a literal '\r' no longer matches the one such a line ends with pattern authors
    SearchAsync is no longer abstract custom stores (source-compatible; override still works)

    The whole surface is [Experimental("MAAI001")]. ApiCompat does not flag any of itFileEditor is internal and Line keeps its type — so a passing Release build is not evidence of compatibility. Removing abstract while keeping the member virtual does pass Package Validation; verified by building the package in Release with IsReleased=true.

    Two further changes are not API breaks but do change what the model is told: every line-addressing tool description gains the counting rule, and the expected_line mismatch message no longer echoes the line it found — raised in review as a read oracle where write tools are auto-approved while read tools are not, and applied in 30115b0.

    Cost. None: no tool re-reads a file, and SearchAsync returns its results directly.

    The narrowing hook is not a speed-up and is not sold as one — it is "same speed, now safe". A selectivity sweep against a store doing the whole job in its own SearchAsync:

    selectivity own SearchAsync base + hook
    5% 4.15 ms 1.21 ms
    25% 4.40 ms 4.43 ms
    50% 8.91 ms 5.88 ms
    100% 12.72 ms 11.83 ms

    Per-method timings across local disk, in-memory, Azure Blob and Redis show no method-level effect; happy to attach the full table if useful.

    file_access_read_lines joins the read-only tool set, so it is exposed under DisableWriteTools and covered by ReadOnlyToolsAutoApprovalRule — the auto-approval docs and sample security notes are updated accordingly.

  • What do you want reviewers to focus on?

    1. Whether the contract belongs on AgentFileStore at all — that is the substantive question, and it is a maintainer call being made here rather than assumed.

    2. The superset semantics of FindMatchingFilesAsync. Over-returning is harmless, under-returning silently loses matches. Whether that is documented clearly enough for someone implementing it against a native index.

    3. Whether documentation alone is enough to carry the numbering contract. Earlier revisions tracked "did the base number these?" by tagging the returned list and had the providers verify it; that is removed per change 11 above. What replaces it is the <remarks> on SearchAsync, plus SplitLines and ScanContent being the published primitives an implementer is meant to build on. The open question is whether an implementer overriding SearchAsync will actually read it there, given the failure mode is silent and shows up in someone else's edit.

      This is where the two PRs now diverge. Python: [BREAKING] Add file_access_read_lines and move the line-numbering contract onto AgentFileStore #7669 (Python) still verifies at runtime and was hardened in that direction on 25 Aug. That divergence is not deliberate design — it is one maintainer preference applied to one PR — so it is worth a view on whether Python should follow.

    4. The per-line snippet offset arithmetic in both stores now that terminators are part of each line. FileSystemAgentFileStoreTests previously asserted no Line values at all, so the six search tests mirrored into it are where that arithmetic is now pinned for the disk-backed store.

    Two deliberate divergences from the Python half (Python: [BREAKING] Add file_access_read_lines and move the line-numbering contract onto AgentFileStore #7669):

    Line-rule parity note: Python addresses a trailing empty line on "a\nb\n"; .NET has two lines there, because .NET's line editor never had that phantom line. Each language stays self-consistent, which is what the grep → read → edit round trip actually depends on.

Related Issue

#7571 — linked without a closing keyword on purpose: the Python half ships as #7669, and the issue should stay open until both land. Will change to Closes in the last one.

Note that #7571's body still says no store-protocol change is needed; that predates the review discussion above and is no longer accurate for either language.

Contribution Checklist

  • The code builds cleanly without any errors or warnings — Release --warnaserror across all five TFMs, Package Validation included
  • All unit tests pass, and I have added new tests where possible — 44 new tests, 16 of them in a new AgentFileStoreContractTests
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue, and there is no other open PR for this issue
  • 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.

Copilot AI balanced review requested due to automatic review settings August 14, 2026 20:17
@agent-framework-automation agent-framework-automation Bot added documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs .NET Usage: [Issues, PRs], Target: .Net breaking change Usage: [PRs], Target: all PRs that introduce changes that are not backward compatible labels Aug 14, 2026
@antsok Anton Sokolovskyi (antsok) changed the title .NET: [BREAKING] Add file_access_read_lines and align grep with the line editor .NET: [BREAKING] Issue 7571 file access read lines Aug 14, 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

Adds line-range reading and aligns .NET grep results with line-editing semantics.

Changes:

  • Adds file_access_read_lines.
  • Preserves line terminators across grep/read/edit workflows.
  • Updates approvals, documentation, and tests.

Reviewed changes

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

Show a summary per file
File Description
InMemoryAgentFileStoreTests.cs Tests updated grep semantics.
FileEditorTests.cs Tests splitting and slicing.
FileAccessProviderTests.cs Tests the new tool and approvals.
HarnessAgentTests.cs Verifies tool exposure.
InMemoryAgentFileStore.cs Aligns grep line handling.
FileSystemAgentFileStore.cs Aligns filesystem grep behavior.
FileSearchMatch.cs Documents verbatim lines.
FileEditor.cs Adds shared splitting and slicing.
FileAccessProviderOptions.cs Documents read-only tool behavior.
FileAccessProvider.cs Implements file_access_read_lines.
Harness_Step03_DataProcessing/README.md Updates security guidance.
Claw_Step02_WorkingWithData/README.md Updates security guidance.

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

Comment thread dotnet/src/Microsoft.Agents.AI/Harness/FileStore/FileEditor.cs Outdated
Comment thread dotnet/src/Microsoft.Agents.AI/Harness/FileAccess/FileAccessProvider.cs Outdated
Anton Sokolovskyi (antsok) added a commit to antsok/agent-framework that referenced this pull request Aug 14, 2026
…as the schema does

Addresses both review comments on microsoft#7671.

TrimTrailingNewline removed only "\n", so grep matched against text such as
"match\r" on CRLF and lone-CR lines and an end-anchored pattern like "match$"
failed even though the line's text was exactly "match". Renamed to
TrimLineTerminator and it now strips "\r\n", "\n", or a lone "\r".

The file_access_read_lines description and the SliceLines failure messages
referred to end_line/start_line, but the generated schema exposes the arguments
as endLine/startLine, so the model could be prompted to emit an invalid argument
name. Both now use the schema's names. (new_line is left as-is: FileLineEdit
sets it explicitly via JsonPropertyName.)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Anton Sokolovskyi (antsok) added a commit to antsok/agent-framework that referenced this pull request Aug 14, 2026
…s_grep

Ports the fix for the same defect found by review on the .NET side (microsoft#7671).

_search_file_content removed only the trailing "\n" before matching, so on a CRLF
file the pattern was applied to text such as "beta match\r" and an end-anchored
pattern like "match$" failed even though the line's text is exactly "beta match".
The terminator is not part of the line's text, so it is stripped in full now.

The per-line offset had to move with it: it advanced by len(scanned) + 1, which
was only correct while scanned still carried the "\r". It now advances by
len(line), whose terminator is already included, keeping the snippet anchored at
the match.

Also drops a stale claim in _split_lines_keepends' docstring, which still said it
reproduced _search_file_content's content.split("\n") — that dependency now runs
the other way round.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@antsok
Anton Sokolovskyi (antsok) requested a balanced review from Copilot August 14, 2026 20:56

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

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

Suppressed comments (2)

dotnet/src/Microsoft.Agents.AI/Harness/FileStore/FileSystemAgentFileStore.cs:209

  • The new newline, line-number, and snippet-offset behavior is tested only against InMemoryAgentFileStore. FileSystemAgentFileStore has its own copied search loop and an existing comprehensive search test suite, so a store-specific regression here would pass. Add equivalent CRLF, lone-CR, trailing-newline, anchored-pattern, and snippet-offset coverage for this implementation.
            // Lines keep their terminators, so these line numbers address the same lines that
            // replace_lines edits and each reported line can be reused as a literal new_line.
            List<string> lines = FileEditor.SplitLinesKeepEnds(fileContent);

dotnet/src/Microsoft.Agents.AI/Harness/FileAccess/FileAccessProvider.cs:326

  • The advertised line-number parity is not guaranteed for custom AgentFileStore implementations. file_access_grep delegates to the public AgentFileStore.SearchAsync, whose contract does not define splitting or terminator retention, while this method and replace_lines split independently through an internal-only helper. An existing custom store can therefore return a grep number that reads or edits a different line. Define the required semantics on the public store contract and make a shared implementation available (or centralize line matching above the store) before promising parity.
    [Description("Read part of a file by 1-based inclusive line number; omit endLine to read to the end of the file, and an endLine past the last line is clamped. Line numbers match file_access_grep and file_access_replace_lines. Each line is prefixed with its number and a tab; everything after that tab is verbatim, including the line's own terminator, so it can be reused as a file_access_replace_lines new_line.")]
    private async Task<string> ReadLinesAsync(string fileName, int startLine, int? endLine = null, CancellationToken cancellationToken = default)

Anton Sokolovskyi (antsok) added a commit to antsok/agent-framework that referenced this pull request Aug 17, 2026
…s_grep

Ports the fix for the same defect found by review on the .NET side (microsoft#7671).

_search_file_content removed only the trailing "\n" before matching, so on a CRLF
file the pattern was applied to text such as "beta match\r" and an end-anchored
pattern like "match$" failed even though the line's text is exactly "beta match".
The terminator is not part of the line's text, so it is stripped in full now.

The per-line offset had to move with it: it advanced by len(scanned) + 1, which
was only correct while scanned still carried the "\r". It now advances by
len(line), whose terminator is already included, keeping the snippet anchored at
the match.

Also drops a stale claim in _split_lines_keepends' docstring, which still said it
reproduced _search_file_content's content.split("\n") — that dependency now runs
the other way round.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Anton Sokolovskyi (antsok) added a commit to antsok/agent-framework that referenced this pull request Aug 17, 2026
file_access_grep runs through AgentFileStore.search, whose contract says nothing
about how content is split or whether terminators survive, while read_lines and
replace_lines split through the module-private _split_lines_keepends. A custom
store can therefore report a line number that addresses a different line than the
two editing tools do — the wrong-line edit this branch exists to prevent, moved
to custom stores. The claim was written as unconditional in four places, so
_split_lines_keepends, _slice_lines, FileSearchMatch.line and AGENTS.md now say
where it holds and where it does not.

AGENTS.md also still described matching as stripping only the trailing "\n" and
anchoring "as before", which stopped being true in 7aa29c6. Corrected to the
whole terminator, in the same wording as the PR description.

The read_lines tool docstring is left unhedged on purpose: it is prompt text, and
teaching the model to doubt the line numbers would send it back to whole-file
reads, which is the cost this branch exists to remove.

Found while reviewing the .NET port (microsoft#7671), where Copilot raised the same gap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread dotnet/src/Microsoft.Agents.AI/Harness/FileAccess/FileAccessProvider.cs Outdated
Comment thread dotnet/src/Microsoft.Agents.AI/Harness/FileStore/SearchAlignment.cs Outdated
Comment thread dotnet/src/Microsoft.Agents.AI/Harness/FileStore/FileSearchMatch.cs Outdated
From @westey-m's review. Policing implementations that are not per spec is not
worth the cost at runtime; the expectation belongs on the type an implementer is
reading when they get it wrong. A conformance test library, which is the real
answer for checking an implementation, is tracked separately as microsoft#7931.

SearchAlignment and BaseSearchResults are deleted, along with
ReportsAlignedLineNumbers and its two overrides and DisableSearchAlignmentCheck
on both provider options. SearchAsync returns its results directly instead of a
tagged list, and neither provider verifies what a store reports.

The contract those types enforced now sits on SearchAsync itself: a store
overriding it must number FileSearchMatch.LineNumber as a 1-based coordinate
into SplitLines of the content ReadAsync returns, ScanContent builds that
correctly, and numbering against anything else is a bug whose failure mode is
silent -- the search looks right, and the damage appears when a later line edit
applies to a line the caller never saw.

Two doc comments elsewhere still said the tools verify the numbering, which is
no longer true of either. The FileSearchMatch and FileEditor remarks also scoped
the rule to the stores shipped here rather than stating what implementers owe;
both now read as expectations, which the same review asked for.

Descriptions that state the line-counting rule now also say the numbers are
1-based, so a model counting lines from a whole-file read knows where to start.

Eight tests went with the check: seven alignment cases and their six helper
stores in AgentFileStoreContractTests, and the memory-refusal test with its
skewed store in FileMemoryProviderTests. Nothing exercised the removed types
otherwise; the remaining 2099 unit tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

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

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

From Copilot review 5051570368. Both are fallout from 72a1f03 rather than
pre-existing: removing SkewedMemoryStore took the last CancellationToken out of
FileMemoryProviderTests, and removing the shipped-stores trust test took the last
Path.GetTempPath() out of AgentFileStoreContractTests, leaving System.Threading
and System.IO imported for nothing.

Neither surfaced in the build because unused usings are not an error here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

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

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

…est rationale

From Copilot review 5052352338.

SearchFiles_SnippetIsAnchoredAtTheMatch exercised the per-line offset with LF
only, so it would have passed had ScanContent advanced by content length plus one
rather than by the line's own length -- correct for LF and a lone CR, a character
short for CRLF. It is now a theory over all three, and the expected snippet
derives from the terminator length instead of hardcoding 49 padding characters.
Confirmed by making that exact substitution in ScanContent: the CRLF case fails
and the other two pass, which is what the single-Fact version could not see.

The comment above the mirrored filesystem search tests still said that store
carries its own copy of the search loop. It does not: both stores now number
through AgentFileStore.ScanContent, which was one of the goals of this PR, so the
rationale contradicted the code. Reworded as what those tests actually add --
the same rules exercised against real files, where content arrives through a
decoded read rather than an in-memory string.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

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

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

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

dotnet/src/Microsoft.Agents.AI/Harness/FileAccess/FileAccessProvider.cs:522

  • The new tool's approval-group wiring is not covered in the isolated option tests: DisableReadOnlyToolApproval_ReadOnlyToolsNotWrappedAsync and DisableWriteToolApproval_WriteToolsNotWrappedAsync omit ReadLinesToolName. If this argument were accidentally changed to writeRequiresApproval, all current approval tests would still pass, reversing the approval boundary for this read tool. Add AssertRequiresApproval checks for ReadLinesToolName to both tests.
            WrapWithApprovalIfRequired(AIFunctionFactory.Create(this.ReadLinesAsync, new AIFunctionFactoryOptions { Name = ReadLinesToolName, SerializerOptions = serializerOptions }), readOnlyRequiresApproval),

From the suppressed comment on Copilot review 5053203365.

DisableReadOnlyToolApproval_ReadOnlyToolsNotWrapped and its write counterpart
enumerated read, ls and grep on one side and the four store-modifying tools on
the other, but never read_lines. Nothing else discriminated which group it is
wired into: the auto-approval theory covers a different mechanism, and
DisableBothToolApprovals only counts tools. Wiring read_lines to
writeRequiresApproval would therefore have reversed the approval boundary for a
read tool with every test still green.

Both tests now assert it alongside the other read-only tools. Confirmed by making
that substitution in FileAccessProvider: both fail, and the other 2114 tests pass,
which is what makes the gap the finding describes visible rather than theoretical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

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

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

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

dotnet/src/Microsoft.Agents.AI/Harness/FileStore/FileLineEdit.cs:38

  • read_lines returns <number>\t<line>, not the line text by itself. This wording can make callers pass the whole row as expected_line, which will always fail because the editor compares only the actual line text. Clarify that only the text after the first tab should be used; this also avoids implying that read_lines exists when this model is used by FileMemoryProvider.
    [Description("Optional: the text you believe is currently on that line, as reported by grep or read_lines. When supplied, the edit is rejected unless it matches, which catches an out-of-date line number or a file that changed since you looked. The trailing newline is ignored in the comparison.")]

From the suppressed comment on Copilot review 5053896798.

The expected_line description told the caller to use the text "as reported by
grep or read_lines". read_lines prefixes each line with its number and a tab, so
a model following that literally can pass the whole row, which never matches:
the editor compares the line's own text. It now says to give the line text only
and names the prefix it must not include.

read_lines is also the wrong tool to name here at all. FileLineEdit is shared by
FileAccessProvider and FileMemoryProvider, and the memory provider registers no
read_lines tool, so half the callers were pointed at something that does not
exist for them. grep is named instead, which both providers do register.

The XML doc above the property carried the same gap and now states the rule too,
since that is what an implementer reads rather than the tool description.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

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

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

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

dotnet/src/Microsoft.Agents.AI/Harness/FileStore/AgentFileStore.cs:170

  • SearchAsync is case-insensitive, and the base glob matcher is also configured case-insensitively, but this narrowing-hook contract does not tell overrides to preserve case-only candidates. A backend that pushes either hint into a case-sensitive index can therefore under-return and silently lose matches before the base re-scan. Document the comparison semantics explicitly and require widening when the index cannot reproduce them.
    /// <param name="regexPattern">The pattern <see cref="SearchAsync"/> was called with, as a hint.</param>
    /// <param name="globPattern">The optional glob, matched against each file's path relative to <paramref name="directory"/>.</param>

dotnet/src/Microsoft.Agents.AI/Harness/FileStore/FileEditor.cs:95

  • The check requires exact equality after removing terminators, not containment. For example, actual text foobar and expected text foo trigger this message even though the line does contain the expected text, which gives the model a false diagnostic. Say that the line “does not match the expected text” and update the corresponding assertion.
                        $"line_number {edit.LineNumber} does not contain the expected text. " +
                        "Re-read the file to get current line numbers.");

…on a mismatch

From the two suppressed comments on Copilot review 5058964476.

FindMatchingFilesAsync took the regex and the glob as hints without saying how
either is compared. SearchAsync builds its regex with RegexOptions.IgnoreCase and
StorePaths.CreateGlobMatcher uses OrdinalIgnoreCase, so a backend pushing either
hint into a case-sensitive index returns only case-exact candidates and drops
matches before the base ever re-scans. That is the under-return the superset rule
exists to forbid, and nothing in the contract warned against it. Both parameters
now state the comparison and require widening when the backend cannot reproduce it.

The expected_line mismatch said the line "does not contain the expected text"
while the check is exact equality after trimming terminators. For actual foobar
and expected foo the line does contain it, so the model was handed a diagnostic
that contradicts what it can see and would send it re-reading a file that had not
changed. It now says the line does not match, and the contract test that pinned
the old wording follows.

Confirmed the assertion discriminates by putting the old wording back:
ApplyReplaceLines_ExpectedLineDiffering_Throws fails and the other 2115 pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

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

Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.

Comment on lines +112 to +116
public virtual async Task<IReadOnlyList<FileSearchResult>> SearchAsync(string directory, string regexPattern, string? globPattern = null, bool recursive = false, CancellationToken cancellationToken = default)
{
// Compile with a match timeout to guard against catastrophic backtracking (ReDoS).
var regex = new Regex(regexPattern, RegexOptions.IgnoreCase, TimeSpan.FromSeconds(5));
IReadOnlyList<string> names = await this.FindMatchingFilesAsync(directory, regexPattern, globPattern, recursive, cancellationToken).ConfigureAwait(false);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

westey (@westey-m) hi. I need your help here to make the decision whether this needs to be an ADR.

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

Labels

breaking change Usage: [PRs], Target: all PRs that introduce changes that are not backward compatible documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs .NET Usage: [Issues, PRs], Target: .Net

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants