instruction-context.ts 3.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. export * as InstructionContext from "./instruction-context"
  2. import { Array, Effect, Layer, Schema } from "effect"
  3. import { isAbsolute, join, relative, sep } from "path"
  4. import { FSUtil } from "./fs-util"
  5. import { Flag } from "./flag/flag"
  6. import { Global } from "./global"
  7. import { Location } from "./location"
  8. import { AbsolutePath } from "./schema"
  9. import { SystemContext } from "./system-context/index"
  10. import { SystemContextRegistry } from "./system-context/registry"
  11. class File extends Schema.Class<File>("InstructionContext.File")({
  12. path: AbsolutePath,
  13. content: Schema.String,
  14. }) {}
  15. const Files = Schema.Array(File)
  16. const key = SystemContext.Key.make("core/instructions")
  17. export const layer = Layer.effectDiscard(
  18. Effect.gen(function* () {
  19. const fs = yield* FSUtil.Service
  20. const global = yield* Global.Service
  21. const location = yield* Location.Service
  22. const registry = yield* SystemContextRegistry.Service
  23. const source = (value: ReadonlyArray<File> | SystemContext.Unavailable) =>
  24. SystemContext.make({
  25. key,
  26. codec: Schema.toCodecJson(Files),
  27. load: Effect.succeed(value),
  28. baseline: render,
  29. update: (_previous, current) =>
  30. `These instructions replace all previously loaded ambient instructions.\n\n${render(current)}`,
  31. removed: () => "Previously loaded instructions no longer apply.",
  32. })
  33. const observe = Effect.fn("InstructionContext.observe")(function* () {
  34. const start = FSUtil.resolve(location.directory)
  35. const stop = FSUtil.resolve(location.project.directory)
  36. const fromProject = relative(stop, start)
  37. const insideProject =
  38. fromProject === "" || (fromProject !== ".." && !fromProject.startsWith(`..${sep}`) && !isAbsolute(fromProject))
  39. const discovered = new Set(
  40. (Flag.OPENCODE_DISABLE_PROJECT_CONFIG || !insideProject
  41. ? []
  42. : yield* fs.up({
  43. targets: ["AGENTS.md"],
  44. start,
  45. stop,
  46. })
  47. ).map(FSUtil.resolve),
  48. )
  49. const paths = Array.dedupe([FSUtil.resolve(join(global.config, "AGENTS.md")), ...discovered])
  50. const files = yield* Effect.forEach(
  51. paths,
  52. (path) =>
  53. fs
  54. .readFileStringSafe(path)
  55. .pipe(
  56. Effect.map((content) =>
  57. content === undefined ? undefined : new File({ path: AbsolutePath.make(path), content }),
  58. ),
  59. ),
  60. { concurrency: "unbounded" },
  61. )
  62. if (files.some((file, index) => file === undefined && discovered.has(paths[index])))
  63. return SystemContext.unavailable
  64. return files.filter((file): file is File => file !== undefined)
  65. })
  66. yield* registry.register({
  67. key,
  68. load: observe().pipe(
  69. Effect.map((files) =>
  70. files === SystemContext.unavailable
  71. ? source(files)
  72. : files.length === 0
  73. ? SystemContext.empty
  74. : source(files),
  75. ),
  76. Effect.catch(() => Effect.succeed(source(SystemContext.unavailable))),
  77. Effect.catchDefect(() => Effect.succeed(source(SystemContext.unavailable))),
  78. ),
  79. })
  80. }),
  81. )
  82. function render(files: ReadonlyArray<File>) {
  83. return files.map((file) => `Instructions from: ${file.path}\n${file.content}`).join("\n\n")
  84. }