2025-07-25 01:20:43 +00:00
import { Config } from "../config/config"
2025-10-26 19:50:41 +00:00
import z from "zod"
2025-07-25 01:20:43 +00:00
import { Provider } from "../provider/provider"
2026-03-12 13:27:52 +00:00
import { ModelID , ProviderID } from "../provider/schema"
2026-01-19 05:22:31 +00:00
import { generateObject , streamObject , type ModelMessage } from "ai"
2025-07-25 01:20:43 +00:00
import { SystemPrompt } from "../session/system"
2025-09-01 21:15:49 +00:00
import { Instance } from "../project/instance"
2026-03-18 01:59:54 +00:00
import { Truncate } from "../tool/truncate"
2026-01-19 05:22:31 +00:00
import { Auth } from "../auth"
import { ProviderTransform } from "../provider/transform"
2025-07-25 01:20:43 +00:00
2025-12-15 02:11:30 +00:00
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"
2026-03-20 20:55:46 +00:00
import { Permission as PermissionNext } from "@/permission/service"
2026-01-01 22:54:11 +00:00
import { mergeDeep , pipe , sortBy , values } from "remeda"
2026-01-14 13:21:17 +00:00
import { Global } from "@/global"
import path from "path"
2026-01-25 22:27:15 +00:00
import { Plugin } from "@/plugin"
2026-02-03 15:58:31 +00:00
import { Skill } from "../skill"
2025-12-15 02:11:30 +00:00
2025-07-25 01:20:43 +00:00
export namespace Agent {
export const Info = z
. object ( {
name : z.string ( ) ,
2025-08-07 20:32:12 +00:00
description : z.string ( ) . optional ( ) ,
2025-11-17 06:06:40 +00:00
mode : z.enum ( [ "subagent" , "primary" , "all" ] ) ,
2025-12-15 02:11:30 +00:00
native : z.boolean ( ) . optional ( ) ,
hidden : z.boolean ( ) . optional ( ) ,
2025-08-07 20:32:12 +00:00
topP : z.number ( ) . optional ( ) ,
temperature : z.number ( ) . optional ( ) ,
2025-11-12 00:32:44 +00:00
color : z.string ( ) . optional ( ) ,
2026-01-01 22:54:11 +00:00
permission : PermissionNext.Ruleset ,
2025-07-25 01:20:43 +00:00
model : z
. object ( {
2026-03-12 13:27:52 +00:00
modelID : ModelID.zod ,
providerID : ProviderID.zod ,
2025-07-25 01:20:43 +00:00
} )
. optional ( ) ,
2026-02-01 20:12:30 +00:00
variant : z.string ( ) . optional ( ) ,
2025-07-25 01:20:43 +00:00
prompt : z.string ( ) . optional ( ) ,
2025-08-11 01:34:28 +00:00
options : z.record ( z . string ( ) , z . any ( ) ) ,
2026-01-01 22:54:11 +00:00
steps : z.number ( ) . int ( ) . positive ( ) . optional ( ) ,
2025-07-25 01:20:43 +00:00
} )
2025-09-15 07:12:07 +00:00
. meta ( {
2025-07-25 01:20:43 +00:00
ref : "Agent" ,
} )
export type Info = z . infer < typeof Info >
2025-08-07 20:32:12 +00:00
2025-09-01 21:15:49 +00:00
const state = Instance . state ( async ( ) = > {
2025-07-25 01:20:43 +00:00
const cfg = await Config . get ( )
2026-01-01 22:54:11 +00:00
2026-02-03 15:58:31 +00:00
const skillDirs = await Skill . dirs ( )
2026-02-18 02:16:55 +00:00
const whitelistedDirs = [ Truncate . GLOB , . . . skillDirs . map ( ( dir ) = > path . join ( dir , "*" ) ) ]
2026-01-01 22:54:11 +00:00
const defaults = PermissionNext . fromConfig ( {
"*" : "allow" ,
2025-11-10 02:21:38 +00:00
doom_loop : "ask" ,
2026-01-07 23:25:00 +00:00
external_directory : {
"*" : "ask" ,
2026-02-18 02:16:55 +00:00
. . . Object . fromEntries ( whitelistedDirs . map ( ( dir ) = > [ dir , "allow" ] ) ) ,
2026-01-07 23:25:00 +00:00
} ,
2026-01-08 03:29:42 +00:00
question : "deny" ,
2026-01-13 20:55:48 +00:00
plan_enter : "deny" ,
plan_exit : "deny" ,
2026-01-04 07:49:31 +00:00
// mirrors github.com/github/gitignore Node.gitignore pattern for .env files
read : {
"*" : "allow" ,
2026-01-12 21:32:43 +00:00
"*.env" : "ask" ,
"*.env.*" : "ask" ,
2026-01-04 07:49:31 +00:00
"*.env.example" : "allow" ,
} ,
2026-01-01 22:54:11 +00:00
} )
const user = PermissionNext . fromConfig ( cfg . permission ? ? { } )
2025-08-21 22:25:31 +00:00
2025-07-25 01:20:43 +00:00
const result : Record < string , Info > = {
2025-12-15 14:58:23 +00:00
build : {
name : "build" ,
2026-01-26 15:43:10 +00:00
description : "The default agent. Executes tools based on configured permissions." ,
2025-12-15 14:58:23 +00:00
options : { } ,
2026-01-08 03:29:42 +00:00
permission : PermissionNext.merge (
defaults ,
PermissionNext . fromConfig ( {
question : "allow" ,
2026-01-13 20:55:48 +00:00
plan_enter : "allow" ,
2026-01-08 03:29:42 +00:00
} ) ,
user ,
) ,
2025-12-15 14:58:23 +00:00
mode : "primary" ,
native : true ,
} ,
plan : {
name : "plan" ,
2026-01-26 15:43:10 +00:00
description : "Plan mode. Disallows all edit tools." ,
2025-12-15 14:58:23 +00:00
options : { } ,
2026-01-01 22:54:11 +00:00
permission : PermissionNext.merge (
defaults ,
PermissionNext . fromConfig ( {
2026-01-08 03:29:42 +00:00
question : "allow" ,
2026-01-13 20:55:48 +00:00
plan_exit : "allow" ,
2026-01-14 13:21:17 +00:00
external_directory : {
[ path . join ( Global . Path . data , "plans" , "*" ) ] : "allow" ,
} ,
2026-01-01 22:54:11 +00:00
edit : {
"*" : "deny" ,
2026-01-14 19:28:48 +00:00
[ path . join ( ".opencode" , "plans" , "*.md" ) ] : "allow" ,
[ path . relative ( Instance . worktree , path . join ( Global . Path . data , path . join ( "plans" , "*.md" ) ) ) ] : "allow" ,
2026-01-01 22:54:11 +00:00
} ,
} ) ,
user ,
) ,
2025-12-15 14:58:23 +00:00
mode : "primary" ,
native : true ,
} ,
2025-07-25 01:20:43 +00:00
general : {
name : "general" ,
2025-11-29 21:40:08 +00:00
description : ` General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel. ` ,
2026-01-01 22:54:11 +00:00
permission : PermissionNext.merge (
defaults ,
PermissionNext . fromConfig ( {
todoread : "deny" ,
todowrite : "deny" ,
} ) ,
user ,
) ,
2025-08-11 00:30:25 +00:00
options : { } ,
2025-08-07 20:32:12 +00:00
mode : "subagent" ,
2025-12-15 02:11:30 +00:00
native : true ,
2025-11-29 02:13:07 +00:00
} ,
explore : {
name : "explore" ,
2026-01-01 22:54:11 +00:00
permission : PermissionNext.merge (
defaults ,
PermissionNext . fromConfig ( {
"*" : "deny" ,
grep : "allow" ,
glob : "allow" ,
list : "allow" ,
bash : "allow" ,
webfetch : "allow" ,
websearch : "allow" ,
codesearch : "allow" ,
read : "allow" ,
2026-01-07 23:25:00 +00:00
external_directory : {
2026-02-18 02:16:55 +00:00
"*" : "ask" ,
. . . Object . fromEntries ( whitelistedDirs . map ( ( dir ) = > [ dir , "allow" ] ) ) ,
2026-01-07 23:25:00 +00:00
} ,
2026-01-01 22:54:11 +00:00
} ) ,
user ,
) ,
2025-11-29 02:47:39 +00:00
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. ` ,
2025-12-15 02:11:30 +00:00
prompt : PROMPT_EXPLORE ,
2025-11-29 02:13:07 +00:00
options : { } ,
mode : "subagent" ,
2025-12-15 02:11:30 +00:00
native : true ,
} ,
compaction : {
name : "compaction" ,
mode : "primary" ,
native : true ,
hidden : true ,
prompt : PROMPT_COMPACTION ,
2026-01-01 22:54:11 +00:00
permission : PermissionNext.merge (
defaults ,
PermissionNext . fromConfig ( {
"*" : "deny" ,
} ) ,
user ,
) ,
2025-12-15 02:11:30 +00:00
options : { } ,
2025-08-07 20:32:12 +00:00
} ,
2025-12-15 02:11:30 +00:00
title : {
name : "title" ,
mode : "primary" ,
options : { } ,
native : true ,
hidden : true ,
2026-01-07 18:56:32 +00:00
temperature : 0.5 ,
2026-01-01 22:54:11 +00:00
permission : PermissionNext.merge (
defaults ,
PermissionNext . fromConfig ( {
"*" : "deny" ,
} ) ,
user ,
) ,
2025-12-15 02:11:30 +00:00
prompt : PROMPT_TITLE ,
} ,
summary : {
name : "summary" ,
mode : "primary" ,
options : { } ,
native : true ,
hidden : true ,
2026-01-01 22:54:11 +00:00
permission : PermissionNext.merge (
defaults ,
PermissionNext . fromConfig ( {
"*" : "deny" ,
} ) ,
user ,
) ,
2025-12-15 02:11:30 +00:00
prompt : PROMPT_SUMMARY ,
2025-08-07 20:32:12 +00:00
} ,
2025-07-25 01:20:43 +00:00
}
2026-01-01 22:54:11 +00:00
2025-07-25 01:20:43 +00:00
for ( const [ key , value ] of Object . entries ( cfg . agent ? ? { } ) ) {
2025-08-05 10:20:00 +00:00
if ( value . disable ) {
delete result [ key ]
continue
}
2025-07-25 01:20:43 +00:00
let item = result [ key ]
if ( ! item )
item = result [ key ] = {
name : key ,
2025-08-07 20:32:12 +00:00
mode : "all" ,
2026-01-01 22:54:11 +00:00
permission : PermissionNext.merge ( defaults , user ) ,
2025-08-11 00:30:25 +00:00
options : { } ,
2025-12-15 02:11:30 +00:00
native : false ,
2025-07-25 01:20:43 +00:00
}
2026-01-01 22:54:11 +00:00
if ( value . model ) item . model = Provider . parseModel ( value . model )
2026-02-01 20:12:30 +00:00
item . variant = value . variant ? ? item . variant
2026-01-01 22:54:11 +00:00
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
2026-01-07 04:29:17 +00:00
item . hidden = value . hidden ? ? item . hidden
2026-01-06 18:31:41 +00:00
item . name = value . name ? ? item . name
2026-01-01 22:54:11 +00:00
item . steps = value . steps ? ? item . steps
item . options = mergeDeep ( item . options , value . options ? ? { } )
item . permission = PermissionNext . merge ( item . permission , PermissionNext . fromConfig ( value . permission ? ? { } ) )
2025-07-25 01:20:43 +00:00
}
2026-01-08 17:07:23 +00:00
2026-02-02 22:43:55 +00:00
// Ensure Truncate.GLOB is allowed unless explicitly configured
2026-01-08 17:07:23 +00:00
for ( const name in result ) {
const agent = result [ name ]
2026-01-12 22:46:32 +00:00
const explicit = agent . permission . some ( ( r ) = > {
if ( r . permission !== "external_directory" ) return false
if ( r . action !== "deny" ) return false
2026-02-02 22:43:55 +00:00
return r . pattern === Truncate . GLOB
2026-01-12 22:46:32 +00:00
} )
2026-01-08 17:07:23 +00:00
if ( explicit ) continue
result [ name ] . permission = PermissionNext . merge (
result [ name ] . permission ,
2026-02-02 22:43:55 +00:00
PermissionNext . fromConfig ( { external_directory : { [ Truncate . GLOB ] : "allow" } } ) ,
2026-01-08 17:07:23 +00:00
)
}
2025-07-25 01:20:43 +00:00
return result
} )
export async function get ( agent : string ) {
return state ( ) . then ( ( x ) = > x [ agent ] )
}
export async function list() {
2026-01-01 22:54:11 +00:00
const cfg = await Config . get ( )
return pipe (
await state ( ) ,
values ( ) ,
2026-03-19 20:04:03 +00:00
sortBy (
[ ( x ) = > ( cfg . default_agent ? x . name === cfg.default_agent : x.name === "build" ) , "desc" ] ,
[ ( x ) = > x . name , "asc" ] ,
) ,
2026-01-01 22:54:11 +00:00
)
2025-07-25 01:20:43 +00:00
}
2026-01-01 22:54:11 +00:00
export async function defaultAgent() {
2026-01-15 23:09:19 +00:00
const cfg = await Config . get ( )
const agents = await state ( )
if ( cfg . default_agent ) {
const agent = agents [ cfg . default_agent ]
if ( ! agent ) throw new Error ( ` default agent " ${ cfg . default_agent } " not found ` )
if ( agent . mode === "subagent" ) throw new Error ( ` default agent " ${ cfg . default_agent } " is a subagent ` )
if ( agent . hidden === true ) throw new Error ( ` default agent " ${ cfg . default_agent } " is hidden ` )
return agent . name
}
const primaryVisible = Object . values ( agents ) . find ( ( a ) = > a . mode !== "subagent" && a . hidden !== true )
if ( ! primaryVisible ) throw new Error ( "no primary visible agent found" )
return primaryVisible . name
2025-12-20 17:46:48 +00:00
}
2026-03-12 14:48:17 +00:00
export async function generate ( input : { description : string ; model ? : { providerID : ProviderID ; modelID : ModelID } } ) {
2025-12-05 16:48:22 +00:00
const cfg = await Config . get ( )
2025-12-15 20:55:59 +00:00
const defaultModel = input . model ? ? ( await Provider . defaultModel ( ) )
2025-07-25 01:20:43 +00:00
const model = await Provider . getModel ( defaultModel . providerID , defaultModel . modelID )
2025-12-04 02:09:03 +00:00
const language = await Provider . getLanguage ( model )
2026-01-19 05:22:31 +00:00
2026-01-25 22:27:15 +00:00
const system = [ PROMPT_GENERATE ]
await Plugin . trigger ( "experimental.chat.system.transform" , { model } , { system } )
2025-07-25 01:20:43 +00:00
const existing = await list ( )
2026-01-19 05:22:31 +00:00
const params = {
2025-12-09 15:46:48 +00:00
experimental_telemetry : {
isEnabled : cfg.experimental?.openTelemetry ,
metadata : {
userId : cfg.username ? ? "unknown" ,
} ,
} ,
2025-07-25 01:20:43 +00:00
temperature : 0.3 ,
2025-12-10 03:09:45 +00:00
messages : [
2025-07-25 01:20:43 +00:00
. . . 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 ` ,
} ,
] ,
2025-12-04 02:09:03 +00:00
model : language ,
2025-07-25 01:20:43 +00:00
schema : z.object ( {
identifier : z.string ( ) ,
whenToUse : z.string ( ) ,
systemPrompt : z.string ( ) ,
} ) ,
2026-01-19 05:22:31 +00:00
} satisfies Parameters < typeof generateObject > [ 0 ]
2026-03-20 15:37:47 +00:00
// TODO: clean this up so provider specific logic doesnt bleed over
2026-01-19 05:22:31 +00:00
if ( defaultModel . providerID === "openai" && ( await Auth . get ( defaultModel . providerID ) ) ? . type === "oauth" ) {
const result = streamObject ( {
. . . params ,
providerOptions : ProviderTransform.providerOptions ( model , {
store : false ,
} ) ,
onError : ( ) = > { } ,
} )
for await ( const part of result . fullStream ) {
if ( part . type === "error" ) throw part . error
}
return result . object
}
const result = await generateObject ( params )
2025-07-25 01:20:43 +00:00
return result . object
}
}