Skip to content

Commit bea6e14

Browse files
committed
feat(core): discover instructions on directory reads
1 parent 50a1fe4 commit bea6e14

2 files changed

Lines changed: 73 additions & 10 deletions

File tree

packages/core/src/tool/read.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -85,22 +85,29 @@ const layer = Layer.effectDiscard(
8585
agent: context.agent,
8686
source,
8787
})
88-
if (type === "directory")
89-
return yield* reader.list(absolute, { offset: input.offset, limit: input.limit })
90-
const content = yield* reader.read(absolute, resource, {
91-
offset: input.offset,
92-
limit: input.limit,
93-
})
94-
// After a successful file content read (not directory listings), discover
95-
// nearby AGENTS.md walking up to the Location root exclusive and inject them
96-
// as durable synthetic instructions. Discovery failures never fail the read.
88+
const content =
89+
type === "directory"
90+
? yield* reader.list(absolute, { offset: input.offset, limit: input.limit })
91+
: yield* reader.read(absolute, resource, {
92+
offset: input.offset,
93+
limit: input.limit,
94+
})
95+
// After a successful read, discover nearby AGENTS.md walking up to the Location
96+
// root exclusive and inject them as durable synthetic instructions. For a
97+
// directory listing the walk starts at the directory itself (so its own AGENTS.md
98+
// is discovered); for a file it starts at the file's dirname. External reads are
99+
// skipped, and discovery failures never fail the read.
97100
yield* Effect.gen(function* () {
98101
if (target.externalDirectory !== undefined) return
99102
const resolved = FSUtil.resolve(target.canonical)
100103
const root = FSUtil.resolve(location.directory)
101104
// up() searches its stop directory, so the Location-root AGENTS.md (already
102105
// supplied by the core/instructions baseline) is dropped by the dirname filter.
103-
const discovered = yield* fs.up({ targets: [FILENAME], start: dirname(resolved), stop: root })
106+
const discovered = yield* fs.up({
107+
targets: [FILENAME],
108+
start: type === "directory" ? resolved : dirname(resolved),
109+
stop: root,
110+
})
104111
const candidates = discovered.map(FSUtil.resolve).filter((file) => dirname(file) !== root)
105112
if (candidates.length === 0) return
106113
yield* sessionInstructions.load({ sessionID: context.sessionID, paths: candidates })

packages/core/test/session-instructions.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,62 @@ describe("SessionInstructions", () => {
201201
}),
202202
)
203203

204+
it.effect("discovers AGENTS.md on a directory listing, including the listed directory's own, and dedups with a later file read", () =>
205+
Effect.gen(function* () {
206+
const location = yield* Location.Service
207+
const dir = location.directory
208+
const rootPath = path.resolve(dir, "AGENTS.md")
209+
const pkgPath = path.resolve(dir, "packages", "foo", "AGENTS.md")
210+
yield* mkdir(path.resolve(dir, "packages", "foo"))
211+
yield* writeAgents(rootPath, "root-instructions")
212+
yield* writeAgents(pkgPath, "pkg-instructions")
213+
yield* Effect.promise(() => fs.writeFile(path.resolve(dir, "packages", "foo", "file.txt"), "content"))
214+
215+
const session = yield* SessionV2.Service
216+
const registry = yield* ToolRegistry.Service
217+
const sessionID = (yield* session.create({ location: Location.Ref.make({ directory: dir }) })).id
218+
219+
// Listing packages/foo/ discovers its own AGENTS.md, walking up to but excluding
220+
// the Location root (already supplied by the core/instructions baseline).
221+
yield* settleTool(registry, readCall(sessionID, "call-list", "packages/foo"))
222+
223+
const firstInjected = yield* synthetics(sessionID)
224+
expect(firstInjected).toHaveLength(1)
225+
expect(firstInjected[0]!.text).toBe(`Instructions from: ${pkgPath}\npkg-instructions`)
226+
expect(firstInjected[0]!.description).toBe(`Loaded ${path.relative(dir, pkgPath)}`)
227+
expect(firstInjected[0]!.metadata).toEqual({ instruction: { paths: [pkgPath] } })
228+
expect(firstInjected[0]!.text).not.toContain("root-instructions")
229+
230+
// A subsequent file read under the listed directory is a dedup: pkg's AGENTS.md is
231+
// already injected for this session, so nothing new is emitted.
232+
yield* settleTool(registry, readCall(sessionID, "call-file", "packages/foo/file.txt"))
233+
234+
expect((yield* synthetics(sessionID))).toHaveLength(1)
235+
}),
236+
)
237+
238+
it.effect("listing the Location root directory injects no instructions", () =>
239+
Effect.gen(function* () {
240+
const location = yield* Location.Service
241+
const dir = location.directory
242+
const rootPath = path.resolve(dir, "AGENTS.md")
243+
const subPath = path.resolve(dir, "sub", "AGENTS.md")
244+
yield* mkdir(path.resolve(dir, "sub"))
245+
yield* writeAgents(rootPath, "root-instructions")
246+
yield* writeAgents(subPath, "sub-instructions")
247+
248+
const session = yield* SessionV2.Service
249+
const registry = yield* ToolRegistry.Service
250+
const sessionID = (yield* session.create({ location: Location.Ref.make({ directory: dir }) })).id
251+
252+
// The walk starts and stops at the Location root: the root AGENTS.md is searched but
253+
// dropped by the dirname filter, and up() only walks upward so nested dirs are unseen.
254+
yield* settleTool(registry, readCall(sessionID, "call-root-list", "."))
255+
256+
expect((yield* synthetics(sessionID))).toHaveLength(0)
257+
}),
258+
)
259+
204260
it.effect("loads instructions directly without a read", () =>
205261
Effect.gen(function* () {
206262
const location = yield* Location.Service

0 commit comments

Comments
 (0)