chore: effectify agent.ts (#18971)
Co-authored-by: Kit Langton <kit.langton@gmail.com>
This commit is contained in:
parent
2c1d8a90d5
commit
5e684c6e80
1 changed files with 340 additions and 269 deletions
|
|
@ -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,295 +51,364 @@ 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">
|
||||||
const whitelistedDirs = [Truncate.GLOB, ...skillDirs.map((dir) => path.join(dir, "*"))]
|
|
||||||
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",
|
|
||||||
"*.env": "ask",
|
|
||||||
"*.env.*": "ask",
|
|
||||||
"*.env.example": "allow",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const user = Permission.fromConfig(cfg.permission ?? {})
|
|
||||||
|
|
||||||
const result: Record<string, Info> = {
|
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Agent") {}
|
||||||
build: {
|
|
||||||
name: "build",
|
export const layer = Layer.effect(
|
||||||
description: "The default agent. Executes tools based on configured permissions.",
|
Service,
|
||||||
options: {},
|
Effect.gen(function* () {
|
||||||
permission: Permission.merge(
|
const config = () => Effect.promise(() => Config.get())
|
||||||
defaults,
|
const auth = yield* Auth.Service
|
||||||
Permission.fromConfig({
|
|
||||||
question: "allow",
|
const state = yield* InstanceState.make<State>(
|
||||||
plan_enter: "allow",
|
Effect.fn("Agent.state")(function* (ctx) {
|
||||||
}),
|
const cfg = yield* config()
|
||||||
user,
|
const skillDirs = yield* Effect.promise(() => Skill.dirs())
|
||||||
),
|
const whitelistedDirs = [Truncate.GLOB, ...skillDirs.map((dir) => path.join(dir, "*"))]
|
||||||
mode: "primary",
|
|
||||||
native: true,
|
const defaults = Permission.fromConfig({
|
||||||
},
|
"*": "allow",
|
||||||
plan: {
|
doom_loop: "ask",
|
||||||
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({
|
|
||||||
todoread: "deny",
|
|
||||||
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: {
|
external_directory: {
|
||||||
"*": "ask",
|
"*": "ask",
|
||||||
...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])),
|
...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])),
|
||||||
},
|
},
|
||||||
}),
|
question: "deny",
|
||||||
user,
|
plan_enter: "deny",
|
||||||
),
|
plan_exit: "deny",
|
||||||
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.`,
|
// mirrors github.com/github/gitignore Node.gitignore pattern for .env files
|
||||||
prompt: PROMPT_EXPLORE,
|
read: {
|
||||||
options: {},
|
"*": "allow",
|
||||||
mode: "subagent",
|
"*.env": "ask",
|
||||||
native: true,
|
"*.env.*": "ask",
|
||||||
},
|
"*.env.example": "allow",
|
||||||
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 ?? {})) {
|
const user = Permission.fromConfig(cfg.permission ?? {})
|
||||||
if (value.disable) {
|
|
||||||
delete result[key]
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
let item = result[key]
|
|
||||||
if (!item)
|
|
||||||
item = result[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
|
const agents: Record<string, Info> = {
|
||||||
for (const name in result) {
|
build: {
|
||||||
const agent = result[name]
|
name: "build",
|
||||||
const explicit = agent.permission.some((r) => {
|
description: "The default agent. Executes tools based on configured permissions.",
|
||||||
if (r.permission !== "external_directory") return false
|
options: {},
|
||||||
if (r.action !== "deny") return false
|
permission: Permission.merge(
|
||||||
return r.pattern === Truncate.GLOB
|
defaults,
|
||||||
})
|
Permission.fromConfig({
|
||||||
if (explicit) continue
|
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({
|
||||||
|
todoread: "deny",
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
result[name].permission = Permission.merge(
|
for (const [key, value] of Object.entries(cfg.agent ?? {})) {
|
||||||
result[name].permission,
|
if (value.disable) {
|
||||||
Permission.fromConfig({ external_directory: { [Truncate.GLOB]: "allow" } }),
|
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
|
||||||
|
|
||||||
|
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 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"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
get,
|
||||||
|
list,
|
||||||
|
defaultAgent,
|
||||||
|
} satisfies State
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
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))
|
||||||
|
|
||||||
|
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())
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const defaultLayer = layer.pipe(Layer.provide(Auth.layer))
|
||||||
|
|
||||||
|
const runPromise = makeRunPromise(Service, defaultLayer)
|
||||||
|
|
||||||
export async function get(agent: string) {
|
export async function get(agent: string) {
|
||||||
return state().then((x) => x[agent])
|
return runPromise((svc) => svc.get(agent))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function list() {
|
export async function list() {
|
||||||
const cfg = await Config.get()
|
return runPromise((svc) => svc.list())
|
||||||
return pipe(
|
|
||||||
await state(),
|
|
||||||
values(),
|
|
||||||
sortBy(
|
|
||||||
[(x) => (cfg.default_agent ? x.name === cfg.default_agent : x.name === "build"), "desc"],
|
|
||||||
[(x) => x.name, "asc"],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function defaultAgent() {
|
export async function defaultAgent() {
|
||||||
const cfg = await Config.get()
|
return runPromise((svc) => svc.defaultAgent())
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generate(input: { description: string; model?: { providerID: ProviderID; modelID: ModelID } }) {
|
export async function generate(input: { description: string; model?: { providerID: ProviderID; modelID: ModelID } }) {
|
||||||
const cfg = await Config.get()
|
return runPromise((svc) => svc.generate(input))
|
||||||
const defaultModel = input.model ?? (await Provider.defaultModel())
|
|
||||||
const model = await Provider.getModel(defaultModel.providerID, defaultModel.modelID)
|
|
||||||
const language = await Provider.getLanguage(model)
|
|
||||||
|
|
||||||
const system = [PROMPT_GENERATE]
|
|
||||||
await Plugin.trigger("experimental.chat.system.transform", { model }, { system })
|
|
||||||
const existing = await list()
|
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
// TODO: clean this up so provider specific logic doesnt bleed over
|
|
||||||
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)
|
|
||||||
return result.object
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue