Merge agent and mode into one (#1689)

The concept of mode has been deprecated, there is now only the agent field in the config.

An agent can be cycled through as your primary agent with <tab> or you can spawn a subagent by @ mentioning it. if you include a description of when to use it, the primary agent will try to automatically use it

Full docs here: https://opencode.ai/docs/agents/
This commit is contained in:
Dax
2025-08-07 16:32:12 -04:00
committed by GitHub
parent 12f1ad521f
commit c34aec060f
42 changed files with 1755 additions and 930 deletions

View File

@@ -36,12 +36,12 @@ import { NamedError } from "../util/error"
import { SystemPrompt } from "./system"
import { FileTime } from "../file/time"
import { MessageV2 } from "./message-v2"
import { Mode } from "./mode"
import { LSP } from "../lsp"
import { ReadTool } from "../tool/read"
import { mergeDeep, pipe, splitWhen } from "remeda"
import { ToolRegistry } from "../tool/registry"
import { Plugin } from "../plugin"
import { Agent } from "../agent/agent"
export namespace Session {
const log = Log.create({ service: "session" })
@@ -357,7 +357,7 @@ export namespace Session {
messageID: Identifier.schema("message").optional(),
providerID: z.string(),
modelID: z.string(),
mode: z.string().optional(),
agent: z.string().optional(),
system: z.string().optional(),
tools: z.record(z.boolean()).optional(),
parts: z.array(
@@ -382,6 +382,16 @@ export namespace Session {
.openapi({
ref: "FilePartInput",
}),
MessageV2.AgentPart.omit({
messageID: true,
sessionID: true,
})
.partial({
id: true,
})
.openapi({
ref: "AgentPartInput",
}),
]),
),
})
@@ -393,7 +403,7 @@ export namespace Session {
const l = log.clone().tag("session", input.sessionID)
l.info("chatting")
const inputMode = input.mode ?? "build"
const inputAgent = input.agent ?? "build"
// Process revert cleanup first, before creating new messages
const session = await get(input.sessionID)
@@ -566,6 +576,28 @@ export namespace Session {
]
}
}
if (part.type === "agent") {
return [
{
id: Identifier.ascending("part"),
...part,
messageID: userMsg.id,
sessionID: input.sessionID,
},
{
id: Identifier.ascending("part"),
messageID: userMsg.id,
sessionID: input.sessionID,
type: "text",
synthetic: true,
text:
"Use the above message and context to generate a prompt and call the task tool with subagent: " +
part.name,
},
]
}
return [
{
id: Identifier.ascending("part"),
@@ -576,7 +608,7 @@ export namespace Session {
]
}),
).then((x) => x.flat())
if (inputMode === "plan")
if (inputAgent === "plan")
userParts.push({
id: Identifier.ascending("part"),
messageID: userMsg.id,
@@ -683,12 +715,12 @@ export namespace Session {
.catch(() => {})
}
const mode = await Mode.get(inputMode)
const agent = await Agent.get(inputAgent)
let system = SystemPrompt.header(input.providerID)
system.push(
...(() => {
if (input.system) return [input.system]
if (mode.prompt) return [mode.prompt]
if (agent.prompt) return [agent.prompt]
return SystemPrompt.provider(input.modelID)
})(),
)
@@ -702,7 +734,7 @@ export namespace Session {
id: Identifier.ascending("message"),
role: "assistant",
system,
mode: inputMode,
mode: inputAgent,
path: {
cwd: app.path.cwd,
root: app.path.root,
@@ -727,7 +759,7 @@ export namespace Session {
const processor = createProcessor(assistantMsg, model.info)
const enabledTools = pipe(
mode.tools,
agent.tools,
mergeDeep(await ToolRegistry.enabled(input.providerID, input.modelID)),
mergeDeep(input.tools ?? {}),
)
@@ -818,9 +850,9 @@ export namespace Session {
const params = {
temperature: model.info.temperature
? (mode.temperature ?? ProviderTransform.temperature(input.providerID, input.modelID))
? (agent.temperature ?? ProviderTransform.temperature(input.providerID, input.modelID))
: undefined,
topP: mode.topP ?? ProviderTransform.topP(input.providerID, input.modelID),
topP: agent.topP ?? ProviderTransform.topP(input.providerID, input.modelID),
}
await Plugin.trigger(
"chat.params",
@@ -871,7 +903,7 @@ export namespace Session {
},
modelID: input.modelID,
providerID: input.providerID,
mode: inputMode,
mode: inputAgent,
time: {
created: Date.now(),
},

View File

@@ -172,6 +172,21 @@ export namespace MessageV2 {
})
export type FilePart = z.infer<typeof FilePart>
export const AgentPart = PartBase.extend({
type: z.literal("agent"),
name: z.string(),
source: z
.object({
value: z.string(),
start: z.number().int(),
end: z.number().int(),
})
.optional(),
}).openapi({
ref: "AgentPart",
})
export type AgentPart = z.infer<typeof AgentPart>
export const StepStartPart = PartBase.extend({
type: z.literal("step-start"),
}).openapi({
@@ -212,7 +227,16 @@ export namespace MessageV2 {
export type User = z.infer<typeof User>
export const Part = z
.discriminatedUnion("type", [TextPart, FilePart, ToolPart, StepStartPart, StepFinishPart, SnapshotPart, PatchPart])
.discriminatedUnion("type", [
TextPart,
FilePart,
ToolPart,
StepStartPart,
StepFinishPart,
SnapshotPart,
PatchPart,
AgentPart,
])
.openapi({
ref: "Part",
})

View File

@@ -1,74 +0,0 @@
import { App } from "../app/app"
import { Config } from "../config/config"
import z from "zod"
import { Provider } from "../provider/provider"
export namespace Mode {
export const Info = z
.object({
name: z.string(),
temperature: z.number().optional(),
topP: z.number().optional(),
model: z
.object({
modelID: z.string(),
providerID: z.string(),
})
.optional(),
prompt: z.string().optional(),
tools: z.record(z.boolean()),
})
.openapi({
ref: "Mode",
})
export type Info = z.infer<typeof Info>
const state = App.state("mode", async () => {
const cfg = await Config.get()
const model = cfg.model ? Provider.parseModel(cfg.model) : undefined
const result: Record<string, Info> = {
build: {
model,
name: "build",
tools: {},
},
plan: {
name: "plan",
model,
tools: {
write: false,
edit: false,
patch: false,
},
},
}
for (const [key, value] of Object.entries(cfg.mode ?? {})) {
if (value.disable) continue
let item = result[key]
if (!item)
item = result[key] = {
name: key,
tools: {},
}
item.name = key
if (value.model) item.model = Provider.parseModel(value.model)
if (value.prompt) item.prompt = value.prompt
if (value.temperature != undefined) item.temperature = value.temperature
if (value.top_p != undefined) item.topP = value.top_p
if (value.tools)
item.tools = {
...value.tools,
...item.tools,
}
}
return result
})
export async function get(mode: string) {
return state().then((x) => x[mode])
}
export async function list() {
return state().then((x) => Object.values(x))
}
}