chore: effectify agent.ts (#18971)

Co-authored-by: Kit Langton <kit.langton@gmail.com>
This commit is contained in:
Aiden Cline
2026-03-24 18:15:23 +00:00
committed by GitHub
co-authored by Kit Langton
parent 2c1d8a90d5
commit 5e684c6e80
+117 -46
View File
@@ -3,7 +3,6 @@ import z from "zod"
import { Provider } from "../provider/provider" import { Provider } from "../provider/provider"
import { ModelID, ProviderID } from "../provider/schema" import { ModelID, ProviderID } from "../provider/schema"
import { generateObject, streamObject, type ModelMessage } from "ai" import { generateObject, streamObject, type ModelMessage } from "ai"
import { SystemPrompt } from "../session/system"
import { Instance } from "../project/instance" import { Instance } from "../project/instance"
import { Truncate } from "../tool/truncate" import { Truncate } from "../tool/truncate"
import { Auth } from "../auth" import { Auth } from "../auth"
@@ -20,6 +19,9 @@ import { Global } from "@/global"
import path from "path" import path from "path"
import { Plugin } from "@/plugin" import { Plugin } from "@/plugin"
import { Skill } from "../skill" import { Skill } from "../skill"
import { Effect, ServiceMap, Layer } from "effect"
import { InstanceState } from "@/effect/instance-state"
import { makeRunPromise } from "@/effect/run-service"
export namespace Agent { export namespace Agent {
export const Info = z export const Info = z
@@ -49,11 +51,36 @@ export namespace Agent {
}) })
export type Info = z.infer<typeof Info> export type Info = z.infer<typeof Info>
const state = Instance.state(async () => { export interface Interface {
const cfg = await Config.get() 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
}>
}
const skillDirs = await Skill.dirs() type State = Omit<Interface, "generate">
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 whitelistedDirs = [Truncate.GLOB, ...skillDirs.map((dir) => path.join(dir, "*"))]
const defaults = Permission.fromConfig({ const defaults = Permission.fromConfig({
"*": "allow", "*": "allow",
doom_loop: "ask", doom_loop: "ask",
@@ -72,9 +99,10 @@ export namespace Agent {
"*.env.example": "allow", "*.env.example": "allow",
}, },
}) })
const user = Permission.fromConfig(cfg.permission ?? {}) const user = Permission.fromConfig(cfg.permission ?? {})
const result: Record<string, Info> = { const agents: Record<string, Info> = {
build: { build: {
name: "build", name: "build",
description: "The default agent. Executes tools based on configured permissions.", description: "The default agent. Executes tools based on configured permissions.",
@@ -105,7 +133,8 @@ export namespace Agent {
edit: { edit: {
"*": "deny", "*": "deny",
[path.join(".opencode", "plans", "*.md")]: "allow", [path.join(".opencode", "plans", "*.md")]: "allow",
[path.relative(Instance.worktree, path.join(Global.Path.data, path.join("plans", "*.md")))]: "allow", [path.relative(Instance.worktree, path.join(Global.Path.data, path.join("plans", "*.md")))]:
"allow",
}, },
}), }),
user, user,
@@ -205,12 +234,12 @@ export namespace Agent {
for (const [key, value] of Object.entries(cfg.agent ?? {})) { for (const [key, value] of Object.entries(cfg.agent ?? {})) {
if (value.disable) { if (value.disable) {
delete result[key] delete agents[key]
continue continue
} }
let item = result[key] let item = agents[key]
if (!item) if (!item)
item = result[key] = { item = agents[key] = {
name: key, name: key,
mode: "all", mode: "all",
permission: Permission.merge(defaults, user), permission: Permission.merge(defaults, user),
@@ -233,8 +262,8 @@ export namespace Agent {
} }
// Ensure Truncate.GLOB is allowed unless explicitly configured // Ensure Truncate.GLOB is allowed unless explicitly configured
for (const name in result) { for (const name in agents) {
const agent = result[name] const agent = agents[name]
const explicit = agent.permission.some((r) => { const explicit = agent.permission.some((r) => {
if (r.permission !== "external_directory") return false if (r.permission !== "external_directory") return false
if (r.action !== "deny") return false if (r.action !== "deny") return false
@@ -242,57 +271,74 @@ export namespace Agent {
}) })
if (explicit) continue if (explicit) continue
result[name].permission = Permission.merge( agents[name].permission = Permission.merge(
result[name].permission, agents[name].permission,
Permission.fromConfig({ external_directory: { [Truncate.GLOB]: "allow" } }), Permission.fromConfig({ external_directory: { [Truncate.GLOB]: "allow" } }),
) )
} }
return result const get = Effect.fnUntraced(function* (agent: string) {
return agents[agent]
}) })
export async function get(agent: string) { const list = Effect.fnUntraced(function* () {
return state().then((x) => x[agent]) const cfg = yield* config()
}
export async function list() {
const cfg = await Config.get()
return pipe( return pipe(
await state(), agents,
values(), values(),
sortBy( sortBy(
[(x) => (cfg.default_agent ? x.name === cfg.default_agent : x.name === "build"), "desc"], [(x) => (cfg.default_agent ? x.name === cfg.default_agent : x.name === "build"), "desc"],
[(x) => x.name, "asc"], [(x) => x.name, "asc"],
), ),
) )
} })
export async function defaultAgent() { const defaultAgent = Effect.fnUntraced(function* () {
const cfg = await Config.get() const c = yield* config()
const agents = await state() if (c.default_agent) {
const agent = agents[c.default_agent]
if (cfg.default_agent) { if (!agent) throw new Error(`default agent "${c.default_agent}" not found`)
const agent = agents[cfg.default_agent] if (agent.mode === "subagent") throw new Error(`default agent "${c.default_agent}" is a subagent`)
if (!agent) throw new Error(`default agent "${cfg.default_agent}" not found`) if (agent.hidden === true) throw new Error(`default agent "${c.default_agent}" is hidden`)
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 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 primaryVisible = Object.values(agents).find((a) => a.mode !== "subagent" && a.hidden !== true) return {
if (!primaryVisible) throw new Error("no primary visible agent found") get,
return primaryVisible.name list,
} defaultAgent,
} satisfies State
}),
)
export async function generate(input: { description: string; model?: { providerID: ProviderID; modelID: ModelID } }) { return Service.of({
const cfg = await Config.get() get: Effect.fn("Agent.get")(function* (agent: string) {
const defaultModel = input.model ?? (await Provider.defaultModel()) return yield* InstanceState.useEffect(state, (s) => s.get(agent))
const model = await Provider.getModel(defaultModel.providerID, defaultModel.modelID) }),
const language = await Provider.getLanguage(model) 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))
const system = [PROMPT_GENERATE] const system = [PROMPT_GENERATE]
await Plugin.trigger("experimental.chat.system.transform", { model }, { system }) yield* Effect.promise(() =>
const existing = await list() Plugin.trigger("experimental.chat.system.transform", { model: resolved }, { system }),
)
const existing = yield* InstanceState.useEffect(state, (s) => s.list())
const params = { const params = {
experimental_telemetry: { experimental_telemetry: {
@@ -323,10 +369,12 @@ export namespace Agent {
} satisfies Parameters<typeof generateObject>[0] } satisfies Parameters<typeof generateObject>[0]
// TODO: clean this up so provider specific logic doesnt bleed over // TODO: clean this up so provider specific logic doesnt bleed over
if (defaultModel.providerID === "openai" && (await Auth.get(defaultModel.providerID))?.type === "oauth") { 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({ const result = streamObject({
...params, ...params,
providerOptions: ProviderTransform.providerOptions(model, { providerOptions: ProviderTransform.providerOptions(resolved, {
store: false, store: false,
}), }),
onError: () => {}, onError: () => {},
@@ -335,9 +383,32 @@ export namespace Agent {
if (part.type === "error") throw part.error if (part.type === "error") throw part.error
} }
return result.object return result.object
})
} }
const result = await generateObject(params) return yield* Effect.promise(() => generateObject(params).then((r) => r.object))
return result.object }),
})
}),
)
export const defaultLayer = layer.pipe(Layer.provide(Auth.layer))
const runPromise = makeRunPromise(Service, defaultLayer)
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))
} }
} }