feat(coding-agent/context): added nested AGENTS.md context injection on file reads#2668
feat(coding-agent/context): added nested AGENTS.md context injection on file reads#2668metaphorics wants to merge 1 commit into
Conversation
…on file reads - Added a pure context/nested-agents-md module (root-to-leaf ancestor walk, realpath strict-containment, per-session injection cache, byte-budgeted UTF-8 truncation, directory-context formatting) ported and omp-adapted from senpi. - Wired it into the read tool: a successful local file read appends the content of AGENTS.md files in directories strictly between the project root and the file, once per directory per session. The repo-root AGENTS.md is never injected (already in the system prompt) and raw reads are skipped. - Added nestedAgents.enabled (UI toggle, default on) plus config-only nestedAgents.maxBytesPerFile/maxBytesPerRead budgets; documented in the read tool description and settings docs.
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds support for injecting nested AGENTS.md directory context into successful read results (once per directory per session), with settings + documentation and a new context-injection module.
Changes:
- Inject ancestor
AGENTS.mdcontent on non-raw file reads with per-session caching and byte budgets. - Introduce
nested-agents-mdcontext module (containment, discovery, truncation, formatting, injection cache) plus tests. - Add settings (
nestedAgents.*) and document the new behavior in prompts/docs/changelog.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/coding-agent/src/tools/read.ts | Hooks nested AGENTS.md injection into file read results with per-session cache |
| packages/coding-agent/src/prompts/tools/read.md | Documents nested context injection behavior for the read tool |
| packages/coding-agent/src/context/nested-agents-md/* | Implements discovery/injection/truncation/cache + tests for nested AGENTS.md |
| packages/coding-agent/src/config/settings-schema.ts | Adds nestedAgents.enabled and byte-budget settings |
| packages/coding-agent/CHANGELOG.md | Notes the new nested context injection feature |
| docs/settings.md | Mentions the new nestedAgents.* settings group |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (!isRawSelector(parsed) && this.session.settings.get("nestedAgents.enabled") !== false) { | ||
| const injection = await injectDirectoryContext({ | ||
| filePath: absolutePath, | ||
| rootDir: this.session.cwd, | ||
| cache: getNestedAgentsCache(this.session), | ||
| sessionKey: NESTED_AGENTS_SESSION_KEY, | ||
| config: { | ||
| maxBytesPerFile: this.session.settings.get("nestedAgents.maxBytesPerFile"), | ||
| maxBytesPerRead: this.session.settings.get("nestedAgents.maxBytesPerRead"), | ||
| }, | ||
| }); | ||
| if (injection.injectedText) { | ||
| const firstText = content.find((c): c is TextContent => c.type === "text"); | ||
| if (firstText) { | ||
| firstText.text += injection.injectedText; | ||
| } else { | ||
| content.push({ type: "text", text: injection.injectedText }); | ||
| } | ||
| } | ||
| } |
| let injectedText = ""; | ||
| let bytesBudget = config.maxBytesPerRead; | ||
|
|
||
| for (const agentsPath of candidates) { | ||
| const agentsDir = path.dirname(agentsPath); | ||
| if (input.cache.hasInjected(input.sessionKey, agentsDir)) continue; | ||
| if (bytesBudget <= 0) break; | ||
|
|
||
| let content: string; | ||
| try { | ||
| content = await Bun.file(agentsPath).text(); | ||
| } catch (error) { | ||
| errors.push({ | ||
| path: agentsPath, | ||
| error: new InjectionFileReadError(agentsPath, error), | ||
| }); | ||
| continue; | ||
| } | ||
|
|
||
| const perFileCap = Math.min(config.maxBytesPerFile, bytesBudget); | ||
| const truncated = truncateBytes(content, perFileCap); | ||
|
|
| injectedText += formatDirectoryContext({ | ||
| absolutePath: agentsPath, | ||
| content: truncated.result, | ||
| truncated: truncated.truncated, | ||
| }); | ||
| input.cache.markInjected(input.sessionKey, agentsDir); | ||
| bytesBudget -= truncated.resultBytes; | ||
| } |
| const decoder = new TextDecoder("utf-8", { fatal: false }); | ||
| let decoded = decoder.decode(bytes.subarray(0, maxBytes)); | ||
| while (decoded.endsWith(REPLACEMENT_CHAR)) { | ||
| decoded = decoded.slice(0, -1); | ||
| } |
| let content: string; | ||
| try { | ||
| content = await Bun.file(agentsPath).text(); | ||
| } catch (error) { |
| constructor(path: string, cause: unknown) { | ||
| const message = cause instanceof Error ? cause.message : String(cause); | ||
| super(`Failed to read ${path}: ${message}`); | ||
| this.name = "InjectionFileReadError"; |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7796873474
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // and the file) once per session. Skips raw reads — verbatim output must stay | ||
| // clean — and the repo-root AGENTS.md, which findAgentsMdUp excludes (the | ||
| // system prompt already carries it). | ||
| if (!isRawSelector(parsed) && this.session.settings.get("nestedAgents.enabled") !== false) { |
There was a problem hiding this comment.
Move nested-context injection before bridge returns
When an ACP client advertises fs.readTextFile, plain and multi-range local file reads return the in-memory bridge result before reaching this new injection block, so nestedAgents.enabled has no effect in those sessions. That skips scoped AGENTS.md instructions for the ACP path that is explicitly treated as the source of truth for open editor buffers; apply the injection before returning bridge-backed local file results or centralize it for all local sourcePath results.
Useful? React with 👍 / 👎.
| @@ -0,0 +1,34 @@ | |||
| import { realpath } from "node:fs/promises"; | |||
There was a problem hiding this comment.
Use a namespace import for fs/promises
AGENTS.md for this repo requires namespace imports for Node filesystem modules, including node:fs/promises, but this new file imports realpath as a named binding. Please switch to import * as fs from "node:fs/promises" and call fs.realpath(...) to keep the package within the documented convention.
Useful? React with 👍 / 👎.
| for (const agentsPath of candidates) { | ||
| const agentsDir = path.dirname(agentsPath); | ||
| if (input.cache.hasInjected(input.sessionKey, agentsDir)) continue; | ||
| if (bytesBudget <= 0) break; |
There was a problem hiding this comment.
Preserve deepest AGENTS.md under budget
Because candidates are processed root-to-leaf and the loop stops once the per-read byte budget is exhausted, a large or merely numerous set of higher-level AGENTS.md files can prevent the most specific child directory's instructions from being injected on the first read of a file in that child. Since nested instructions are the ones that actually govern that subtree, reserve budget for deeper files or prioritize the leaf contexts so local rules are not omitted.
Useful? React with 👍 / 👎.
Summary
Adds a pure ancestor-AGENTS.md injection module (root-to-leaf walk, realpath strict-containment, per-session dedup cache, byte-budgeted truncation, directory-context formatting) wired into the read tool: a successful local file read appends the content of
AGENTS.mdfiles in directories strictly between the project root and the file, once per directory per session. The repo-root AGENTS.md is never injected and raw reads are skipped. Gated bynestedAgents.enabled.Tests
Pure up-walk / containment / dedup / truncate / format regression test;
bun checkgreen.Note: touches
config/settings-schema.tsanddocs/settings.mdalongside #2655 and #2661; rebase if flagged.Closes #2660