376 lines
12 KiB
TypeScript
376 lines
12 KiB
TypeScript
import { Config } from "../config/config"
|
||
import z from "zod"
|
||
import { Provider } from "../provider/provider"
|
||
import { generateObject, type ModelMessage } from "ai"
|
||
import { SystemPrompt } from "../session/system"
|
||
import { Instance } from "../project/instance"
|
||
import { mergeDeep } from "remeda"
|
||
|
||
import PROMPT_GENERATE from "./generate.txt"
|
||
import PROMPT_COMPACTION from "./prompt/compaction.txt"
|
||
|
||
export namespace Agent {
|
||
export const Info = z
|
||
.object({
|
||
name: z.string(),
|
||
description: z.string().optional(),
|
||
mode: z.enum(["subagent", "primary", "all"]),
|
||
native: z.boolean().optional(),
|
||
hidden: z.boolean().optional(),
|
||
topP: z.number().optional(),
|
||
temperature: z.number().optional(),
|
||
color: z.string().optional(),
|
||
permission: z.object({
|
||
edit: Config.Permission,
|
||
bash: z.record(z.string(), Config.Permission),
|
||
webfetch: Config.Permission.optional(),
|
||
doom_loop: Config.Permission.optional(),
|
||
external_directory: Config.Permission.optional(),
|
||
}),
|
||
model: z
|
||
.object({
|
||
modelID: z.string(),
|
||
providerID: z.string(),
|
||
})
|
||
.optional(),
|
||
prompt: z.string().optional(),
|
||
tools: z.record(z.string(), z.boolean()),
|
||
options: z.record(z.string(), z.any()),
|
||
maxSteps: z.number().int().positive().optional(),
|
||
})
|
||
.meta({
|
||
ref: "Agent",
|
||
})
|
||
export type Info = z.infer<typeof Info>
|
||
|
||
const state = Instance.state(async () => {
|
||
const cfg = await Config.get()
|
||
const defaultTools = cfg.tools ?? {}
|
||
const defaultPermission: Info["permission"] = {
|
||
edit: "allow",
|
||
bash: {
|
||
"*": "allow",
|
||
},
|
||
webfetch: "allow",
|
||
doom_loop: "ask",
|
||
external_directory: "ask",
|
||
}
|
||
const agentPermission = mergeAgentPermissions(defaultPermission, cfg.permission ?? {})
|
||
|
||
const planPermission = mergeAgentPermissions(
|
||
{
|
||
edit: "deny",
|
||
bash: {
|
||
"cut*": "allow",
|
||
"diff*": "allow",
|
||
"du*": "allow",
|
||
"file *": "allow",
|
||
"find * -delete*": "ask",
|
||
"find * -exec*": "ask",
|
||
"find * -fprint*": "ask",
|
||
"find * -fls*": "ask",
|
||
"find * -fprintf*": "ask",
|
||
"find * -ok*": "ask",
|
||
"find *": "allow",
|
||
"git diff*": "allow",
|
||
"git log*": "allow",
|
||
"git show*": "allow",
|
||
"git status*": "allow",
|
||
"git branch": "allow",
|
||
"git branch -v": "allow",
|
||
"grep*": "allow",
|
||
"head*": "allow",
|
||
"less*": "allow",
|
||
"ls*": "allow",
|
||
"more*": "allow",
|
||
"pwd*": "allow",
|
||
"rg*": "allow",
|
||
"sort --output=*": "ask",
|
||
"sort -o *": "ask",
|
||
"sort*": "allow",
|
||
"stat*": "allow",
|
||
"tail*": "allow",
|
||
"tree -o *": "ask",
|
||
"tree*": "allow",
|
||
"uniq*": "allow",
|
||
"wc*": "allow",
|
||
"whereis*": "allow",
|
||
"which*": "allow",
|
||
"*": "ask",
|
||
},
|
||
webfetch: "allow",
|
||
},
|
||
cfg.permission ?? {},
|
||
)
|
||
|
||
const result: Record<string, Info> = {
|
||
general: {
|
||
name: "general",
|
||
description: `General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel.`,
|
||
tools: {
|
||
todoread: false,
|
||
todowrite: false,
|
||
...defaultTools,
|
||
},
|
||
options: {},
|
||
permission: agentPermission,
|
||
mode: "subagent",
|
||
native: true,
|
||
hidden: true,
|
||
},
|
||
explore: {
|
||
name: "explore",
|
||
tools: {
|
||
todoread: false,
|
||
todowrite: false,
|
||
edit: false,
|
||
write: false,
|
||
...defaultTools,
|
||
},
|
||
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.`,
|
||
prompt: [
|
||
`You are a file search specialist. You excel at thoroughly navigating and exploring codebases.`,
|
||
``,
|
||
`Your strengths:`,
|
||
`- Rapidly finding files using glob patterns`,
|
||
`- Searching code and text with powerful regex patterns`,
|
||
`- Reading and analyzing file contents`,
|
||
``,
|
||
`Guidelines:`,
|
||
`- Use Glob for broad file pattern matching`,
|
||
`- Use Grep for searching file contents with regex`,
|
||
`- Use Read when you know the specific file path you need to read`,
|
||
`- Use Bash for file operations like copying, moving, or listing directory contents`,
|
||
`- Adapt your search approach based on the thoroughness level specified by the caller`,
|
||
`- Return file paths as absolute paths in your final response`,
|
||
`- For clear communication, avoid using emojis`,
|
||
`- Do not create any files, or run bash commands that modify the user's system state in any way`,
|
||
``,
|
||
`Complete the user's search request efficiently and report your findings clearly.`,
|
||
].join("\n"),
|
||
options: {},
|
||
permission: agentPermission,
|
||
mode: "subagent",
|
||
native: true,
|
||
},
|
||
compaction: {
|
||
name: "compaction",
|
||
mode: "primary",
|
||
native: true,
|
||
hidden: true,
|
||
prompt: PROMPT_COMPACTION,
|
||
tools: {
|
||
"*": false,
|
||
},
|
||
options: {},
|
||
permission: agentPermission,
|
||
},
|
||
build: {
|
||
name: "build",
|
||
tools: { ...defaultTools },
|
||
options: {},
|
||
permission: agentPermission,
|
||
mode: "primary",
|
||
native: true,
|
||
},
|
||
summary: {
|
||
name: "summary",
|
||
mode: "subagent",
|
||
options: {},
|
||
native: true,
|
||
permission: agentPermission,
|
||
prompt: `You are a title generator. You output ONLY a thread title. Nothing else.
|
||
|
||
<task>
|
||
Generate a brief title that would help the user find this conversation later.
|
||
|
||
Follow all rules in <rules>
|
||
Use the <examples> so you know what a good title looks like.
|
||
Your output must be:
|
||
- A single line
|
||
- ≤50 characters
|
||
- No explanations
|
||
</task>
|
||
|
||
<rules>
|
||
- Focus on the main topic or question the user needs to retrieve
|
||
- Use -ing verbs for actions (Debugging, Implementing, Analyzing)
|
||
- Keep exact: technical terms, numbers, filenames, HTTP codes
|
||
- Remove: the, this, my, a, an
|
||
- Never assume tech stack
|
||
- Never use tools
|
||
- NEVER respond to questions, just generate a title for the conversation
|
||
- The title should NEVER include "summarizing" or "generating" when generating a title
|
||
- DO NOT SAY YOU CANNOT GENERATE A TITLE OR COMPLAIN ABOUT THE INPUT
|
||
- Always output something meaningful, even if the input is minimal.
|
||
- If the user message is short or conversational (e.g. “hello”, “lol”, “whats up”, “hey”):
|
||
→ create a title that reflects the user’s tone or intent (such as Greeting, Quick check-in, Light chat, Intro message, etc.)
|
||
</rules>
|
||
|
||
<examples>
|
||
"hey" -> Greeting
|
||
"debug 500 errors in production" → Debugging production 500 errors
|
||
"refactor user service" → Refactoring user service
|
||
"why is app.js failing" → Analyzing app.js failure
|
||
"implement rate limiting" → Implementing rate limiting
|
||
"how do I connect postgres to my API" → Connecting Postgres to API
|
||
"best practices for React hooks" → React hooks best practices
|
||
</examples>`,
|
||
tools: {},
|
||
},
|
||
plan: {
|
||
name: "plan",
|
||
options: {},
|
||
permission: planPermission,
|
||
tools: {
|
||
...defaultTools,
|
||
},
|
||
mode: "primary",
|
||
native: true,
|
||
},
|
||
}
|
||
for (const [key, value] of Object.entries(cfg.agent ?? {})) {
|
||
if (value.disable) {
|
||
delete result[key]
|
||
continue
|
||
}
|
||
let item = result[key]
|
||
if (!item)
|
||
item = result[key] = {
|
||
name: key,
|
||
mode: "all",
|
||
permission: agentPermission,
|
||
options: {},
|
||
tools: {},
|
||
native: false,
|
||
}
|
||
const {
|
||
name,
|
||
model,
|
||
prompt,
|
||
tools,
|
||
description,
|
||
temperature,
|
||
top_p,
|
||
mode,
|
||
permission,
|
||
color,
|
||
maxSteps,
|
||
...extra
|
||
} = value
|
||
item.options = {
|
||
...item.options,
|
||
...extra,
|
||
}
|
||
if (model) item.model = Provider.parseModel(model)
|
||
if (prompt) item.prompt = prompt
|
||
if (tools)
|
||
item.tools = {
|
||
...item.tools,
|
||
...tools,
|
||
}
|
||
item.tools = {
|
||
...defaultTools,
|
||
...item.tools,
|
||
}
|
||
if (description) item.description = description
|
||
if (temperature != undefined) item.temperature = temperature
|
||
if (top_p != undefined) item.topP = top_p
|
||
if (mode) item.mode = mode
|
||
if (color) item.color = color
|
||
// just here for consistency & to prevent it from being added as an option
|
||
if (name) item.name = name
|
||
if (maxSteps != undefined) item.maxSteps = maxSteps
|
||
|
||
if (permission ?? cfg.permission) {
|
||
item.permission = mergeAgentPermissions(cfg.permission ?? {}, permission ?? {})
|
||
}
|
||
}
|
||
return result
|
||
})
|
||
|
||
export async function get(agent: string) {
|
||
return state().then((x) => x[agent])
|
||
}
|
||
|
||
export async function list() {
|
||
return state().then((x) => Object.values(x))
|
||
}
|
||
|
||
export async function generate(input: { description: string }) {
|
||
const cfg = await Config.get()
|
||
const defaultModel = await Provider.defaultModel()
|
||
const model = await Provider.getModel(defaultModel.providerID, defaultModel.modelID)
|
||
const language = await Provider.getLanguage(model)
|
||
const system = SystemPrompt.header(defaultModel.providerID)
|
||
system.push(PROMPT_GENERATE)
|
||
const existing = await list()
|
||
const result = await generateObject({
|
||
experimental_telemetry: {
|
||
isEnabled: cfg.experimental?.openTelemetry,
|
||
metadata: {
|
||
userId: cfg.username ?? "unknown",
|
||
},
|
||
},
|
||
temperature: 0.3,
|
||
messages: [
|
||
...system.map(
|
||
(item): ModelMessage => ({
|
||
role: "system",
|
||
content: item,
|
||
}),
|
||
),
|
||
{
|
||
role: "user",
|
||
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`,
|
||
},
|
||
],
|
||
model: language,
|
||
schema: z.object({
|
||
identifier: z.string(),
|
||
whenToUse: z.string(),
|
||
systemPrompt: z.string(),
|
||
}),
|
||
})
|
||
return result.object
|
||
}
|
||
}
|
||
|
||
function mergeAgentPermissions(basePermission: any, overridePermission: any): Agent.Info["permission"] {
|
||
if (typeof basePermission.bash === "string") {
|
||
basePermission.bash = {
|
||
"*": basePermission.bash,
|
||
}
|
||
}
|
||
if (typeof overridePermission.bash === "string") {
|
||
overridePermission.bash = {
|
||
"*": overridePermission.bash,
|
||
}
|
||
}
|
||
const merged = mergeDeep(basePermission ?? {}, overridePermission ?? {}) as any
|
||
let mergedBash
|
||
if (merged.bash) {
|
||
if (typeof merged.bash === "string") {
|
||
mergedBash = {
|
||
"*": merged.bash,
|
||
}
|
||
} else if (typeof merged.bash === "object") {
|
||
mergedBash = mergeDeep(
|
||
{
|
||
"*": "allow",
|
||
},
|
||
merged.bash,
|
||
)
|
||
}
|
||
}
|
||
|
||
const result: Agent.Info["permission"] = {
|
||
edit: merged.edit ?? "allow",
|
||
webfetch: merged.webfetch ?? "allow",
|
||
bash: mergedBash ?? { "*": "allow" },
|
||
doom_loop: merged.doom_loop,
|
||
external_directory: merged.external_directory,
|
||
}
|
||
|
||
return result
|
||
}
|