instruction-context.ts 3.5 KB

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