Skip to content

Commit 8f6c884

Browse files
elithrarobserverw
authored andcommitted
feat: support configuring a default_agent across all API/user surfaces (anomalyco#5843)
Co-authored-by: observerw <observerw@users.noreply.github.com>
1 parent da6e0e6 commit 8f6c884

File tree

14 files changed

+128
-12
lines changed

14 files changed

+128
-12
lines changed

github/action.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ inputs:
99
description: "Model to use"
1010
required: true
1111

12+
agent:
13+
description: "Agent to use. Must be a primary agent. Falls back to default_agent from config or 'build' if not found."
14+
required: false
15+
1216
share:
1317
description: "Share the opencode session (defaults to true for public repos)"
1418
required: false
@@ -62,6 +66,7 @@ runs:
6266
run: opencode github run
6367
env:
6468
MODEL: ${{ inputs.model }}
69+
AGENT: ${{ inputs.agent }}
6570
SHARE: ${{ inputs.share }}
6671
PROMPT: ${{ inputs.prompt }}
6772
USE_GITHUB_TOKEN: ${{ inputs.use_github_token }}

github/index.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,10 @@ function useEnvRunUrl() {
318318
return `/${repo.owner}/${repo.repo}/actions/runs/${runId}`
319319
}
320320

321+
function useEnvAgent() {
322+
return process.env["AGENT"] || undefined
323+
}
324+
321325
function useEnvShare() {
322326
const value = process.env["SHARE"]
323327
if (!value) return undefined
@@ -578,16 +582,38 @@ async function summarize(response: string) {
578582
}
579583
}
580584

585+
async function resolveAgent(): Promise<string | undefined> {
586+
const envAgent = useEnvAgent()
587+
if (!envAgent) return undefined
588+
589+
// Validate the agent exists and is a primary agent
590+
const agents = await client.agent.list<true>()
591+
const agent = agents.data?.find((a) => a.name === envAgent)
592+
593+
if (!agent) {
594+
console.warn(`agent "${envAgent}" not found. Falling back to default agent`)
595+
return undefined
596+
}
597+
598+
if (agent.mode === "subagent") {
599+
console.warn(`agent "${envAgent}" is a subagent, not a primary agent. Falling back to default agent`)
600+
return undefined
601+
}
602+
603+
return envAgent
604+
}
605+
581606
async function chat(text: string, files: PromptFiles = []) {
582607
console.log("Sending message to opencode...")
583608
const { providerID, modelID } = useEnvModel()
609+
const agent = await resolveAgent()
584610

585611
const chat = await client.session.chat<true>({
586612
path: session,
587613
body: {
588614
providerID,
589615
modelID,
590-
agent: "build",
616+
agent,
591617
parts: [
592618
{
593619
type: "text",

packages/opencode/src/acp/agent.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { Log } from "../util/log"
2222
import { ACPSessionManager } from "./session"
2323
import type { ACPConfig, ACPSessionState } from "./types"
2424
import { Provider } from "../provider/provider"
25+
import { Agent as AgentModule } from "../agent/agent"
2526
import { Installation } from "@/installation"
2627
import { MessageV2 } from "@/session/message-v2"
2728
import { Config } from "@/config/config"
@@ -705,7 +706,8 @@ export namespace ACP {
705706
description: agent.description,
706707
}))
707708

708-
const currentModeId = availableModes.find((m) => m.name === "build")?.id ?? availableModes[0].id
709+
const defaultAgentName = await AgentModule.defaultAgent()
710+
const currentModeId = availableModes.find((m) => m.name === defaultAgentName)?.id ?? availableModes[0].id
709711

710712
const mcpServers: Record<string, Config.Mcp> = {}
711713
for (const server of params.mcpServers) {
@@ -807,7 +809,7 @@ export namespace ACP {
807809
if (!current) {
808810
this.sessionManager.setModel(session.id, model)
809811
}
810-
const agent = session.modeId ?? "build"
812+
const agent = session.modeId ?? (await AgentModule.defaultAgent())
811813

812814
const parts: Array<
813815
{ type: "text"; text: string } | { type: "file"; url: string; filename: string; mime: string }

packages/opencode/src/agent/agent.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import { generateObject, type ModelMessage } from "ai"
55
import { SystemPrompt } from "../session/system"
66
import { Instance } from "../project/instance"
77
import { mergeDeep } from "remeda"
8+
import { Log } from "../util/log"
9+
10+
const log = Log.create({ service: "agent" })
811

912
import PROMPT_GENERATE from "./generate.txt"
1013
import PROMPT_COMPACTION from "./prompt/compaction.txt"
@@ -20,6 +23,7 @@ export namespace Agent {
2023
mode: z.enum(["subagent", "primary", "all"]),
2124
native: z.boolean().optional(),
2225
hidden: z.boolean().optional(),
26+
default: z.boolean().optional(),
2327
topP: z.number().optional(),
2428
temperature: z.number().optional(),
2529
color: z.string().optional(),
@@ -245,6 +249,19 @@ export namespace Agent {
245249
item.permission = mergeAgentPermissions(cfg.permission ?? {}, permission ?? {})
246250
}
247251
}
252+
253+
// Mark the default agent
254+
const defaultName = cfg.default_agent ?? "build"
255+
const defaultCandidate = result[defaultName]
256+
if (defaultCandidate && defaultCandidate.mode !== "subagent") {
257+
defaultCandidate.default = true
258+
} else {
259+
// Fall back to "build" if configured default is invalid
260+
if (result["build"]) {
261+
result["build"].default = true
262+
}
263+
}
264+
248265
return result
249266
})
250267

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

276+
export async function defaultAgent(): Promise<string> {
277+
const agents = await state()
278+
const defaultCandidate = Object.values(agents).find((a) => a.default)
279+
return defaultCandidate?.name ?? "build"
280+
}
281+
259282
export async function generate(input: { description: string; model?: { providerID: string; modelID: string } }) {
260283
const cfg = await Config.get()
261284
const defaultModel = input.model ?? (await Provider.defaultModel())

packages/opencode/src/cli/cmd/github.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -762,7 +762,7 @@ export const GithubRunCommand = cmd({
762762
providerID,
763763
modelID,
764764
},
765-
agent: "build",
765+
// agent is omitted - server will use default_agent from config or fall back to "build"
766766
parts: [
767767
{
768768
id: Identifier.ascending("part"),

packages/opencode/src/cli/cmd/run.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { select } from "@clack/prompts"
1010
import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2"
1111
import { Server } from "../../server/server"
1212
import { Provider } from "../../provider/provider"
13+
import { Agent } from "../../agent/agent"
1314

1415
const TOOL: Record<string, [string, string]> = {
1516
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
@@ -223,10 +224,33 @@ export const RunCommand = cmd({
223224
}
224225
})()
225226

227+
// Validate agent if specified
228+
const resolvedAgent = await (async () => {
229+
if (!args.agent) return undefined
230+
const agent = await Agent.get(args.agent)
231+
if (!agent) {
232+
UI.println(
233+
UI.Style.TEXT_WARNING_BOLD + "!",
234+
UI.Style.TEXT_NORMAL,
235+
`agent "${args.agent}" not found. Falling back to default agent`,
236+
)
237+
return undefined
238+
}
239+
if (agent.mode === "subagent") {
240+
UI.println(
241+
UI.Style.TEXT_WARNING_BOLD + "!",
242+
UI.Style.TEXT_NORMAL,
243+
`agent "${args.agent}" is a subagent, not a primary agent. Falling back to default agent`,
244+
)
245+
return undefined
246+
}
247+
return args.agent
248+
})()
249+
226250
if (args.command) {
227251
await sdk.session.command({
228252
sessionID,
229-
agent: args.agent || "build",
253+
agent: resolvedAgent,
230254
model: args.model,
231255
command: args.command,
232256
arguments: message,
@@ -235,7 +259,7 @@ export const RunCommand = cmd({
235259
const modelParam = args.model ? Provider.parseModel(args.model) : undefined
236260
await sdk.session.prompt({
237261
sessionID,
238-
agent: args.agent || "build",
262+
agent: resolvedAgent,
239263
model: modelParam,
240264
parts: [...fileParts, { type: "text", text: message }],
241265
})

packages/opencode/src/cli/cmd/tui/context/local.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
5656
const [agentStore, setAgentStore] = createStore<{
5757
current: string
5858
}>({
59-
current: agents()[0].name,
59+
current: agents().find((x) => x.default)?.name ?? agents()[0].name,
6060
})
6161
const { theme } = useTheme()
6262
const colors = createMemo(() => [

packages/opencode/src/config/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,12 @@ export namespace Config {
666666
.string()
667667
.describe("Small model to use for tasks like title generation in the format of provider/model")
668668
.optional(),
669+
default_agent: z
670+
.string()
671+
.optional()
672+
.describe(
673+
"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.",
674+
),
669675
username: z
670676
.string()
671677
.optional()

packages/opencode/src/server/server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1060,11 +1060,11 @@ export namespace Server {
10601060
const sessionID = c.req.valid("param").sessionID
10611061
const body = c.req.valid("json")
10621062
const msgs = await Session.messages({ sessionID })
1063-
let currentAgent = "build"
1063+
let currentAgent = await Agent.defaultAgent()
10641064
for (let i = msgs.length - 1; i >= 0; i--) {
10651065
const info = msgs[i].info
10661066
if (info.role === "user") {
1067-
currentAgent = info.agent || "build"
1067+
currentAgent = info.agent || (await Agent.defaultAgent())
10681068
break
10691069
}
10701070
}

packages/opencode/src/session/prompt.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -715,7 +715,7 @@ export namespace SessionPrompt {
715715
}
716716

717717
async function createUserMessage(input: PromptInput) {
718-
const agent = await Agent.get(input.agent ?? "build")
718+
const agent = await Agent.get(input.agent ?? (await Agent.defaultAgent()))
719719
const info: MessageV2.Info = {
720720
id: input.messageID ?? Identifier.ascending("message"),
721721
role: "user",
@@ -1282,7 +1282,7 @@ export namespace SessionPrompt {
12821282
export async function command(input: CommandInput) {
12831283
log.info("command", input)
12841284
const command = await Command.get(input.command)
1285-
const agentName = command.agent ?? input.agent ?? "build"
1285+
const agentName = command.agent ?? input.agent ?? (await Agent.defaultAgent())
12861286

12871287
const raw = input.arguments.match(argsRegex) ?? []
12881288
const args = raw.map((arg) => arg.replace(quoteTrimRegex, ""))
@@ -1425,7 +1425,7 @@ export namespace SessionPrompt {
14251425
time: {
14261426
created: Date.now(),
14271427
},
1428-
agent: input.message.info.role === "user" ? input.message.info.agent : "build",
1428+
agent: input.message.info.role === "user" ? input.message.info.agent : await Agent.defaultAgent(),
14291429
model: {
14301430
providerID: input.providerID,
14311431
modelID: input.modelID,

0 commit comments

Comments
 (0)