agent.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. import { Config } from "../config/config"
  2. import z from "zod"
  3. import { Provider } from "../provider/provider"
  4. import { generateObject, type ModelMessage } from "ai"
  5. import PROMPT_GENERATE from "./generate.txt"
  6. import { SystemPrompt } from "../session/system"
  7. import { Instance } from "../project/instance"
  8. import { mergeDeep } from "remeda"
  9. export namespace Agent {
  10. export const Info = z
  11. .object({
  12. name: z.string(),
  13. description: z.string().optional(),
  14. mode: z.enum(["subagent", "primary", "all"]),
  15. builtIn: z.boolean(),
  16. topP: z.number().optional(),
  17. temperature: z.number().optional(),
  18. color: z.string().optional(),
  19. permission: z.object({
  20. edit: Config.Permission,
  21. bash: z.record(z.string(), Config.Permission),
  22. webfetch: Config.Permission.optional(),
  23. doom_loop: Config.Permission.optional(),
  24. external_directory: Config.Permission.optional(),
  25. }),
  26. model: z
  27. .object({
  28. modelID: z.string(),
  29. providerID: z.string(),
  30. })
  31. .optional(),
  32. prompt: z.string().optional(),
  33. tools: z.record(z.string(), z.boolean()),
  34. options: z.record(z.string(), z.any()),
  35. })
  36. .meta({
  37. ref: "Agent",
  38. })
  39. export type Info = z.infer<typeof Info>
  40. const state = Instance.state(async () => {
  41. const cfg = await Config.get()
  42. const defaultTools = cfg.tools ?? {}
  43. const defaultPermission: Info["permission"] = {
  44. edit: "allow",
  45. bash: {
  46. "*": "allow",
  47. },
  48. webfetch: "allow",
  49. doom_loop: "ask",
  50. external_directory: "ask",
  51. }
  52. const agentPermission = mergeAgentPermissions(defaultPermission, cfg.permission ?? {})
  53. const planPermission = mergeAgentPermissions(
  54. {
  55. edit: "deny",
  56. bash: {
  57. "cut*": "allow",
  58. "diff*": "allow",
  59. "du*": "allow",
  60. "file *": "allow",
  61. "find * -delete*": "ask",
  62. "find * -exec*": "ask",
  63. "find * -fprint*": "ask",
  64. "find * -fls*": "ask",
  65. "find * -fprintf*": "ask",
  66. "find * -ok*": "ask",
  67. "find *": "allow",
  68. "git diff*": "allow",
  69. "git log*": "allow",
  70. "git show*": "allow",
  71. "git status*": "allow",
  72. "git branch": "allow",
  73. "git branch -v": "allow",
  74. "grep*": "allow",
  75. "head*": "allow",
  76. "less*": "allow",
  77. "ls*": "allow",
  78. "more*": "allow",
  79. "pwd*": "allow",
  80. "rg*": "allow",
  81. "sort --output=*": "ask",
  82. "sort -o *": "ask",
  83. "sort*": "allow",
  84. "stat*": "allow",
  85. "tail*": "allow",
  86. "tree -o *": "ask",
  87. "tree*": "allow",
  88. "uniq*": "allow",
  89. "wc*": "allow",
  90. "whereis*": "allow",
  91. "which*": "allow",
  92. "*": "ask",
  93. },
  94. webfetch: "allow",
  95. },
  96. cfg.permission ?? {},
  97. )
  98. const result: Record<string, Info> = {
  99. general: {
  100. name: "general",
  101. description: `General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel.`,
  102. tools: {
  103. todoread: false,
  104. todowrite: false,
  105. ...defaultTools,
  106. },
  107. options: {},
  108. permission: agentPermission,
  109. mode: "subagent",
  110. builtIn: true,
  111. },
  112. explore: {
  113. name: "explore",
  114. tools: {
  115. todoread: false,
  116. todowrite: false,
  117. edit: false,
  118. write: false,
  119. ...defaultTools,
  120. },
  121. description: `Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.`,
  122. prompt: [
  123. `You are a file search specialist. You excel at thoroughly navigating and exploring codebases.`,
  124. ``,
  125. `Your strengths:`,
  126. `- Rapidly finding files using glob patterns`,
  127. `- Searching code and text with powerful regex patterns`,
  128. `- Reading and analyzing file contents`,
  129. ``,
  130. `Guidelines:`,
  131. `- Use Glob for broad file pattern matching`,
  132. `- Use Grep for searching file contents with regex`,
  133. `- Use Read when you know the specific file path you need to read`,
  134. `- Use Bash for file operations like copying, moving, or listing directory contents`,
  135. `- Adapt your search approach based on the thoroughness level specified by the caller`,
  136. `- Return file paths as absolute paths in your final response`,
  137. `- For clear communication, avoid using emojis`,
  138. `- Do not create any files, or run bash commands that modify the user's system state in any way`,
  139. ``,
  140. `Complete the user's search request efficiently and report your findings clearly.`,
  141. ].join("\n"),
  142. options: {},
  143. permission: agentPermission,
  144. mode: "subagent",
  145. builtIn: true,
  146. },
  147. build: {
  148. name: "build",
  149. tools: { ...defaultTools },
  150. options: {},
  151. permission: agentPermission,
  152. mode: "primary",
  153. builtIn: true,
  154. },
  155. plan: {
  156. name: "plan",
  157. options: {},
  158. permission: planPermission,
  159. tools: {
  160. ...defaultTools,
  161. },
  162. mode: "primary",
  163. builtIn: true,
  164. },
  165. }
  166. for (const [key, value] of Object.entries(cfg.agent ?? {})) {
  167. if (value.disable) {
  168. delete result[key]
  169. continue
  170. }
  171. let item = result[key]
  172. if (!item)
  173. item = result[key] = {
  174. name: key,
  175. mode: "all",
  176. permission: agentPermission,
  177. options: {},
  178. tools: {},
  179. builtIn: false,
  180. }
  181. const { name, model, prompt, tools, description, temperature, top_p, mode, permission, color, ...extra } = value
  182. item.options = {
  183. ...item.options,
  184. ...extra,
  185. }
  186. if (model) item.model = Provider.parseModel(model)
  187. if (prompt) item.prompt = prompt
  188. if (tools)
  189. item.tools = {
  190. ...item.tools,
  191. ...tools,
  192. }
  193. item.tools = {
  194. ...defaultTools,
  195. ...item.tools,
  196. }
  197. if (description) item.description = description
  198. if (temperature != undefined) item.temperature = temperature
  199. if (top_p != undefined) item.topP = top_p
  200. if (mode) item.mode = mode
  201. if (color) item.color = color
  202. // just here for consistency & to prevent it from being added as an option
  203. if (name) item.name = name
  204. if (permission ?? cfg.permission) {
  205. item.permission = mergeAgentPermissions(cfg.permission ?? {}, permission ?? {})
  206. }
  207. }
  208. return result
  209. })
  210. export async function get(agent: string) {
  211. return state().then((x) => x[agent])
  212. }
  213. export async function list() {
  214. return state().then((x) => Object.values(x))
  215. }
  216. export async function generate(input: { description: string }) {
  217. const defaultModel = await Provider.defaultModel()
  218. const model = await Provider.getModel(defaultModel.providerID, defaultModel.modelID)
  219. const system = SystemPrompt.header(defaultModel.providerID)
  220. system.push(PROMPT_GENERATE)
  221. const existing = await list()
  222. const result = await generateObject({
  223. temperature: 0.3,
  224. prompt: [
  225. ...system.map(
  226. (item): ModelMessage => ({
  227. role: "system",
  228. content: item,
  229. }),
  230. ),
  231. {
  232. role: "user",
  233. content: `Create an agent configuration based on this request: \"${input.description}\".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(", ")}\n Return ONLY the JSON object, no other text, do not wrap in backticks`,
  234. },
  235. ],
  236. model: model.language,
  237. schema: z.object({
  238. identifier: z.string(),
  239. whenToUse: z.string(),
  240. systemPrompt: z.string(),
  241. }),
  242. })
  243. return result.object
  244. }
  245. }
  246. function mergeAgentPermissions(basePermission: any, overridePermission: any): Agent.Info["permission"] {
  247. if (typeof basePermission.bash === "string") {
  248. basePermission.bash = {
  249. "*": basePermission.bash,
  250. }
  251. }
  252. if (typeof overridePermission.bash === "string") {
  253. overridePermission.bash = {
  254. "*": overridePermission.bash,
  255. }
  256. }
  257. const merged = mergeDeep(basePermission ?? {}, overridePermission ?? {}) as any
  258. let mergedBash
  259. if (merged.bash) {
  260. if (typeof merged.bash === "string") {
  261. mergedBash = {
  262. "*": merged.bash,
  263. }
  264. } else if (typeof merged.bash === "object") {
  265. mergedBash = mergeDeep(
  266. {
  267. "*": "allow",
  268. },
  269. merged.bash,
  270. )
  271. }
  272. }
  273. const result: Agent.Info["permission"] = {
  274. edit: merged.edit ?? "allow",
  275. webfetch: merged.webfetch ?? "allow",
  276. bash: mergedBash ?? { "*": "allow" },
  277. doom_loop: merged.doom_loop,
  278. external_directory: merged.external_directory,
  279. }
  280. return result
  281. }