summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/opencode/src/agent/agent.ts718
1 files changed, 359 insertions, 359 deletions
diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts
index 54ca48455..07f742fe1 100644
--- a/packages/opencode/src/agent/agent.ts
+++ b/packages/opencode/src/agent/agent.ts
@@ -24,389 +24,389 @@ import { InstanceState } from "@/effect"
import * as Option from "effect/Option"
import * as OtelTracer from "@effect/opentelemetry/Tracer"
-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: Permission.Ruleset.zod,
- model: z
- .object({
- modelID: ModelID.zod,
- providerID: ProviderID.zod,
- })
- .optional(),
- variant: 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>
+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: Permission.Ruleset.zod,
+ model: z
+ .object({
+ modelID: ModelID.zod,
+ providerID: ProviderID.zod,
+ })
+ .optional(),
+ variant: 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>
- 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
- }>
- }
+export interface Interface {
+ readonly get: (agent: string) => Effect.Effect<Info>
+ readonly list: () => Effect.Effect<Info[]>
+ readonly defaultAgent: () => Effect.Effect<string>
+ readonly generate: (input: {
+ description: string
+ model?: { providerID: ProviderID; modelID: ModelID }
+ }) => Effect.Effect<{
+ identifier: string
+ whenToUse: string
+ systemPrompt: string
+ }>
+}
- type State = Omit<Interface, "generate">
+type State = Omit<Interface, "generate">
- export class Service extends Context.Service<Service, Interface>()("@opencode/Agent") {}
+export class Service extends Context.Service<Service, Interface>()("@opencode/Agent") {}
- 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
+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
- const state = yield* InstanceState.make<State>(
- Effect.fn("Agent.state")(function* (_ctx) {
- const cfg = yield* config.get()
- const skillDirs = yield* skill.dirs()
- const whitelistedDirs = [Truncate.GLOB, ...skillDirs.map((dir) => path.join(dir, "*"))]
+ const state = yield* InstanceState.make<State>(
+ Effect.fn("Agent.state")(function* (_ctx) {
+ const cfg = yield* config.get()
+ const skillDirs = yield* skill.dirs()
+ const whitelistedDirs = [Truncate.GLOB, ...skillDirs.map((dir) => path.join(dir, "*"))]
- const defaults = Permission.fromConfig({
+ 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",
+ // mirrors github.com/github/gitignore Node.gitignore pattern for .env files
+ read: {
"*": "allow",
- doom_loop: "ask",
- external_directory: {
- "*": "ask",
- ...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "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",
- },
- })
+ "*.env": "ask",
+ "*.env.*": "ask",
+ "*.env.example": "allow",
+ },
+ })
- const user = Permission.fromConfig(cfg.permission ?? {})
+ const user = Permission.fromConfig(cfg.permission ?? {})
- 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({
+ 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",
- }),
- user,
- ),
- prompt: PROMPT_SUMMARY,
- },
- }
+ [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,
+ },
+ }
- 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 ?? {}))
+ 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 ?? {}))
+ }
- // 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
+ // 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
- agents[name].permission = Permission.merge(
- agents[name].permission,
- Permission.fromConfig({ external_directory: { [Truncate.GLOB]: "allow" } }),
- )
- }
+ agents[name].permission = Permission.merge(
+ agents[name].permission,
+ Permission.fromConfig({ external_directory: { [Truncate.GLOB]: "allow" } }),
+ )
+ }
- const get = Effect.fnUntraced(function* (agent: string) {
- return agents[agent]
- })
+ const get = Effect.fnUntraced(function* (agent: string) {
+ return agents[agent]
+ })
- 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"],
- ),
- )
- })
+ 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"],
+ ),
+ )
+ })
- const defaultAgent = Effect.fnUntraced(function* () {
- 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`)
- 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
- })
+ const defaultAgent = Effect.fnUntraced(function* () {
+ 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`)
+ 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
+ })
- return {
- get,
- list,
- defaultAgent,
- } satisfies State
- }),
- )
+ return {
+ get,
+ list,
+ defaultAgent,
+ } satisfies State
+ }),
+ )
- 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.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
+ 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.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
- const system = [PROMPT_GENERATE]
- yield* plugin.trigger("experimental.chat.system.transform", { model: resolved }, { system })
- const existing = yield* InstanceState.useEffect(state, (s) => s.list())
+ const system = [PROMPT_GENERATE]
+ yield* plugin.trigger("experimental.chat.system.transform", { model: resolved }, { system })
+ const existing = yield* InstanceState.useEffect(state, (s) => s.list())
- // 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"
+ // 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"
- const params = {
- experimental_telemetry: {
- isEnabled: cfg.experimental?.openTelemetry,
- tracer,
- metadata: {
- userId: cfg.username ?? "unknown",
- },
+ 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`,
},
- 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`,
- },
- ],
- model: language,
- schema: z.object({
- identifier: z.string(),
- whenToUse: z.string(),
- systemPrompt: z.string(),
- }),
- } satisfies Parameters<typeof generateObject>[0]
+ ],
+ model: language,
+ schema: z.object({
+ identifier: z.string(),
+ whenToUse: z.string(),
+ systemPrompt: z.string(),
+ }),
+ } satisfies Parameters<typeof generateObject>[0]
- if (isOpenaiOauth) {
- return yield* Effect.promise(async () => {
- const result = streamObject({
- ...params,
- providerOptions: ProviderTransform.providerOptions(resolved, {
- instructions: system.join("\n"),
- store: false,
- }),
- onError: () => {},
- })
- for await (const part of result.fullStream) {
- if (part.type === "error") throw part.error
- }
- return result.object
+ if (isOpenaiOauth) {
+ return yield* Effect.promise(async () => {
+ const result = streamObject({
+ ...params,
+ providerOptions: ProviderTransform.providerOptions(resolved, {
+ instructions: system.join("\n"),
+ 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))
- }),
- })
- }),
- )
+ return yield* Effect.promise(() => generateObject(params).then((r) => r.object))
+ }),
+ })
+ }),
+)
- 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),
- )
-}
+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),
+)
+
+export * as Agent from "./agent"