1
0
Эх сурвалжийг харах

feat(core): discover instructions on directory reads

Dax Raad 1 сар өмнө
parent
commit
bea6e1499d

+ 17 - 10
packages/core/src/tool/read.ts

@@ -85,22 +85,29 @@ const layer = Layer.effectDiscard(
                 agent: context.agent,
                 source,
               })
-              if (type === "directory")
-                return yield* reader.list(absolute, { offset: input.offset, limit: input.limit })
-              const content = yield* reader.read(absolute, resource, {
-                offset: input.offset,
-                limit: input.limit,
-              })
-              // After a successful file content read (not directory listings), discover
-              // nearby AGENTS.md walking up to the Location root exclusive and inject them
-              // as durable synthetic instructions. Discovery failures never fail the read.
+              const content =
+                type === "directory"
+                  ? yield* reader.list(absolute, { offset: input.offset, limit: input.limit })
+                  : yield* reader.read(absolute, resource, {
+                      offset: input.offset,
+                      limit: input.limit,
+                    })
+              // After a successful read, discover nearby AGENTS.md walking up to the Location
+              // root exclusive and inject them as durable synthetic instructions. For a
+              // directory listing the walk starts at the directory itself (so its own AGENTS.md
+              // is discovered); for a file it starts at the file's dirname. External reads are
+              // skipped, and discovery failures never fail the read.
               yield* Effect.gen(function* () {
                 if (target.externalDirectory !== undefined) return
                 const resolved = FSUtil.resolve(target.canonical)
                 const root = FSUtil.resolve(location.directory)
                 // up() searches its stop directory, so the Location-root AGENTS.md (already
                 // supplied by the core/instructions baseline) is dropped by the dirname filter.
-                const discovered = yield* fs.up({ targets: [FILENAME], start: dirname(resolved), stop: root })
+                const discovered = yield* fs.up({
+                  targets: [FILENAME],
+                  start: type === "directory" ? resolved : dirname(resolved),
+                  stop: root,
+                })
                 const candidates = discovered.map(FSUtil.resolve).filter((file) => dirname(file) !== root)
                 if (candidates.length === 0) return
                 yield* sessionInstructions.load({ sessionID: context.sessionID, paths: candidates })

+ 56 - 0
packages/core/test/session-instructions.test.ts

@@ -201,6 +201,62 @@ describe("SessionInstructions", () => {
     }),
   )
 
+  it.effect("discovers AGENTS.md on a directory listing, including the listed directory's own, and dedups with a later file read", () =>
+    Effect.gen(function* () {
+      const location = yield* Location.Service
+      const dir = location.directory
+      const rootPath = path.resolve(dir, "AGENTS.md")
+      const pkgPath = path.resolve(dir, "packages", "foo", "AGENTS.md")
+      yield* mkdir(path.resolve(dir, "packages", "foo"))
+      yield* writeAgents(rootPath, "root-instructions")
+      yield* writeAgents(pkgPath, "pkg-instructions")
+      yield* Effect.promise(() => fs.writeFile(path.resolve(dir, "packages", "foo", "file.txt"), "content"))
+
+      const session = yield* SessionV2.Service
+      const registry = yield* ToolRegistry.Service
+      const sessionID = (yield* session.create({ location: Location.Ref.make({ directory: dir }) })).id
+
+      // Listing packages/foo/ discovers its own AGENTS.md, walking up to but excluding
+      // the Location root (already supplied by the core/instructions baseline).
+      yield* settleTool(registry, readCall(sessionID, "call-list", "packages/foo"))
+
+      const firstInjected = yield* synthetics(sessionID)
+      expect(firstInjected).toHaveLength(1)
+      expect(firstInjected[0]!.text).toBe(`Instructions from: ${pkgPath}\npkg-instructions`)
+      expect(firstInjected[0]!.description).toBe(`Loaded ${path.relative(dir, pkgPath)}`)
+      expect(firstInjected[0]!.metadata).toEqual({ instruction: { paths: [pkgPath] } })
+      expect(firstInjected[0]!.text).not.toContain("root-instructions")
+
+      // A subsequent file read under the listed directory is a dedup: pkg's AGENTS.md is
+      // already injected for this session, so nothing new is emitted.
+      yield* settleTool(registry, readCall(sessionID, "call-file", "packages/foo/file.txt"))
+
+      expect((yield* synthetics(sessionID))).toHaveLength(1)
+    }),
+  )
+
+  it.effect("listing the Location root directory injects no instructions", () =>
+    Effect.gen(function* () {
+      const location = yield* Location.Service
+      const dir = location.directory
+      const rootPath = path.resolve(dir, "AGENTS.md")
+      const subPath = path.resolve(dir, "sub", "AGENTS.md")
+      yield* mkdir(path.resolve(dir, "sub"))
+      yield* writeAgents(rootPath, "root-instructions")
+      yield* writeAgents(subPath, "sub-instructions")
+
+      const session = yield* SessionV2.Service
+      const registry = yield* ToolRegistry.Service
+      const sessionID = (yield* session.create({ location: Location.Ref.make({ directory: dir }) })).id
+
+      // The walk starts and stops at the Location root: the root AGENTS.md is searched but
+      // dropped by the dirname filter, and up() only walks upward so nested dirs are unseen.
+      yield* settleTool(registry, readCall(sessionID, "call-root-list", "."))
+
+      expect((yield* synthetics(sessionID))).toHaveLength(0)
+    }),
+  )
+
   it.effect("loads instructions directly without a read", () =>
     Effect.gen(function* () {
       const location = yield* Location.Service