diff --git a/github/action.yml b/github/action.yml index cf276b51c..57e26d856 100644 --- a/github/action.yml +++ b/github/action.yml @@ -9,6 +9,10 @@ inputs: description: "Model to use" required: true + agent: + description: "Agent to use. Must be a primary agent. Falls back to default_agent from config or 'build' if not found." + required: false + share: description: "Share the opencode session (defaults to true for public repos)" required: false @@ -62,6 +66,7 @@ runs: run: opencode github run env: MODEL: ${{ inputs.model }} + AGENT: ${{ inputs.agent }} SHARE: ${{ inputs.share }} PROMPT: ${{ inputs.prompt }} USE_GITHUB_TOKEN: ${{ inputs.use_github_token }} diff --git a/github/index.ts b/github/index.ts index 6d826326e..7f6018232 100644 --- a/github/index.ts +++ b/github/index.ts @@ -318,6 +318,10 @@ function useEnvRunUrl() { return `/${repo.owner}/${repo.repo}/actions/runs/${runId}` } +function useEnvAgent() { + return process.env["AGENT"] || undefined +} + function useEnvShare() { const value = process.env["SHARE"] if (!value) return undefined @@ -578,16 +582,38 @@ async function summarize(response: string) { } } +async function resolveAgent(): Promise { + const envAgent = useEnvAgent() + if (!envAgent) return undefined + + // Validate the agent exists and is a primary agent + const agents = await client.agent.list() + const agent = agents.data?.find((a) => a.name === envAgent) + + if (!agent) { + console.warn(`agent "${envAgent}" not found. Falling back to default agent`) + return undefined + } + + if (agent.mode === "subagent") { + console.warn(`agent "${envAgent}" is a subagent, not a primary agent. Falling back to default agent`) + return undefined + } + + return envAgent +} + async function chat(text: string, files: PromptFiles = []) { console.log("Sending message to opencode...") const { providerID, modelID } = useEnvModel() + const agent = await resolveAgent() const chat = await client.session.chat({ path: session, body: { providerID, modelID, - agent: "build", + agent, parts: [ { type: "text", diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 2817adf5d..e6419dd76 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -22,6 +22,7 @@ import { Log } from "../util/log" import { ACPSessionManager } from "./session" import type { ACPConfig, ACPSessionState } from "./types" import { Provider } from "../provider/provider" +import { Agent as AgentModule } from "../agent/agent" import { Installation } from "@/installation" import { MessageV2 } from "@/session/message-v2" import { Config } from "@/config/config" @@ -705,7 +706,8 @@ export namespace ACP { description: agent.description, })) - const currentModeId = availableModes.find((m) => m.name === "build")?.id ?? availableModes[0].id + const defaultAgentName = await AgentModule.defaultAgent() + const currentModeId = availableModes.find((m) => m.name === defaultAgentName)?.id ?? availableModes[0].id const mcpServers: Record = {} for (const server of params.mcpServers) { @@ -807,7 +809,7 @@ export namespace ACP { if (!current) { this.sessionManager.setModel(session.id, model) } - const agent = session.modeId ?? "build" + const agent = session.modeId ?? (await AgentModule.defaultAgent()) const parts: Array< { type: "text"; text: string } | { type: "file"; url: string; filename: string; mime: string } diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index add120f91..26f241fab 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -5,6 +5,9 @@ import { generateObject, type ModelMessage } from "ai" import { SystemPrompt } from "../session/system" import { Instance } from "../project/instance" import { mergeDeep } from "remeda" +import { Log } from "../util/log" + +const log = Log.create({ service: "agent" }) import PROMPT_GENERATE from "./generate.txt" import PROMPT_COMPACTION from "./prompt/compaction.txt" @@ -20,6 +23,7 @@ export namespace Agent { mode: z.enum(["subagent", "primary", "all"]), native: z.boolean().optional(), hidden: z.boolean().optional(), + default: z.boolean().optional(), topP: z.number().optional(), temperature: z.number().optional(), color: z.string().optional(), @@ -245,6 +249,19 @@ export namespace Agent { item.permission = mergeAgentPermissions(cfg.permission ?? {}, permission ?? {}) } } + + // Mark the default agent + const defaultName = cfg.default_agent ?? "build" + const defaultCandidate = result[defaultName] + if (defaultCandidate && defaultCandidate.mode !== "subagent") { + defaultCandidate.default = true + } else { + // Fall back to "build" if configured default is invalid + if (result["build"]) { + result["build"].default = true + } + } + return result }) @@ -256,6 +273,12 @@ export namespace Agent { return state().then((x) => Object.values(x)) } + export async function defaultAgent(): Promise { + const agents = await state() + const defaultCandidate = Object.values(agents).find((a) => a.default) + return defaultCandidate?.name ?? "build" + } + export async function generate(input: { description: string; model?: { providerID: string; modelID: string } }) { const cfg = await Config.get() const defaultModel = input.model ?? (await Provider.defaultModel()) diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts index f4f026d4c..26340044c 100644 --- a/packages/opencode/src/cli/cmd/github.ts +++ b/packages/opencode/src/cli/cmd/github.ts @@ -762,7 +762,7 @@ export const GithubRunCommand = cmd({ providerID, modelID, }, - agent: "build", + // agent is omitted - server will use default_agent from config or fall back to "build" parts: [ { id: Identifier.ascending("part"), diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 3a0b2f23f..0c371b864 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -10,6 +10,7 @@ import { select } from "@clack/prompts" import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2" import { Server } from "../../server/server" import { Provider } from "../../provider/provider" +import { Agent } from "../../agent/agent" const TOOL: Record = { todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD], @@ -223,10 +224,33 @@ export const RunCommand = cmd({ } })() + // Validate agent if specified + const resolvedAgent = await (async () => { + if (!args.agent) return undefined + const agent = await Agent.get(args.agent) + if (!agent) { + UI.println( + UI.Style.TEXT_WARNING_BOLD + "!", + UI.Style.TEXT_NORMAL, + `agent "${args.agent}" not found. Falling back to default agent`, + ) + return undefined + } + if (agent.mode === "subagent") { + UI.println( + UI.Style.TEXT_WARNING_BOLD + "!", + UI.Style.TEXT_NORMAL, + `agent "${args.agent}" is a subagent, not a primary agent. Falling back to default agent`, + ) + return undefined + } + return args.agent + })() + if (args.command) { await sdk.session.command({ sessionID, - agent: args.agent || "build", + agent: resolvedAgent, model: args.model, command: args.command, arguments: message, @@ -235,7 +259,7 @@ export const RunCommand = cmd({ const modelParam = args.model ? Provider.parseModel(args.model) : undefined await sdk.session.prompt({ sessionID, - agent: args.agent || "build", + agent: resolvedAgent, model: modelParam, parts: [...fileParts, { type: "text", text: message }], }) diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index f04b79685..55c04621e 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -56,7 +56,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const [agentStore, setAgentStore] = createStore<{ current: string }>({ - current: agents()[0].name, + current: agents().find((x) => x.default)?.name ?? agents()[0].name, }) const { theme } = useTheme() const colors = createMemo(() => [ diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index a01cc832a..031bdd31b 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -666,6 +666,12 @@ export namespace Config { .string() .describe("Small model to use for tasks like title generation in the format of provider/model") .optional(), + default_agent: z + .string() + .optional() + .describe( + "Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.", + ), username: z .string() .optional() diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 77bf5085b..2f4b3b221 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -1060,11 +1060,11 @@ export namespace Server { const sessionID = c.req.valid("param").sessionID const body = c.req.valid("json") const msgs = await Session.messages({ sessionID }) - let currentAgent = "build" + let currentAgent = await Agent.defaultAgent() for (let i = msgs.length - 1; i >= 0; i--) { const info = msgs[i].info if (info.role === "user") { - currentAgent = info.agent || "build" + currentAgent = info.agent || (await Agent.defaultAgent()) break } } diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index cbb3eedf3..ebd54a6c8 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -715,7 +715,7 @@ export namespace SessionPrompt { } async function createUserMessage(input: PromptInput) { - const agent = await Agent.get(input.agent ?? "build") + const agent = await Agent.get(input.agent ?? (await Agent.defaultAgent())) const info: MessageV2.Info = { id: input.messageID ?? Identifier.ascending("message"), role: "user", @@ -1282,7 +1282,7 @@ export namespace SessionPrompt { export async function command(input: CommandInput) { log.info("command", input) const command = await Command.get(input.command) - const agentName = command.agent ?? input.agent ?? "build" + const agentName = command.agent ?? input.agent ?? (await Agent.defaultAgent()) const raw = input.arguments.match(argsRegex) ?? [] const args = raw.map((arg) => arg.replace(quoteTrimRegex, "")) @@ -1425,7 +1425,7 @@ export namespace SessionPrompt { time: { created: Date.now(), }, - agent: input.message.info.role === "user" ? input.message.info.agent : "build", + agent: input.message.info.role === "user" ? input.message.info.agent : await Agent.defaultAgent(), model: { providerID: input.providerID, modelID: input.modelID, diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index cdbdfdfda..1b43d3f48 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1414,6 +1414,10 @@ export type Config = { * Small model to use for tasks like title generation in the format of provider/model */ small_model?: string + /** + * Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid. + */ + default_agent?: string /** * Custom username to display in conversations instead of system username */ @@ -1767,6 +1771,7 @@ export type Agent = { mode: "subagent" | "primary" | "all" native?: boolean hidden?: boolean + default?: boolean topP?: number temperature?: number color?: string diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index eeb81a844..f33d20069 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -8153,6 +8153,10 @@ "description": "Small model to use for tasks like title generation in the format of provider/model", "type": "string" }, + "default_agent": { + "description": "Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.", + "type": "string" + }, "username": { "description": "Custom username to display in conversations instead of system username", "type": "string" @@ -9152,6 +9156,9 @@ "hidden": { "type": "boolean" }, + "default": { + "type": "boolean" + }, "topP": { "type": "number" }, diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index 302d79d17..5ba22ff2d 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -194,6 +194,23 @@ You can also define agents using markdown files in `~/.config/opencode/agent/` o --- +### Default agent + +You can set the default agent using the `default_agent` option. This determines which agent is used when none is explicitly specified. + +```json title="opencode.json" +{ + "$schema": "https://opencode.ai/config.json", + "default_agent": "plan" +} +``` + +The default agent must be a primary agent (not a subagent). This can be a built-in agent like `"build"` or `"plan"`, or a [custom agent](/docs/agents) you've defined. If the specified agent doesn't exist or is a subagent, OpenCode will fall back to `"build"` with a warning. + +This setting applies across all interfaces: TUI, CLI (`opencode run`), desktop app, and GitHub Action. + +--- + ### Sharing You can configure the [share](/docs/share) feature through the `share` option. diff --git a/packages/web/src/content/docs/github.mdx b/packages/web/src/content/docs/github.mdx index a38df68f4..1d6078840 100644 --- a/packages/web/src/content/docs/github.mdx +++ b/packages/web/src/content/docs/github.mdx @@ -81,6 +81,7 @@ Or you can set it up manually. ## Configuration - `model`: The model to use with OpenCode. Takes the format of `provider/model`. This is **required**. +- `agent`: The agent to use. Must be a primary agent. Falls back to `default_agent` from config or `"build"` if not found. - `share`: Whether to share the OpenCode session. Defaults to **true** for public repositories. - `prompt`: Optional custom prompt to override the default behavior. Use this to customize how OpenCode processes requests. - `token`: Optional GitHub access token for performing operations such as creating comments, committing changes, and opening pull requests. By default, OpenCode uses the installation access token from the OpenCode GitHub App, so commits, comments, and pull requests appear as coming from the app.