cloudaxe-opencode/packages/opencode/src/agent/agent.ts

377 lines
12 KiB
TypeScript
Raw Normal View History

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"
2025-12-12 21:23:48 +00:00
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"]),
2025-12-12 21:44:00 +00:00
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()
2025-08-17 02:51:56 +00:00
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,
2025-08-17 02:51:56 +00:00
...defaultTools,
},
2025-08-11 00:30:25 +00:00
options: {},
permission: agentPermission,
mode: "subagent",
2025-12-12 21:44:00 +00:00
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",
2025-12-12 21:44:00 +00:00
native: true,
},
2025-12-12 21:23:48 +00:00
compaction: {
name: "compaction",
mode: "primary",
2025-12-12 21:44:00 +00:00
native: true,
hidden: true,
2025-12-12 21:23:48 +00:00
prompt: PROMPT_COMPACTION,
tools: {
"*": false,
},
options: {},
permission: agentPermission,
},
build: {
name: "build",
2025-08-17 02:51:56 +00:00
tools: { ...defaultTools },
2025-08-11 00:30:25 +00:00
options: {},
permission: agentPermission,
mode: "primary",
2025-12-12 21:44:00 +00:00
native: true,
},
2025-12-10 05:39:24 +00:00
summary: {
name: "summary",
mode: "subagent",
options: {},
2025-12-12 21:44:00 +00:00
native: true,
2025-12-10 05:39:24 +00:00
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 users 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",
2025-08-11 00:30:25 +00:00
options: {},
permission: planPermission,
tools: {
2025-08-17 02:51:56 +00:00
...defaultTools,
},
mode: "primary",
2025-12-12 21:44:00 +00:00
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,
2025-08-11 00:30:25 +00:00
options: {},
tools: {},
2025-12-12 21:44:00 +00:00
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,
}
2025-08-17 02:51:56 +00:00
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
}
}
2025-11-08 01:59:02 +00:00
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
}