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-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-21 04:51:35 +00:00
import { Permission } from "@/permission"
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"
2026-03-24 18:15:23 +00:00
import { Effect , ServiceMap , Layer } from "effect"
import { InstanceState } from "@/effect/instance-state"
2026-03-26 00:19:24 +00:00
import { makeRuntime } from "@/effect/run-service"
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-03-21 04:51:35 +00:00
permission : Permission.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
2026-03-24 18:15:23 +00:00
export interface Interface {
readonly get : ( agent : string ) = > Effect . Effect < Agent.Info >
readonly list : ( ) = > Effect . Effect < Agent.Info [ ] >
readonly defaultAgent : ( ) = > Effect . Effect < string >
readonly generate : ( input : {
description : string
model ? : { providerID : ProviderID ; modelID : ModelID }
} ) = > Effect . Effect < {
identifier : string
whenToUse : string
systemPrompt : string
} >
}
2026-01-01 22:54:11 +00:00
2026-03-24 18:15:23 +00:00
type State = Omit < Interface , " generate " >
2025-08-21 22:25:31 +00:00
2026-03-24 18:15:23 +00:00
export class Service extends ServiceMap . Service < Service , Interface > ( ) ( "@opencode/Agent" ) { }
export const layer = Layer . effect (
Service ,
Effect . gen ( function * ( ) {
const config = ( ) = > Effect . promise ( ( ) = > Config . get ( ) )
const auth = yield * Auth . Service
const state = yield * InstanceState . make < State > (
Effect . fn ( "Agent.state" ) ( function * ( ctx ) {
const cfg = yield * config ( )
const skillDirs = yield * Effect . promise ( ( ) = > Skill . dirs ( ) )
const whitelistedDirs = [ Truncate . GLOB , . . . skillDirs . map ( ( dir ) = > path . join ( dir , "*" ) ) ]
const defaults = Permission . fromConfig ( {
"*" : "allow" ,
doom_loop : "ask" ,
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-03-24 18:15:23 +00:00
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" ,
} ,
} )
2026-01-01 22:54:11 +00:00
2026-03-24 18:15:23 +00:00
const user = Permission . fromConfig ( cfg . permission ? ? { } )
2026-01-08 17:07:23 +00:00
2026-03-24 18:15:23 +00:00
const agents : Record < string , Info > = {
build : {
name : "build" ,
description : "The default agent. Executes tools based on configured permissions." ,
options : { } ,
permission : Permission.merge (
defaults ,
Permission . fromConfig ( {
question : "allow" ,
plan_enter : "allow" ,
} ) ,
user ,
) ,
mode : "primary" ,
native : true ,
} ,
plan : {
name : "plan" ,
description : "Plan mode. Disallows all edit tools." ,
options : { } ,
permission : Permission.merge (
defaults ,
Permission . 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 : Permission.merge (
defaults ,
Permission . fromConfig ( {
todowrite : "deny" ,
} ) ,
user ,
) ,
options : { } ,
mode : "subagent" ,
native : true ,
} ,
explore : {
name : "explore" ,
permission : Permission.merge (
defaults ,
Permission . fromConfig ( {
"*" : "deny" ,
grep : "allow" ,
glob : "allow" ,
list : "allow" ,
bash : "allow" ,
webfetch : "allow" ,
websearch : "allow" ,
codesearch : "allow" ,
read : "allow" ,
external_directory : {
"*" : "ask" ,
. . . Object . fromEntries ( whitelistedDirs . map ( ( dir ) = > [ dir , "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 : Permission.merge (
defaults ,
Permission . fromConfig ( {
"*" : "deny" ,
} ) ,
user ,
) ,
options : { } ,
} ,
title : {
name : "title" ,
mode : "primary" ,
options : { } ,
native : true ,
hidden : true ,
temperature : 0.5 ,
permission : Permission.merge (
defaults ,
Permission . fromConfig ( {
"*" : "deny" ,
} ) ,
user ,
) ,
prompt : PROMPT_TITLE ,
} ,
summary : {
name : "summary" ,
mode : "primary" ,
options : { } ,
native : true ,
hidden : true ,
permission : Permission.merge (
defaults ,
Permission . fromConfig ( {
"*" : "deny" ,
} ) ,
user ,
) ,
prompt : PROMPT_SUMMARY ,
} ,
}
2026-01-08 17:07:23 +00:00
2026-03-24 18:15:23 +00:00
for ( const [ key , value ] of Object . entries ( cfg . agent ? ? { } ) ) {
if ( value . disable ) {
delete agents [ key ]
continue
}
let item = agents [ key ]
if ( ! item )
item = agents [ key ] = {
name : key ,
mode : "all" ,
permission : Permission.merge ( defaults , user ) ,
options : { } ,
native : false ,
}
if ( value . model ) item . model = Provider . parseModel ( value . model )
item . variant = value . variant ? ? item . variant
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 = Permission . merge ( item . permission , Permission . fromConfig ( value . permission ? ? { } ) )
}
2026-01-08 17:07:23 +00:00
2026-03-24 18:15:23 +00:00
// Ensure Truncate.GLOB is allowed unless explicitly configured
for ( const name in agents ) {
const agent = agents [ name ]
const explicit = agent . permission . some ( ( r ) = > {
if ( r . permission !== "external_directory" ) return false
if ( r . action !== "deny" ) return false
return r . pattern === Truncate . GLOB
} )
if ( explicit ) continue
2025-07-25 01:20:43 +00:00
2026-03-24 18:15:23 +00:00
agents [ name ] . permission = Permission . merge (
agents [ name ] . permission ,
Permission . fromConfig ( { external_directory : { [ Truncate . GLOB ] : "allow" } } ) ,
)
}
2025-07-25 01:20:43 +00:00
2026-03-24 18:15:23 +00:00
const get = Effect . fnUntraced ( function * ( agent : string ) {
return agents [ agent ]
} )
2025-07-25 01:20:43 +00:00
2026-03-24 18:15:23 +00:00
const list = Effect . fnUntraced ( function * ( ) {
const cfg = yield * config ( )
return pipe (
agents ,
values ( ) ,
sortBy (
[ ( x ) = > ( cfg . default_agent ? x . name === cfg.default_agent : x.name === "build" ) , "desc" ] ,
[ ( x ) = > x . name , "asc" ] ,
) ,
)
} )
2026-01-15 23:09:19 +00:00
2026-03-24 18:15:23 +00:00
const defaultAgent = Effect . fnUntraced ( function * ( ) {
const c = yield * config ( )
if ( c . default_agent ) {
const agent = agents [ c . default_agent ]
if ( ! agent ) throw new Error ( ` default agent " ${ c . default_agent } " not found ` )
if ( agent . mode === "subagent" ) throw new Error ( ` default agent " ${ c . default_agent } " is a subagent ` )
if ( agent . hidden === true ) throw new Error ( ` default agent " ${ c . default_agent } " is hidden ` )
return agent . name
}
const visible = Object . values ( agents ) . find ( ( a ) = > a . mode !== "subagent" && a . hidden !== true )
if ( ! visible ) throw new Error ( "no primary visible agent found" )
return visible . name
} )
2026-01-15 23:09:19 +00:00
2026-03-24 18:15:23 +00:00
return {
get ,
list ,
defaultAgent ,
} satisfies State
} ) ,
)
2025-12-20 17:46:48 +00:00
2026-03-24 18:15:23 +00:00
return Service . of ( {
get : Effect . fn ( "Agent.get" ) ( function * ( agent : string ) {
return yield * InstanceState . useEffect ( state , ( s ) = > s . get ( agent ) )
} ) ,
list : Effect.fn ( "Agent.list" ) ( function * ( ) {
return yield * InstanceState . useEffect ( state , ( s ) = > s . list ( ) )
} ) ,
defaultAgent : Effect.fn ( "Agent.defaultAgent" ) ( function * ( ) {
return yield * InstanceState . useEffect ( state , ( s ) = > s . defaultAgent ( ) )
} ) ,
generate : Effect.fn ( "Agent.generate" ) ( function * ( input : {
description : string
model ? : { providerID : ProviderID ; modelID : ModelID }
} ) {
const cfg = yield * config ( )
const model = input . model ? ? ( yield * Effect . promise ( ( ) = > Provider . defaultModel ( ) ) )
const resolved = yield * Effect . promise ( ( ) = > Provider . getModel ( model . providerID , model . modelID ) )
const language = yield * Effect . promise ( ( ) = > Provider . getLanguage ( resolved ) )
2026-01-19 05:22:31 +00:00
2026-03-24 18:15:23 +00:00
const system = [ PROMPT_GENERATE ]
yield * Effect . promise ( ( ) = >
Plugin . trigger ( "experimental.chat.system.transform" , { model : resolved } , { system } ) ,
)
const existing = yield * InstanceState . useEffect ( state , ( s ) = > s . list ( ) )
2026-01-19 05:22:31 +00:00
2026-03-24 18:15:23 +00:00
const params = {
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 ( ) ,
} ) ,
} satisfies Parameters < typeof generateObject > [ 0 ]
2026-01-19 05:22:31 +00:00
2026-03-24 18:15:23 +00:00
// TODO: clean this up so provider specific logic doesnt bleed over
const authInfo = yield * auth . get ( model . providerID ) . pipe ( Effect . orDie )
if ( model . providerID === "openai" && authInfo ? . type === "oauth" ) {
return yield * Effect . promise ( async ( ) = > {
const result = streamObject ( {
. . . params ,
providerOptions : ProviderTransform.providerOptions ( resolved , {
store : false ,
} ) ,
onError : ( ) = > { } ,
} )
for await ( const part of result . fullStream ) {
if ( part . type === "error" ) throw part . error
}
return result . object
} )
}
return yield * Effect . promise ( ( ) = > generateObject ( params ) . then ( ( r ) = > r . object ) )
2026-01-19 05:22:31 +00:00
} ) ,
} )
2026-03-24 18:15:23 +00:00
} ) ,
)
export const defaultLayer = layer . pipe ( Layer . provide ( Auth . layer ) )
2026-01-19 05:22:31 +00:00
2026-03-26 00:19:24 +00:00
const { runPromise } = makeRuntime ( Service , defaultLayer )
2026-03-24 18:15:23 +00:00
export async function get ( agent : string ) {
return runPromise ( ( svc ) = > svc . get ( agent ) )
}
export async function list() {
return runPromise ( ( svc ) = > svc . list ( ) )
}
export async function defaultAgent() {
return runPromise ( ( svc ) = > svc . defaultAgent ( ) )
}
export async function generate ( input : { description : string ; model ? : { providerID : ProviderID ; modelID : ModelID } } ) {
return runPromise ( ( svc ) = > svc . generate ( input ) )
2025-07-25 01:20:43 +00:00
}
}