Skip to content

Commit 81983d4

Browse files
authored
fix(agent): default agent selection in acp and headless mode (anomalyco#8678)
Signed-off-by: assagman <ahmetsercansagman@gmail.com>
1 parent 7443b99 commit 81983d4

File tree

3 files changed

+141
-1
lines changed

3 files changed

+141
-1
lines changed

packages/opencode/src/acp/agent.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,9 @@ export namespace ACP {
731731
const defaultAgentName = await AgentModule.defaultAgent()
732732
const currentModeId = availableModes.find((m) => m.name === defaultAgentName)?.id ?? availableModes[0].id
733733

734+
// Persist the default mode so prompt() uses it immediately
735+
this.sessionManager.setMode(sessionId, currentModeId)
736+
734737
const mcpServers: Record<string, Config.Mcp> = {}
735738
for (const server of params.mcpServers) {
736739
if ("type" in server) {

packages/opencode/src/agent/agent.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,20 @@ export namespace Agent {
255255
}
256256

257257
export async function defaultAgent() {
258-
return state().then((x) => Object.keys(x)[0])
258+
const cfg = await Config.get()
259+
const agents = await state()
260+
261+
if (cfg.default_agent) {
262+
const agent = agents[cfg.default_agent]
263+
if (!agent) throw new Error(`default agent "${cfg.default_agent}" not found`)
264+
if (agent.mode === "subagent") throw new Error(`default agent "${cfg.default_agent}" is a subagent`)
265+
if (agent.hidden === true) throw new Error(`default agent "${cfg.default_agent}" is hidden`)
266+
return agent.name
267+
}
268+
269+
const primaryVisible = Object.values(agents).find((a) => a.mode !== "subagent" && a.hidden !== true)
270+
if (!primaryVisible) throw new Error("no primary visible agent found")
271+
return primaryVisible.name
259272
}
260273

261274
export async function generate(input: { description: string; model?: { providerID: string; modelID: string } }) {

packages/opencode/test/agent/agent.test.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,3 +512,127 @@ test("explicit Truncate.DIR deny is respected", async () => {
512512
},
513513
})
514514
})
515+
516+
test("defaultAgent returns build when no default_agent config", async () => {
517+
await using tmp = await tmpdir()
518+
await Instance.provide({
519+
directory: tmp.path,
520+
fn: async () => {
521+
const agent = await Agent.defaultAgent()
522+
expect(agent).toBe("build")
523+
},
524+
})
525+
})
526+
527+
test("defaultAgent respects default_agent config set to plan", async () => {
528+
await using tmp = await tmpdir({
529+
config: {
530+
default_agent: "plan",
531+
},
532+
})
533+
await Instance.provide({
534+
directory: tmp.path,
535+
fn: async () => {
536+
const agent = await Agent.defaultAgent()
537+
expect(agent).toBe("plan")
538+
},
539+
})
540+
})
541+
542+
test("defaultAgent respects default_agent config set to custom agent with mode all", async () => {
543+
await using tmp = await tmpdir({
544+
config: {
545+
default_agent: "my_custom",
546+
agent: {
547+
my_custom: {
548+
description: "My custom agent",
549+
},
550+
},
551+
},
552+
})
553+
await Instance.provide({
554+
directory: tmp.path,
555+
fn: async () => {
556+
const agent = await Agent.defaultAgent()
557+
expect(agent).toBe("my_custom")
558+
},
559+
})
560+
})
561+
562+
test("defaultAgent throws when default_agent points to subagent", async () => {
563+
await using tmp = await tmpdir({
564+
config: {
565+
default_agent: "explore",
566+
},
567+
})
568+
await Instance.provide({
569+
directory: tmp.path,
570+
fn: async () => {
571+
await expect(Agent.defaultAgent()).rejects.toThrow('default agent "explore" is a subagent')
572+
},
573+
})
574+
})
575+
576+
test("defaultAgent throws when default_agent points to hidden agent", async () => {
577+
await using tmp = await tmpdir({
578+
config: {
579+
default_agent: "compaction",
580+
},
581+
})
582+
await Instance.provide({
583+
directory: tmp.path,
584+
fn: async () => {
585+
await expect(Agent.defaultAgent()).rejects.toThrow('default agent "compaction" is hidden')
586+
},
587+
})
588+
})
589+
590+
test("defaultAgent throws when default_agent points to non-existent agent", async () => {
591+
await using tmp = await tmpdir({
592+
config: {
593+
default_agent: "does_not_exist",
594+
},
595+
})
596+
await Instance.provide({
597+
directory: tmp.path,
598+
fn: async () => {
599+
await expect(Agent.defaultAgent()).rejects.toThrow('default agent "does_not_exist" not found')
600+
},
601+
})
602+
})
603+
604+
test("defaultAgent returns plan when build is disabled and default_agent not set", async () => {
605+
await using tmp = await tmpdir({
606+
config: {
607+
agent: {
608+
build: { disable: true },
609+
},
610+
},
611+
})
612+
await Instance.provide({
613+
directory: tmp.path,
614+
fn: async () => {
615+
const agent = await Agent.defaultAgent()
616+
// build is disabled, so it should return plan (next primary agent)
617+
expect(agent).toBe("plan")
618+
},
619+
})
620+
})
621+
622+
test("defaultAgent throws when all primary agents are disabled", async () => {
623+
await using tmp = await tmpdir({
624+
config: {
625+
agent: {
626+
build: { disable: true },
627+
plan: { disable: true },
628+
},
629+
},
630+
})
631+
await Instance.provide({
632+
directory: tmp.path,
633+
fn: async () => {
634+
// build and plan are disabled, no primary-capable agents remain
635+
await expect(Agent.defaultAgent()).rejects.toThrow("no primary visible agent found")
636+
},
637+
})
638+
})

0 commit comments

Comments
 (0)