Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
19 changes: 9 additions & 10 deletions packages/opencode/src/acp/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,15 +461,13 @@ export namespace ACP {
description: "compact the session",
})

const availableModes = agents
.filter((agent) => agent.mode !== "subagent")
.map((agent) => ({
id: agent.name,
name: agent.name,
description: agent.description,
}))

const currentModeId = availableModes.find((m) => m.name === "build")?.id ?? availableModes[0].id
const availableAgents = agents.filter((agent) => agent.mode !== "subagent")
const availableModes = availableAgents.map((agent) => ({
id: agent.name,
name: agent.name,
description: agent.description,
}))
const currentModeId = availableAgents.find((agent) => agent.default)?.name ?? availableAgents[0]?.name

const mcpServers: Record<string, Config.Mcp> = {}
for (const server of params.mcpServers) {
Expand Down Expand Up @@ -565,13 +563,14 @@ export namespace ACP {
const sessionID = params.sessionId
const session = this.sessionManager.get(sessionID)
const directory = session.cwd
const globalCfg = await Config.get()

const current = session.model
const model = current ?? (await defaultModel(this.config, directory))
if (!current) {
this.sessionManager.setModel(session.id, model)
}
const agent = session.modeId ?? "build"
const agent = session.modeId ?? globalCfg.default_agent!

const parts: Array<
{ type: "text"; text: string } | { type: "file"; url: string; filename: string; mime: string }
Expand Down
3 changes: 3 additions & 0 deletions packages/opencode/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export namespace Agent {
name: z.string(),
description: z.string().optional(),
mode: z.enum(["subagent", "primary", "all"]),
default: z.boolean().optional(),
builtIn: z.boolean(),
topP: z.number().optional(),
temperature: z.number().optional(),
Expand Down Expand Up @@ -224,6 +225,8 @@ export namespace Agent {
if (permission ?? cfg.permission) {
item.permission = mergeAgentPermissions(cfg.permission ?? {}, permission ?? {})
}

if (key === cfg.default_agent) result[key].default = true
}
return result
})
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ const AgentListCommand = cmd({
})

for (const agent of sortedAgents) {
process.stdout.write(`${agent.name} (${agent.mode})${EOL}`)
process.stdout.write(`${agent.name} (${agent.mode}${agent.default ? ", default" : ""})${EOL}`)
}
},
})
Expand Down
6 changes: 4 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 { Config } from "@/config/config"

const TOOL: Record<string, [string, string]> = {
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
Expand Down Expand Up @@ -221,10 +222,11 @@ export const RunCommand = cmd({
}
})()

const cfg = await Config.get()
if (args.command) {
await sdk.session.command({
sessionID,
agent: args.agent || "build",
agent: args.agent || cfg.default_agent!,
model: args.model,
command: args.command,
arguments: message,
Expand All @@ -233,7 +235,7 @@ export const RunCommand = cmd({
const modelParam = args.model ? Provider.parseModel(args.model) : undefined
await sdk.session.prompt({
sessionID,
agent: args.agent || "build",
agent: args.agent || cfg.default_agent!,
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,
})
const { theme } = useTheme()
const colors = createMemo(() => [
Expand Down
30 changes: 25 additions & 5 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ export namespace Config {
return merged
}

function firstPrimaryAgent(agents: Record<string, Agent>): string {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't it still default to build if it is enabled tho, rather than arbitrary first agent

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

for (const [name, config] of Object.entries(agents)) {
if (config.mode !== "subagent" && !config.disable) {
return name
}
}

throw new InvalidError({
path: path.join(Instance.directory, "opencode.jsonc"),
message:
"No available primary agent found. Please ensure at least one primary agent is configured and not disabled.",
})
}

export const state = Instance.state(async () => {
const auth = await Auth.all()
let result = await global()
Expand Down Expand Up @@ -126,13 +140,18 @@ export namespace Config {
result.share = "auto"
}

// Handle migration from autoshare to share field
if (result.autoshare === true && !result.share) {
result.share = "auto"
}

if (!result.keybinds) result.keybinds = Info.shape.keybinds.parse({})

if (
!result.default_agent || // not configured
!(result.default_agent in result.agent) || // not exist
result.agent[result.default_agent]?.disable || // disabled
result.agent[result.default_agent]?.mode === "subagent" // not primary
) {
log.warn(`default agent not available, fallback to the first primary agent`)
result.default_agent = firstPrimaryAgent(result.agent)
}

return {
config: result,
directories,
Expand Down Expand Up @@ -589,6 +608,7 @@ export namespace Config {
.catchall(Agent)
.optional()
.describe("@deprecated Use `agent` field instead."),
default_agent: z.string().optional().describe("Default agent to use when no specific agent is specified"),
agent: z
.object({
plan: Agent.optional(),
Expand Down
6 changes: 4 additions & 2 deletions packages/opencode/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -999,11 +999,13 @@ 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"
const cfg = await Config.get()

let currentAgent = cfg.default_agent!
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
break
}
}
Expand Down
3 changes: 2 additions & 1 deletion packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -832,7 +832,8 @@ export namespace SessionPrompt {
}

async function createUserMessage(input: PromptInput) {
const agent = await Agent.get(input.agent ?? "build")
const cfg = await Config.get()
const agent = await Agent.get(input.agent ?? cfg.default_agent!)
const info: MessageV2.Info = {
id: input.messageID ?? Identifier.ascending("message"),
role: "user",
Expand Down
85 changes: 85 additions & 0 deletions packages/opencode/test/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -501,3 +501,88 @@ test("deduplicates duplicate plugins from global and local configs", async () =>
},
})
})

test("uses specified default_agent when provided", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
default_agent: "custom",
agent: {
build: {
model: "test/model",
description: "build agent",
},
custom: {
model: "test/model",
description: "custom agent",
},
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const config = await Config.get()
expect(config.default_agent).toBe("custom")
},
})
})

test("falls back when default_agent is disabled", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
default_agent: "build",
agent: {
build: {
model: "test/model",
description: "build agent",
disable: true,
},
custom: {
model: "test/model",
description: "custom agent",
mode: "primary",
},
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const config = await Config.get()
expect(config.default_agent).toBe("custom")
},
})
})

test("falls back when configured default_agent does not exist", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
default_agent: "nonexistent-agent",
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const config = await Config.get()
expect(config.default_agent).toBe("build")
},
})
})
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 @@ -1266,6 +1266,10 @@ export type Config = {
plan?: AgentConfig
[key: string]: AgentConfig | undefined
}
/**
* Default agent to use when no specific agent is specified
*/
default_agent?: string
/**
* Agent configuration, see https://opencode.ai/docs/agent
*/
Expand Down Expand Up @@ -1594,6 +1598,7 @@ export type Agent = {
name: string
description?: string
mode: "subagent" | "primary" | "all"
default?: boolean
builtIn: boolean
topP?: number
temperature?: number
Expand Down