mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-03-30 05:43:55 +00:00
299 lines
9.3 KiB
TypeScript
299 lines
9.3 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 { Truncate } from "../tool/truncation"
|
|
|
|
import PROMPT_GENERATE from "./generate.txt"
|
|
import PROMPT_COMPACTION from "./prompt/compaction.txt"
|
|
import PROMPT_EXPLORE from "./prompt/explore.txt"
|
|
import PROMPT_SUMMARY from "./prompt/summary.txt"
|
|
import PROMPT_TITLE from "./prompt/title.txt"
|
|
import { PermissionNext } from "@/permission/next"
|
|
import { mergeDeep, pipe, sortBy, values } from "remeda"
|
|
import { Global } from "@/global"
|
|
import path from "path"
|
|
|
|
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: PermissionNext.Ruleset,
|
|
model: z
|
|
.object({
|
|
modelID: z.string(),
|
|
providerID: z.string(),
|
|
})
|
|
.optional(),
|
|
prompt: z.string().optional(),
|
|
options: z.record(z.string(), z.any()),
|
|
steps: 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 defaults = PermissionNext.fromConfig({
|
|
"*": "allow",
|
|
doom_loop: "ask",
|
|
external_directory: {
|
|
"*": "ask",
|
|
[Truncate.DIR]: "allow",
|
|
[Truncate.GLOB]: "allow",
|
|
},
|
|
question: "deny",
|
|
plan_enter: "deny",
|
|
plan_exit: "deny",
|
|
// mirrors github.com/github/gitignore Node.gitignore pattern for .env files
|
|
read: {
|
|
"*": "allow",
|
|
"*.env": "ask",
|
|
"*.env.*": "ask",
|
|
"*.env.example": "allow",
|
|
},
|
|
})
|
|
const user = PermissionNext.fromConfig(cfg.permission ?? {})
|
|
|
|
const result: Record<string, Info> = {
|
|
build: {
|
|
name: "build",
|
|
options: {},
|
|
permission: PermissionNext.merge(
|
|
defaults,
|
|
PermissionNext.fromConfig({
|
|
question: "allow",
|
|
plan_enter: "allow",
|
|
}),
|
|
user,
|
|
),
|
|
mode: "primary",
|
|
native: true,
|
|
},
|
|
plan: {
|
|
name: "plan",
|
|
options: {},
|
|
permission: PermissionNext.merge(
|
|
defaults,
|
|
PermissionNext.fromConfig({
|
|
question: "allow",
|
|
plan_exit: "allow",
|
|
external_directory: {
|
|
[path.join(Global.Path.data, "plans", "*")]: "allow",
|
|
},
|
|
edit: {
|
|
"*": "deny",
|
|
[path.join(".opencode", "plans", "*.md")]: "allow",
|
|
[path.relative(Instance.worktree, path.join(Global.Path.data, path.join("plans", "*.md")))]: "allow",
|
|
},
|
|
}),
|
|
user,
|
|
),
|
|
mode: "primary",
|
|
native: true,
|
|
},
|
|
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.`,
|
|
permission: PermissionNext.merge(
|
|
defaults,
|
|
PermissionNext.fromConfig({
|
|
todoread: "deny",
|
|
todowrite: "deny",
|
|
}),
|
|
user,
|
|
),
|
|
options: {},
|
|
mode: "subagent",
|
|
native: true,
|
|
},
|
|
explore: {
|
|
name: "explore",
|
|
permission: PermissionNext.merge(
|
|
defaults,
|
|
PermissionNext.fromConfig({
|
|
"*": "deny",
|
|
grep: "allow",
|
|
glob: "allow",
|
|
list: "allow",
|
|
bash: "allow",
|
|
webfetch: "allow",
|
|
websearch: "allow",
|
|
codesearch: "allow",
|
|
read: "allow",
|
|
external_directory: {
|
|
[Truncate.DIR]: "allow",
|
|
[Truncate.GLOB]: "allow",
|
|
},
|
|
}),
|
|
user,
|
|
),
|
|
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: PROMPT_EXPLORE,
|
|
options: {},
|
|
mode: "subagent",
|
|
native: true,
|
|
},
|
|
compaction: {
|
|
name: "compaction",
|
|
mode: "primary",
|
|
native: true,
|
|
hidden: true,
|
|
prompt: PROMPT_COMPACTION,
|
|
permission: PermissionNext.merge(
|
|
defaults,
|
|
PermissionNext.fromConfig({
|
|
"*": "deny",
|
|
}),
|
|
user,
|
|
),
|
|
options: {},
|
|
},
|
|
title: {
|
|
name: "title",
|
|
mode: "primary",
|
|
options: {},
|
|
native: true,
|
|
hidden: true,
|
|
temperature: 0.5,
|
|
permission: PermissionNext.merge(
|
|
defaults,
|
|
PermissionNext.fromConfig({
|
|
"*": "deny",
|
|
}),
|
|
user,
|
|
),
|
|
prompt: PROMPT_TITLE,
|
|
},
|
|
summary: {
|
|
name: "summary",
|
|
mode: "primary",
|
|
options: {},
|
|
native: true,
|
|
hidden: true,
|
|
permission: PermissionNext.merge(
|
|
defaults,
|
|
PermissionNext.fromConfig({
|
|
"*": "deny",
|
|
}),
|
|
user,
|
|
),
|
|
prompt: PROMPT_SUMMARY,
|
|
},
|
|
}
|
|
|
|
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: PermissionNext.merge(defaults, user),
|
|
options: {},
|
|
native: false,
|
|
}
|
|
if (value.model) item.model = Provider.parseModel(value.model)
|
|
item.prompt = value.prompt ?? item.prompt
|
|
item.description = value.description ?? item.description
|
|
item.temperature = value.temperature ?? item.temperature
|
|
item.topP = value.top_p ?? item.topP
|
|
item.mode = value.mode ?? item.mode
|
|
item.color = value.color ?? item.color
|
|
item.hidden = value.hidden ?? item.hidden
|
|
item.name = value.name ?? item.name
|
|
item.steps = value.steps ?? item.steps
|
|
item.options = mergeDeep(item.options, value.options ?? {})
|
|
item.permission = PermissionNext.merge(item.permission, PermissionNext.fromConfig(value.permission ?? {}))
|
|
}
|
|
|
|
// Ensure Truncate.DIR is allowed unless explicitly configured
|
|
for (const name in result) {
|
|
const agent = result[name]
|
|
const explicit = agent.permission.some((r) => {
|
|
if (r.permission !== "external_directory") return false
|
|
if (r.action !== "deny") return false
|
|
return r.pattern === Truncate.DIR || r.pattern === Truncate.GLOB
|
|
})
|
|
if (explicit) continue
|
|
|
|
result[name].permission = PermissionNext.merge(
|
|
result[name].permission,
|
|
PermissionNext.fromConfig({ external_directory: { [Truncate.DIR]: "allow", [Truncate.GLOB]: "allow" } }),
|
|
)
|
|
}
|
|
|
|
return result
|
|
})
|
|
|
|
export async function get(agent: string) {
|
|
return state().then((x) => x[agent])
|
|
}
|
|
|
|
export async function list() {
|
|
const cfg = await Config.get()
|
|
return pipe(
|
|
await state(),
|
|
values(),
|
|
sortBy([(x) => (cfg.default_agent ? x.name === cfg.default_agent : x.name === "build"), "desc"]),
|
|
)
|
|
}
|
|
|
|
export async function defaultAgent() {
|
|
return state().then((x) => Object.keys(x)[0])
|
|
}
|
|
|
|
export async function generate(input: { description: string; model?: { providerID: string; modelID: string } }) {
|
|
const cfg = await Config.get()
|
|
const defaultModel = input.model ?? (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
|
|
}
|
|
}
|