Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions github/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 }}
Expand Down
28 changes: 27 additions & 1 deletion github/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -578,16 +582,38 @@ async function summarize(response: string) {
}
}

async function resolveAgent(): Promise<string | undefined> {
const envAgent = useEnvAgent()
if (!envAgent) return undefined

// Validate the agent exists and is a primary agent
const agents = await client.agent.list<true>()
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<true>({
path: session,
body: {
providerID,
modelID,
agent: "build",
agent,
parts: [
{
type: "text",
Expand Down
6 changes: 4 additions & 2 deletions packages/opencode/src/acp/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<string, Config.Mcp> = {}
for (const server of params.mcpServers) {
Expand Down Expand Up @@ -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 }
Expand Down
23 changes: 23 additions & 0 deletions packages/opencode/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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(),
Expand Down Expand Up @@ -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
})

Expand All @@ -256,6 +273,12 @@ export namespace Agent {
return state().then((x) => Object.values(x))
}

export async function defaultAgent(): Promise<string> {
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())
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
28 changes: 26 additions & 2 deletions packages/opencode/src/cli/cmd/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, [string, string]> = {
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
Expand Down Expand Up @@ -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,
Expand All @@ -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 }],
})
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/tui/context/local.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => [
Expand Down
6 changes: 6 additions & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions packages/opencode/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
6 changes: 3 additions & 3 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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, ""))
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions packages/sdk/js/src/v2/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -1767,6 +1771,7 @@ export type Agent = {
mode: "subagent" | "primary" | "all"
native?: boolean
hidden?: boolean
default?: boolean
topP?: number
temperature?: number
color?: string
Expand Down
7 changes: 7 additions & 0 deletions packages/sdk/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -7986,6 +7986,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"
Expand Down Expand Up @@ -8985,6 +8989,9 @@
"hidden": {
"type": "boolean"
},
"default": {
"type": "boolean"
},
"topP": {
"type": "number"
},
Expand Down
17 changes: 17 additions & 0 deletions packages/web/src/content/docs/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/content/docs/github.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down