2026-04-27 18:33:33 +00:00
import { Config } from "@/config/config"
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"
2026-04-27 18:33:33 +00:00
import { Truncate } from "@/tool/truncate"
2026-01-19 05:22:31 +00:00
import { Auth } from "../auth"
2026-04-27 18:33:33 +00:00
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"
2026-05-08 20:20:08 +00:00
import PROMPT_SCOUT from "./prompt/scout.txt"
2025-12-15 02:11:30 +00:00
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-04-25 17:51:37 +00:00
import { Global } from "@opencode-ai/core/global"
2026-01-14 13:21:17 +00:00
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-04-25 18:00:30 +00:00
import { Effect , Context , Layer , Schema } from "effect"
2026-04-27 18:33:33 +00:00
import { InstanceState } from "@/effect/instance-state"
2026-05-13 11:37:53 +00:00
import { RuntimeFlags } from "@/effect/runtime-flags"
2026-04-15 02:30:50 +00:00
import * as Option from "effect/Option"
import * as OtelTracer from "@effect/opentelemetry/Tracer"
2026-05-11 20:10:58 +00:00
import { type DeepMutable } from "@opencode-ai/core/schema"
2026-05-08 20:20:08 +00:00
2026-04-25 18:00:30 +00:00
export const Info = Schema . Struct ( {
name : Schema.String ,
description : Schema.optional ( Schema . String ) ,
mode : Schema.Literals ( [ "subagent" , "primary" , "all" ] ) ,
native : Schema.optional ( Schema . Boolean ) ,
hidden : Schema.optional ( Schema . Boolean ) ,
2026-04-29 13:34:50 +00:00
topP : Schema.optional ( Schema . Finite ) ,
temperature : Schema.optional ( Schema . Finite ) ,
2026-04-25 18:00:30 +00:00
color : Schema.optional ( Schema . String ) ,
permission : Permission.Ruleset ,
model : Schema.optional (
Schema . Struct ( {
modelID : ModelID ,
providerID : ProviderID ,
} ) ,
) ,
variant : Schema.optional ( Schema . String ) ,
prompt : Schema.optional ( Schema . String ) ,
options : Schema.Record ( Schema . String , Schema . Unknown ) ,
2026-04-29 13:34:50 +00:00
steps : Schema.optional ( Schema . Finite ) ,
2026-05-11 20:10:58 +00:00
} ) . annotate ( { identifier : "Agent" } )
2026-04-25 18:00:30 +00:00
export type Info = DeepMutable < Schema.Schema.Type < typeof Info > >
2025-08-07 20:32:12 +00:00
2026-05-12 01:42:04 +00:00
const GeneratedAgent = Schema . Struct ( {
identifier : Schema.String ,
whenToUse : Schema.String ,
systemPrompt : Schema.String ,
} )
2026-04-17 00:01:44 +00:00
export interface Interface {
readonly get : ( agent : string ) = > Effect . Effect < Info >
readonly list : ( ) = > Effect . Effect < Info [ ] >
2026-05-12 19:38:30 +00:00
readonly defaultInfo : ( ) = > Effect . Effect < Info >
2026-04-17 00:01:44 +00:00
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-04-17 00:01:44 +00:00
type State = Omit < Interface , " generate " >
2025-08-21 22:25:31 +00:00
2026-04-17 00:01:44 +00:00
export class Service extends Context . Service < Service , Interface > ( ) ( "@opencode/Agent" ) { }
2026-03-24 18:15:23 +00:00
2026-04-17 00:01:44 +00:00
export const layer = Layer . effect (
Service ,
Effect . gen ( function * ( ) {
const config = yield * Config . Service
const auth = yield * Auth . Service
const plugin = yield * Plugin . Service
const skill = yield * Skill . Service
const provider = yield * Provider . Service
2026-05-13 11:37:53 +00:00
const flags = yield * RuntimeFlags . Service
2026-03-24 18:15:23 +00:00
2026-04-17 00:01:44 +00:00
const state = yield * InstanceState . make < State > (
2026-04-25 18:00:30 +00:00
Effect . fn ( "Agent.state" ) ( function * ( ctx ) {
2026-04-17 00:01:44 +00:00
const cfg = yield * config . get ( )
const skillDirs = yield * skill . dirs ( )
2026-05-01 03:47:15 +00:00
const whitelistedDirs = [
Truncate . GLOB ,
path . join ( Global . Path . tmp , "*" ) ,
. . . skillDirs . map ( ( dir ) = > path . join ( dir , "*" ) ) ,
]
2026-05-08 20:20:08 +00:00
const readonlyExternalDirectory = {
"*" : "ask" ,
. . . Object . fromEntries ( whitelistedDirs . map ( ( dir ) = > [ dir , "allow" ] ) ) ,
} satisfies Record < string , " allow " | " ask " | " deny " >
2026-03-24 18:15:23 +00:00
2026-04-17 00:01:44 +00:00
const defaults = Permission . fromConfig ( {
"*" : "allow" ,
doom_loop : "ask" ,
external_directory : {
"*" : "ask" ,
. . . Object . fromEntries ( whitelistedDirs . map ( ( dir ) = > [ dir , "allow" ] ) ) ,
} ,
question : "deny" ,
plan_enter : "deny" ,
plan_exit : "deny" ,
2026-05-08 20:20:08 +00:00
repo_clone : "deny" ,
repo_overview : "deny" ,
2026-04-17 00:01:44 +00:00
// mirrors github.com/github/gitignore Node.gitignore pattern for .env files
read : {
2026-03-24 18:15:23 +00:00
"*" : "allow" ,
2026-04-17 00:01:44 +00:00
"*.env" : "ask" ,
"*.env.*" : "ask" ,
"*.env.example" : "allow" ,
} ,
} )
2026-01-01 22:54:11 +00:00
2026-05-13 04:55:45 +00:00
const user = Permission . fromConfig ( cfg . permission ? ? { } )
2026-01-08 17:07:23 +00:00
2026-04-17 00:01:44 +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 : {
2026-03-24 18:15:23 +00:00
"*" : "deny" ,
2026-04-17 00:01:44 +00:00
[ path . join ( ".opencode" , "plans" , "*.md" ) ] : "allow" ,
2026-04-25 18:00:30 +00:00
[ path . relative ( ctx . worktree , path . join ( Global . Path . data , path . join ( "plans" , "*.md" ) ) ) ] : "allow" ,
2026-04-17 00:01:44 +00:00
} ,
} ) ,
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" ,
read : "allow" ,
2026-05-08 20:20:08 +00:00
external_directory : readonlyExternalDirectory ,
2026-04-17 00:01:44 +00:00
} ) ,
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 ,
} ,
2026-05-13 11:37:53 +00:00
. . . ( flags . experimentalScout
2026-05-08 20:20:08 +00:00
? {
scout : {
name : "scout" ,
permission : Permission.merge (
defaults ,
Permission . fromConfig ( {
"*" : "deny" ,
grep : "allow" ,
glob : "allow" ,
webfetch : "allow" ,
websearch : "allow" ,
read : "allow" ,
repo_clone : "allow" ,
repo_overview : "allow" ,
external_directory : {
. . . readonlyExternalDirectory ,
[ path . join ( Global . Path . repos , "*" ) ] : "allow" ,
} ,
} ) ,
user ,
) ,
description : ` Docs and dependency-source specialist. Use this when you need to inspect external documentation, clone dependency repositories into the managed cache, and research library implementation details without modifying the user's workspace. ` ,
prompt : PROMPT_SCOUT ,
options : { } ,
mode : "subagent" as const ,
native : true ,
} ,
}
: { } ) ,
2026-04-17 00:01:44 +00:00
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-04-17 00:01:44 +00:00
for ( const [ key , value ] of Object . entries ( cfg . agent ? ? { } ) ) {
if ( value . disable ) {
delete agents [ key ]
continue
2026-03-24 18:15:23 +00:00
}
2026-04-17 00:01:44 +00:00
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-04-17 00:01:44 +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-04-17 00:01:44 +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-04-17 00:01:44 +00:00
const get = Effect . fnUntraced ( function * ( agent : string ) {
return agents [ agent ]
} )
2025-07-25 01:20:43 +00:00
2026-04-17 00:01:44 +00:00
const list = Effect . fnUntraced ( function * ( ) {
const cfg = yield * config . get ( )
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-05-12 19:38:30 +00:00
const defaultInfo = Effect . fnUntraced ( function * ( ) {
2026-04-17 00:01:44 +00:00
const c = yield * config . get ( )
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 ` )
2026-05-12 19:38:30 +00:00
return agent
2026-04-17 00:01:44 +00:00
}
const visible = Object . values ( agents ) . find ( ( a ) = > a . mode !== "subagent" && a . hidden !== true )
if ( ! visible ) throw new Error ( "no primary visible agent found" )
2026-05-12 19:38:30 +00:00
return visible
} )
const defaultAgent = Effect . fnUntraced ( function * ( ) {
return ( yield * defaultInfo ( ) ) . name
2026-04-17 00:01:44 +00:00
} )
2026-01-15 23:09:19 +00:00
2026-04-17 00:01:44 +00:00
return {
get ,
list ,
2026-05-12 19:38:30 +00:00
defaultInfo ,
2026-04-17 00:01:44 +00:00
defaultAgent ,
} satisfies State
} ) ,
)
2025-12-20 17:46:48 +00:00
2026-04-17 00:01:44 +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 ( ) )
} ) ,
2026-05-12 19:38:30 +00:00
defaultInfo : Effect.fn ( "Agent.defaultInfo" ) ( function * ( ) {
return yield * InstanceState . useEffect ( state , ( s ) = > s . defaultInfo ( ) )
} ) ,
2026-04-17 00:01:44 +00:00
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 . get ( )
const model = input . model ? ? ( yield * provider . defaultModel ( ) )
const resolved = yield * provider . getModel ( model . providerID , model . modelID )
const language = yield * provider . getLanguage ( resolved )
const tracer = cfg . experimental ? . openTelemetry
? Option . getOrUndefined ( yield * Effect . serviceOption ( OtelTracer . OtelTracer ) )
: undefined
2026-01-19 05:22:31 +00:00
2026-04-17 00:01:44 +00:00
const system = [ PROMPT_GENERATE ]
yield * 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-04-17 00:01:44 +00:00
// TODO: clean this up so provider specific logic doesnt bleed over
const authInfo = yield * auth . get ( model . providerID ) . pipe ( Effect . orDie )
const isOpenaiOauth = model . providerID === "openai" && authInfo ? . type === "oauth"
2026-04-09 20:25:59 +00:00
2026-04-17 00:01:44 +00:00
const params = {
experimental_telemetry : {
isEnabled : cfg.experimental?.openTelemetry ,
tracer ,
metadata : {
userId : cfg.username ? ? "unknown" ,
} ,
} ,
temperature : 0.3 ,
messages : [
. . . ( isOpenaiOauth
? [ ]
: 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 ` ,
2026-03-24 18:15:23 +00:00
} ,
2026-04-17 00:01:44 +00:00
] ,
model : language ,
2026-05-12 01:42:04 +00:00
schema : Object.assign (
Schema . toStandardSchemaV1 ( GeneratedAgent ) ,
Schema . toStandardJSONSchemaV1 ( GeneratedAgent ) ,
) ,
2026-04-17 00:01:44 +00:00
} satisfies Parameters < typeof generateObject > [ 0 ]
2026-01-19 05:22:31 +00:00
2026-04-17 00:01:44 +00:00
if ( isOpenaiOauth ) {
return yield * Effect . promise ( async ( ) = > {
const result = streamObject ( {
. . . params ,
providerOptions : ProviderTransform.providerOptions ( resolved , {
instructions : system.join ( "\n" ) ,
store : false ,
} ) ,
onError : ( ) = > { } ,
2026-03-24 18:15:23 +00:00
} )
2026-04-17 00:01:44 +00:00
for await ( const part of result . fullStream ) {
if ( part . type === "error" ) throw part . error
}
return result . object
} )
}
2026-03-24 18:15:23 +00:00
2026-04-17 00:01:44 +00:00
return yield * Effect . promise ( ( ) = > generateObject ( params ) . then ( ( r ) = > r . object ) )
} ) ,
} )
} ) ,
)
2026-03-24 18:15:23 +00:00
2026-04-17 00:01:44 +00:00
export const defaultLayer = layer . pipe (
Layer . provide ( Plugin . defaultLayer ) ,
Layer . provide ( Provider . defaultLayer ) ,
Layer . provide ( Auth . defaultLayer ) ,
Layer . provide ( Config . defaultLayer ) ,
Layer . provide ( Skill . defaultLayer ) ,
2026-05-13 11:37:53 +00:00
Layer . provide ( RuntimeFlags . defaultLayer ) ,
2026-04-17 00:01:44 +00:00
)
export * as Agent from "./agent"