agent.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. export * as ConfigAgentPlugin from "./agent"
  2. import path from "path"
  3. import { Effect, Option, Schema } from "effect"
  4. import { AgentV2 } from "../../agent"
  5. import { Config } from "../../config"
  6. import { ConfigAgent } from "../agent"
  7. import { ConfigMarkdown } from "../markdown"
  8. import { FSUtil } from "../../fs-util"
  9. import { ModelV2 } from "../../model"
  10. import { PluginV2 } from "../../plugin"
  11. import { ConfigAgentV1 } from "../../v1/config/agent"
  12. import { ConfigMigrateV1 } from "../../v1/config/migrate"
  13. const legacySources = [
  14. { pattern: "{agent,agents}/**/*.md", primary: false },
  15. { pattern: "{mode,modes}/*.md", primary: true },
  16. ] as const
  17. const decodeAgent = Schema.decodeUnknownOption(ConfigAgent.Info)
  18. const decodeLegacyAgent = Schema.decodeUnknownOption(ConfigAgentV1.Info)
  19. const decodeConfig = Schema.decodeUnknownOption(Config.Info)
  20. const agentKeys = new Set([
  21. "model",
  22. "variant",
  23. "request",
  24. "system",
  25. "description",
  26. "mode",
  27. "hidden",
  28. "color",
  29. "steps",
  30. "disabled",
  31. "permissions",
  32. ])
  33. export const Plugin = PluginV2.define({
  34. id: PluginV2.ID.make("config-agent"),
  35. effect: Effect.gen(function* () {
  36. const agent = yield* AgentV2.Service
  37. const config = yield* Config.Service
  38. const fs = yield* FSUtil.Service
  39. const documents = yield* Effect.forEach(yield* config.entries(), (entry) => {
  40. if (entry.type === "document") return Effect.succeed([entry])
  41. return Effect.gen(function* () {
  42. const files = yield* discover(fs, entry.path)
  43. return yield* Effect.forEach(files, (file) =>
  44. fs.readFileStringSafe(file.filepath).pipe(
  45. Effect.map((content) => content && decode(file, content)),
  46. Effect.catch(() => Effect.succeed(undefined)),
  47. ),
  48. ).pipe(
  49. Effect.map((documents) =>
  50. documents.filter((document): document is Config.Document => document !== undefined),
  51. ),
  52. )
  53. })
  54. }).pipe(Effect.map((documents) => documents.flat()))
  55. yield* agent.update((editor) => {
  56. const global = documents.flatMap((document) => document.info.permissions ?? [])
  57. const configuredDefault = Config.latest(documents, "default_agent")
  58. if (configuredDefault !== undefined) editor.default(AgentV2.ID.make(configuredDefault))
  59. for (const current of editor.list()) {
  60. editor.update(current.id, (agent) => agent.permissions.push(...global))
  61. }
  62. for (const document of documents) {
  63. for (const [id, item] of Object.entries(document.info.agents ?? {})) {
  64. const agentID = AgentV2.ID.make(id)
  65. if (item.disabled) {
  66. editor.remove(agentID)
  67. continue
  68. }
  69. const exists = editor.get(agentID) !== undefined
  70. editor.update(agentID, (agent) => {
  71. if (!exists) agent.permissions.push(...global)
  72. if (item.model !== undefined) {
  73. const model = ModelV2.parse(item.model)
  74. agent.model = { id: model.modelID, providerID: model.providerID, variant: agent.model?.variant }
  75. }
  76. if (item.variant !== undefined && agent.model !== undefined) {
  77. agent.model.variant = ModelV2.VariantID.make(item.variant)
  78. }
  79. if (item.request !== undefined) {
  80. Object.assign(agent.request.headers, item.request.headers ?? {})
  81. Object.assign(agent.request.body, item.request.body ?? {})
  82. }
  83. if (item.system !== undefined) agent.system = item.system
  84. if (item.description !== undefined) agent.description = item.description
  85. if (item.mode !== undefined) agent.mode = item.mode
  86. if (item.hidden !== undefined) agent.hidden = item.hidden
  87. if (item.color !== undefined) agent.color = item.color
  88. if (item.steps !== undefined) agent.steps = item.steps
  89. if (item.permissions !== undefined) agent.permissions.push(...item.permissions)
  90. })
  91. }
  92. }
  93. })
  94. }),
  95. })
  96. function discover(fs: FSUtil.Interface, directory: string) {
  97. return Effect.forEach(legacySources, (source) =>
  98. fs
  99. .glob(source.pattern, { cwd: directory, absolute: true, dot: true, symlink: true })
  100. .pipe(
  101. Effect.map((files) => files.toSorted().map((filepath) => ({ directory, filepath, primary: source.primary }))),
  102. ),
  103. ).pipe(
  104. Effect.map((files) => files.flat()),
  105. Effect.catch(() => Effect.succeed([])),
  106. )
  107. }
  108. function decode(file: { directory: string; filepath: string; primary: boolean }, content: string) {
  109. const markdown = ConfigMarkdown.parseOption(content)
  110. if (!markdown) return
  111. const name = path
  112. .relative(file.directory, file.filepath)
  113. .replaceAll("\\", "/")
  114. .replace(/^(agent|agents|mode|modes)\//, "")
  115. .replace(/\.md$/, "")
  116. const body = markdown.content.trim()
  117. const legacy = Object.keys(markdown.data).some((key) => !agentKeys.has(key))
  118. const agent = Option.getOrUndefined(
  119. legacy
  120. ? Option.map(
  121. decodeLegacyAgent({ name, ...markdown.data, prompt: body }, { errors: "all", propertyOrder: "original" }),
  122. ConfigMigrateV1.migrateAgent,
  123. )
  124. : decodeAgent({ ...markdown.data, system: body }, { errors: "all", propertyOrder: "original" }),
  125. )
  126. if (!agent) return
  127. const info = Option.getOrUndefined(
  128. decodeConfig({
  129. agents: { [name]: file.primary ? { ...agent, mode: "primary" } : agent },
  130. }),
  131. )
  132. if (!info) return
  133. return new Config.Document({ type: "document", path: file.filepath, info })
  134. }