Преглед на файлове

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/
Dax преди 1 година
родител
ревизия
c34aec060f
променени са 42 файла, в които са добавени 1416 реда и са изтрити 921 реда
  1. 12 0
      .opencode/agent/docs.md
  2. 0 44
      .opencode/agent/example-driven-docs-writer.md
  3. 18 0
      bun.lock
  4. 7 0
      packages/opencode/package.json
  5. 26 8
      packages/opencode/src/agent/agent.ts
  6. 1 1
      packages/opencode/src/cli/cmd/github.ts
  7. 23 9
      packages/opencode/src/cli/cmd/run.ts
  8. 2 2
      packages/opencode/src/cli/cmd/tui.ts
  9. 36 17
      packages/opencode/src/config/config.ts
  10. 2 3
      packages/opencode/src/index.ts
  11. 8 8
      packages/opencode/src/server/server.ts
  12. 43 11
      packages/opencode/src/session/index.ts
  13. 25 1
      packages/opencode/src/session/message-v2.ts
  14. 0 74
      packages/opencode/src/session/mode.ts
  15. 11 5
      packages/opencode/src/tool/task.ts
  16. 11 47
      packages/opencode/src/trace/index.ts
  17. 3 3
      packages/sdk/go/.stats.yml
  18. 4 3
      packages/sdk/go/api.md
  19. 80 60
      packages/sdk/go/app.go
  20. 15 15
      packages/sdk/go/app_test.go
  21. 262 44
      packages/sdk/go/config.go
  22. 5 3
      packages/sdk/go/internal/requestconfig/requestconfig.go
  23. 132 14
      packages/sdk/go/session.go
  24. 1 1
      packages/sdk/go/session_test.go
  25. 1 1
      packages/sdk/go/tui.go
  26. 4 2
      packages/sdk/stainless/stainless.yml
  27. 6 6
      packages/tui/cmd/opencode/main.go
  28. 91 50
      packages/tui/internal/app/app.go
  29. 38 61
      packages/tui/internal/app/prompt.go
  30. 13 13
      packages/tui/internal/app/state.go
  31. 24 0
      packages/tui/internal/attachment/attachment.go
  32. 6 6
      packages/tui/internal/commands/command.go
  33. 74 0
      packages/tui/internal/completions/agents.go
  34. 25 0
      packages/tui/internal/components/chat/editor.go
  35. 41 2
      packages/tui/internal/components/chat/message.go
  36. 8 0
      packages/tui/internal/components/chat/messages.go
  37. 37 19
      packages/tui/internal/components/dialog/complete.go
  38. 11 11
      packages/tui/internal/components/status/status.go
  39. 10 7
      packages/tui/internal/tui/tui.go
  40. 299 38
      packages/web/src/content/docs/docs/agents.mdx
  41. 1 1
      packages/web/src/content/docs/docs/keybinds.mdx
  42. 0 331
      packages/web/src/content/docs/docs/modes.mdx

+ 12 - 0
.opencode/agent/docs.md

@@ -0,0 +1,12 @@
+---
+description: You MUST use this agent when writing documentation
+---
+
+You are an expert technical documentation writer
+
+You are not verbose
+
+Every chunk of text should be followed by an example or something besides text
+to look at.
+
+Chunks of text should not be more than 2 sentences long.

+ 0 - 44
.opencode/agent/example-driven-docs-writer.md

@@ -1,44 +0,0 @@
----
-description: >-
-  Use this agent when you need to create or improve documentation that requires
-  concrete examples to illustrate every concept. Examples include:
-  <example>Context: User has written a new API endpoint and needs documentation.
-  user: 'I just created a POST /users endpoint that accepts name and email
-  fields. Can you document this?' assistant: 'I'll use the
-  example-driven-docs-writer agent to create documentation with practical
-  examples for your API endpoint.' <commentary>Since the user needs
-  documentation with examples, use the example-driven-docs-writer agent to
-  create comprehensive docs with code samples.</commentary></example>
-  <example>Context: User has a complex configuration file that needs
-  documentation. user: 'This config file has multiple sections and I need docs
-  that show how each option works' assistant: 'Let me use the
-  example-driven-docs-writer agent to create documentation that breaks down each
-  configuration option with practical examples.' <commentary>The user needs
-  documentation that demonstrates configuration options, perfect for the
-  example-driven-docs-writer agent.</commentary></example>
----
-You are an expert technical documentation writer who specializes in creating clear, example-rich documentation that never leaves readers guessing. Your core principle is that every concept must be immediately illustrated with concrete examples, code samples, or practical demonstrations.
-
-Your documentation approach:
-- Never write more than one sentence in any section without providing an example, code snippet, diagram, or practical illustration
-- Break up longer explanations with multiple examples showing different scenarios or use cases
-- Use concrete, realistic examples rather than abstract or placeholder content
-- Include both basic and advanced examples when covering complex topics
-- Show expected inputs, outputs, and results for all examples
-- Use code blocks, bullet points, tables, or other formatting to visually separate examples from explanatory text
-
-Structural requirements:
-- Start each section with a brief one-sentence explanation followed immediately by an example
-- For multi-step processes, provide an example after each step
-- Include error examples and edge cases alongside success scenarios
-- Use consistent formatting and naming conventions throughout examples
-- Ensure examples are copy-pasteable and functional when applicable
-
-Quality standards:
-- Verify that no paragraph exceeds one sentence without an accompanying example
-- Test that examples are accurate and would work in real scenarios
-- Ensure examples progress logically from simple to complex
-- Include context for when and why to use different approaches shown in examples
-- Provide troubleshooting examples for common issues
-
-When you receive a documentation request, immediately identify what needs examples and plan to illustrate every single concept, feature, or instruction with concrete demonstrations. Ask for clarification if you need more context to create realistic, useful examples.

Файловите разлики са ограничени, защото са твърде много
+ 18 - 0
bun.lock


+ 7 - 0
packages/opencode/package.json

@@ -38,6 +38,13 @@
     "@openauthjs/openauth": "0.4.3",
     "@opencode-ai/plugin": "workspace:*",
     "@opencode-ai/sdk": "workspace:*",
+    "@opentelemetry/auto-instrumentations-node": "0.62.0",
+    "@opentelemetry/exporter-jaeger": "2.0.1",
+    "@opentelemetry/exporter-otlp-http": "0.26.0",
+    "@opentelemetry/exporter-trace-otlp-http": "0.203.0",
+    "@opentelemetry/instrumentation-fetch": "0.203.0",
+    "@opentelemetry/sdk-node": "0.203.0",
+    "@opentelemetry/sdk-trace-node": "2.0.1",
     "@standard-schema/spec": "1.0.0",
     "@zip.js/zip.js": "2.7.62",
     "ai": "catalog:",

+ 26 - 8
packages/opencode/src/agent/agent.ts

@@ -10,13 +10,16 @@ export namespace Agent {
   export const Info = z
     .object({
       name: z.string(),
+      description: z.string().optional(),
+      mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]),
+      topP: z.number().optional(),
+      temperature: z.number().optional(),
       model: z
         .object({
           modelID: z.string(),
           providerID: z.string(),
         })
         .optional(),
-      description: z.string(),
       prompt: z.string().optional(),
       tools: z.record(z.boolean()),
     })
@@ -24,6 +27,7 @@ export namespace Agent {
       ref: "Agent",
     })
   export type Info = z.infer<typeof Info>
+
   const state = App.state("agent", async () => {
     const cfg = await Config.get()
     const result: Record<string, Info> = {
@@ -35,6 +39,21 @@ export namespace Agent {
           todoread: false,
           todowrite: false,
         },
+        mode: "subagent",
+      },
+      build: {
+        name: "build",
+        tools: {},
+        mode: "primary",
+      },
+      plan: {
+        name: "plan",
+        tools: {
+          write: false,
+          edit: false,
+          patch: false,
+        },
+        mode: "primary",
       },
     }
     for (const [key, value] of Object.entries(cfg.agent ?? {})) {
@@ -46,14 +65,10 @@ export namespace Agent {
       if (!item)
         item = result[key] = {
           name: key,
-          description: "",
-          tools: {
-            todowrite: false,
-            todoread: false,
-          },
+          mode: "all",
+          tools: {},
         }
-      const model = value.model ?? cfg.model
-      if (model) item.model = Provider.parseModel(model)
+      if (value.model) item.model = Provider.parseModel(value.model)
       if (value.prompt) item.prompt = value.prompt
       if (value.tools)
         item.tools = {
@@ -61,6 +76,9 @@ export namespace Agent {
           ...value.tools,
         }
       if (value.description) item.description = value.description
+      if (value.temperature != undefined) item.temperature = value.temperature
+      if (value.top_p != undefined) item.topP = value.top_p
+      if (value.mode) item.mode = value.mode
     }
     return result
   })

+ 1 - 1
packages/opencode/src/cli/cmd/github.ts

@@ -641,7 +641,7 @@ export const GithubRunCommand = cmd({
           messageID: Identifier.ascending("message"),
           providerID,
           modelID,
-          mode: "build",
+          agent: "build",
           parts: [
             {
               id: Identifier.ascending("part"),

+ 23 - 9
packages/opencode/src/cli/cmd/run.ts

@@ -8,8 +8,8 @@ import { Flag } from "../../flag/flag"
 import { Config } from "../../config/config"
 import { bootstrap } from "../bootstrap"
 import { MessageV2 } from "../../session/message-v2"
-import { Mode } from "../../session/mode"
 import { Identifier } from "../../id/id"
+import { Agent } from "../../agent/agent"
 
 const TOOL: Record<string, [string, string]> = {
   todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
@@ -54,9 +54,9 @@ export const RunCommand = cmd({
         alias: ["m"],
         describe: "model to use in the format of provider/model",
       })
-      .option("mode", {
+      .option("agent", {
         type: "string",
-        describe: "mode to use",
+        describe: "agent to use",
       })
   },
   handler: async (args) => {
@@ -103,8 +103,19 @@ export const RunCommand = cmd({
       }
       UI.empty()
 
-      const mode = args.mode ? await Mode.get(args.mode) : await Mode.list().then((x) => x[0])
-      const { providerID, modelID } = args.model ? Provider.parseModel(args.model) : mode.model ?? await Provider.defaultModel()
+      const agent = await (async () => {
+        if (args.agent) return Agent.get(args.agent)
+        const build = Agent.get("build")
+        if (build) return build
+        return Agent.list().then((x) => x[0])
+      })()
+
+      const { providerID, modelID } = await (() => {
+        if (args.model) return Provider.parseModel(args.model)
+        if (agent.model) return agent.model
+        return Provider.defaultModel()
+      })()
+
       UI.println(UI.Style.TEXT_NORMAL_BOLD + "@ ", UI.Style.TEXT_NORMAL + `${providerID}/${modelID}`)
       UI.empty()
 
@@ -157,14 +168,17 @@ export const RunCommand = cmd({
         UI.error(err)
       })
 
-
       const messageID = Identifier.ascending("message")
       const result = await Session.chat({
         sessionID: session.id,
         messageID,
-        providerID,
-        modelID,
-        mode: mode.name,
+        ...(agent.model
+          ? agent.model
+          : {
+              providerID,
+              modelID,
+            }),
+        agent: agent.name,
         parts: [
           {
             id: Identifier.ascending("part"),

+ 2 - 2
packages/opencode/src/cli/cmd/tui.ts

@@ -11,8 +11,8 @@ import { Config } from "../../config/config"
 import { Bus } from "../../bus"
 import { Log } from "../../util/log"
 import { FileWatcher } from "../../file/watch"
-import { Mode } from "../../session/mode"
 import { Ide } from "../../ide"
+import { Agent } from "../../agent/agent"
 
 declare global {
   const OPENCODE_TUI_PATH: string
@@ -115,7 +115,7 @@ export const TuiCommand = cmd({
             CGO_ENABLED: "0",
             OPENCODE_SERVER: server.url.toString(),
             OPENCODE_APP_INFO: JSON.stringify(app),
-            OPENCODE_MODES: JSON.stringify(await Mode.list()),
+            OPENCODE_AGENTS: JSON.stringify(await Agent.list()),
           },
           onExit: () => {
             server.stop()

+ 36 - 17
packages/opencode/src/config/config.ts

@@ -83,7 +83,7 @@ export namespace Config {
         ...md.data,
         prompt: md.content.trim(),
       }
-      const parsed = Mode.safeParse(config)
+      const parsed = Agent.safeParse(config)
       if (parsed.success) {
         result.mode = mergeDeep(result.mode, {
           [config.name]: parsed.data,
@@ -92,6 +92,15 @@ export namespace Config {
       }
       throw new InvalidError({ path: item }, { cause: parsed.error })
     }
+    // Migrate deprecated mode field to agent field
+    for (const [name, mode] of Object.entries(result.mode)) {
+      result.agent = mergeDeep(result.agent ?? {}, {
+        [name]: {
+          ...mode,
+          mode: "primary" as const,
+        },
+      })
+    }
 
     result.plugin = result.plugin || []
     result.plugin.push(
@@ -108,6 +117,12 @@ export namespace Config {
     if (result.keybinds?.messages_revert && !result.keybinds.messages_undo) {
       result.keybinds.messages_undo = result.keybinds.messages_revert
     }
+    if (result.keybinds?.switch_mode && !result.keybinds.switch_agent) {
+      result.keybinds.switch_agent = result.keybinds.switch_mode
+    }
+    if (result.keybinds?.switch_mode_reverse && !result.keybinds.switch_agent_reverse) {
+      result.keybinds.switch_agent_reverse = result.keybinds.switch_mode_reverse
+    }
 
     if (!result.username) {
       const os = await import("os")
@@ -149,7 +164,7 @@ export namespace Config {
   export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
   export type Mcp = z.infer<typeof Mcp>
 
-  export const Mode = z
+  export const Agent = z
     .object({
       model: z.string().optional(),
       temperature: z.number().optional(),
@@ -157,24 +172,26 @@ export namespace Config {
       prompt: z.string().optional(),
       tools: z.record(z.string(), z.boolean()).optional(),
       disable: z.boolean().optional(),
+      description: z.string().optional().describe("Description of when to use the agent"),
+      mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]).optional(),
     })
     .openapi({
-      ref: "ModeConfig",
+      ref: "AgentConfig",
     })
-  export type Mode = z.infer<typeof Mode>
-
-  export const Agent = Mode.extend({
-    description: z.string(),
-  }).openapi({
-    ref: "AgentConfig",
-  })
+  export type Agent = z.infer<typeof Agent>
 
   export const Keybinds = z
     .object({
       leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"),
       app_help: z.string().optional().default("<leader>h").describe("Show help dialog"),
-      switch_mode: z.string().optional().default("tab").describe("Next mode"),
-      switch_mode_reverse: z.string().optional().default("shift+tab").describe("Previous Mode"),
+      switch_mode: z.string().optional().default("none").describe("@deprecated use switch_agent. Next mode"),
+      switch_mode_reverse: z
+        .string()
+        .optional()
+        .default("none")
+        .describe("@deprecated use switch_agent_reverse. Previous mode"),
+      switch_agent: z.string().optional().default("tab").describe("Next agent"),
+      switch_agent_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
       editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
       session_export: z.string().optional().default("<leader>x").describe("Export session to editor"),
       session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
@@ -257,19 +274,21 @@ export namespace Config {
         .describe("Custom username to display in conversations instead of system username"),
       mode: z
         .object({
-          build: Mode.optional(),
-          plan: Mode.optional(),
+          build: Agent.optional(),
+          plan: Agent.optional(),
         })
-        .catchall(Mode)
+        .catchall(Agent)
         .optional()
-        .describe("Modes configuration, see https://opencode.ai/docs/modes"),
+        .describe("@deprecated Use `agent` field instead."),
       agent: z
         .object({
+          plan: Agent.optional(),
+          build: Agent.optional(),
           general: Agent.optional(),
         })
         .catchall(Agent)
         .optional()
-        .describe("Modes configuration, see https://opencode.ai/docs/modes"),
+        .describe("Agent configuration, see https://opencode.ai/docs/agent"),
       provider: z
         .record(
           ModelsDev.Provider.partial()

+ 2 - 3
packages/opencode/src/index.ts

@@ -1,4 +1,6 @@
 import "zod-openapi/extend"
+import { Trace } from "./trace"
+Trace.init()
 import yargs from "yargs"
 import { hideBin } from "yargs/helpers"
 import { RunCommand } from "./cli/cmd/run"
@@ -18,9 +20,6 @@ import { DebugCommand } from "./cli/cmd/debug"
 import { StatsCommand } from "./cli/cmd/stats"
 import { McpCommand } from "./cli/cmd/mcp"
 import { GithubCommand } from "./cli/cmd/github"
-import { Trace } from "./trace"
-
-Trace.init()
 
 const cancel = new AbortController()
 

+ 8 - 8
packages/opencode/src/server/server.ts

@@ -16,10 +16,10 @@ import { Config } from "../config/config"
 import { File } from "../file"
 import { LSP } from "../lsp"
 import { MessageV2 } from "../session/message-v2"
-import { Mode } from "../session/mode"
 import { callTui, TuiRoute } from "./tui"
 import { Permission } from "../permission"
 import { lazy } from "../util/lazy"
+import { Agent } from "../agent/agent"
 
 const ERRORS = {
   400: {
@@ -872,23 +872,23 @@ export namespace Server {
         },
       )
       .get(
-        "/mode",
+        "/agent",
         describeRoute({
-          description: "List all modes",
-          operationId: "app.modes",
+          description: "List all agents",
+          operationId: "app.agents",
           responses: {
             200: {
-              description: "List of modes",
+              description: "List of agents",
               content: {
                 "application/json": {
-                  schema: resolver(Mode.Info.array()),
+                  schema: resolver(Agent.Info.array()),
                 },
               },
             },
           },
         }),
         async (c) => {
-          const modes = await Mode.list()
+          const modes = await Agent.list()
           return c.json(modes)
         },
       )
@@ -1027,7 +1027,7 @@ export namespace Server {
       .post(
         "/tui/execute-command",
         describeRoute({
-          description: "Execute a TUI command (e.g. switch_mode)",
+          description: "Execute a TUI command (e.g. switch_agent)",
           operationId: "tui.executeCommand",
           responses: {
             200: {

+ 43 - 11
packages/opencode/src/session/index.ts

@@ -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(),
             },

+ 25 - 1
packages/opencode/src/session/message-v2.ts

@@ -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",
     })

+ 0 - 74
packages/opencode/src/session/mode.ts

@@ -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))
-  }
-}

+ 11 - 5
packages/opencode/src/tool/task.ts

@@ -8,8 +8,13 @@ import { Identifier } from "../id/id"
 import { Agent } from "../agent/agent"
 
 export const TaskTool = Tool.define("task", async () => {
-  const agents = await Agent.list()
-  const description = DESCRIPTION.replace("{agents}", agents.map((a) => `- ${a.name}: ${a.description}`).join("\n"))
+  const agents = await Agent.list().then((x) => x.filter((a) => a.mode !== "primary"))
+  const description = DESCRIPTION.replace(
+    "{agents}",
+    agents
+      .map((a) => `- ${a.name}: ${a.description ?? "This subagent should only be called manually by the user."}`)
+      .join("\n"),
+  )
   return {
     description,
     parameters: z.object({
@@ -51,11 +56,12 @@ export const TaskTool = Tool.define("task", async () => {
         sessionID: session.id,
         modelID: model.modelID,
         providerID: model.providerID,
-        mode: msg.info.mode,
-        system: agent.prompt,
+        agent: agent.name,
         tools: {
-          ...agent.tools,
+          todowrite: false,
+          todoread: false,
           task: false,
+          ...agent.tools,
         },
         parts: [
           {

+ 11 - 47
packages/opencode/src/trace/index.ts

@@ -1,53 +1,17 @@
-import { Global } from "../global"
-import { Installation } from "../installation"
-import path from "path"
+import { NodeSDK } from "@opentelemetry/sdk-node"
+import { FetchInstrumentation } from "@opentelemetry/instrumentation-fetch"
+import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"
 
 export namespace Trace {
   export function init() {
-    if (!Installation.isDev()) return
-    const writer = Bun.file(path.join(Global.Path.data, "log", "fetch.log")).writer()
+    const sdk = new NodeSDK({
+      serviceName: "opencode",
+      instrumentations: [new FetchInstrumentation()],
+      traceExporter: new OTLPTraceExporter({
+        url: "http://localhost:4318/v1/traces", // or your OTLP endpoint
+      }),
+    })
 
-    const originalFetch = globalThis.fetch
-    // @ts-expect-error
-    globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
-      const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url
-      const method = init?.method || "GET"
-
-      const urlObj = new URL(url)
-
-      writer.write(`\n${method} ${urlObj.pathname}${urlObj.search} HTTP/1.1\n`)
-      writer.write(`Host: ${urlObj.host}\n`)
-
-      if (init?.headers) {
-        if (init.headers instanceof Headers) {
-          init.headers.forEach((value, key) => {
-            writer.write(`${key}: ${value}\n`)
-          })
-        } else {
-          for (const [key, value] of Object.entries(init.headers)) {
-            writer.write(`${key}: ${value}\n`)
-          }
-        }
-      }
-
-      if (init?.body) {
-        writer.write(`\n${init.body}`)
-      }
-      writer.flush()
-      const response = await originalFetch(input, init)
-      const clonedResponse = response.clone()
-      writer.write(`\nHTTP/1.1 ${response.status} ${response.statusText}\n`)
-      response.headers.forEach((value, key) => {
-        writer.write(`${key}: ${value}\n`)
-      })
-      if (clonedResponse.body) {
-        clonedResponse.text().then(async (x) => {
-          writer.write(`\n${x}\n`)
-        })
-      }
-      writer.flush()
-
-      return response
-    }
+    sdk.start()
   }
 }

+ 3 - 3
packages/sdk/go/.stats.yml

@@ -1,4 +1,4 @@
 configured_endpoints: 34
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-52fd0b61e84fdc1cdd31ec12e1600510e9dd2f9d4fb20c2315b4975cb763ee98.yml
-openapi_spec_hash: e851b8d5a2412f5fc9be82ab88ebdfde
-config_hash: 11a6f0803eb407367c3f677d3e524c37
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-da1c340135c3dd6b1edb4e00e7039d2ac54d59271683a8b6ed528e51137ce41a.yml
+openapi_spec_hash: 0cdd9b6273d72f5a6f484a2999ff0632
+config_hash: 7581d5948150d4ef7dd7b13d0845dbeb

+ 4 - 3
packages/sdk/go/api.md

@@ -18,18 +18,18 @@ Methods:
 
 Response Types:
 
+- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Agent">Agent</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#App">App</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Mode">Mode</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Model">Model</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Provider">Provider</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppProvidersResponse">AppProvidersResponse</a>
 
 Methods:
 
+- <code title="get /agent">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Agents">Agents</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Agent">Agent</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
 - <code title="get /app">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#App">App</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
 - <code title="post /app/init">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Init">Init</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
 - <code title="post /log">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Log">Log</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppLogParams">AppLogParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
-- <code title="get /mode">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Modes">Modes</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Mode">Mode</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
 - <code title="get /config/providers">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Providers">Providers</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppProvidersResponse">AppProvidersResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
 
 # Find
@@ -65,7 +65,6 @@ Response Types:
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#KeybindsConfig">KeybindsConfig</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#McpLocalConfig">McpLocalConfig</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#McpRemoteConfig">McpRemoteConfig</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ModeConfig">ModeConfig</a>
 
 Methods:
 
@@ -75,6 +74,7 @@ Methods:
 
 Params Types:
 
+- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AgentPartInputParam">AgentPartInputParam</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartInputParam">FilePartInputParam</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartSourceUnionParam">FilePartSourceUnionParam</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartSourceTextParam">FilePartSourceTextParam</a>
@@ -84,6 +84,7 @@ Params Types:
 
 Response Types:
 
+- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AgentPart">AgentPart</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AssistantMessage">AssistantMessage</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePart">FilePart</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartSource">FilePartSource</a>

+ 80 - 60
packages/sdk/go/app.go

@@ -31,6 +31,14 @@ func NewAppService(opts ...option.RequestOption) (r *AppService) {
 	return
 }
 
+// List all agents
+func (r *AppService) Agents(ctx context.Context, opts ...option.RequestOption) (res *[]Agent, err error) {
+	opts = append(r.Options[:], opts...)
+	path := "agent"
+	err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
+	return
+}
+
 // Get app info
 func (r *AppService) Get(ctx context.Context, opts ...option.RequestOption) (res *App, err error) {
 	opts = append(r.Options[:], opts...)
@@ -55,14 +63,6 @@ func (r *AppService) Log(ctx context.Context, body AppLogParams, opts ...option.
 	return
 }
 
-// List all modes
-func (r *AppService) Modes(ctx context.Context, opts ...option.RequestOption) (res *[]Mode, err error) {
-	opts = append(r.Options[:], opts...)
-	path := "mode"
-	err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
-	return
-}
-
 // List all providers
 func (r *AppService) Providers(ctx context.Context, opts ...option.RequestOption) (res *AppProvidersResponse, err error) {
 	opts = append(r.Options[:], opts...)
@@ -71,6 +71,78 @@ func (r *AppService) Providers(ctx context.Context, opts ...option.RequestOption
 	return
 }
 
+type Agent struct {
+	Mode        AgentMode       `json:"mode,required"`
+	Name        string          `json:"name,required"`
+	Tools       map[string]bool `json:"tools,required"`
+	Description string          `json:"description"`
+	Model       AgentModel      `json:"model"`
+	Prompt      string          `json:"prompt"`
+	Temperature float64         `json:"temperature"`
+	TopP        float64         `json:"topP"`
+	JSON        agentJSON       `json:"-"`
+}
+
+// agentJSON contains the JSON metadata for the struct [Agent]
+type agentJSON struct {
+	Mode        apijson.Field
+	Name        apijson.Field
+	Tools       apijson.Field
+	Description apijson.Field
+	Model       apijson.Field
+	Prompt      apijson.Field
+	Temperature apijson.Field
+	TopP        apijson.Field
+	raw         string
+	ExtraFields map[string]apijson.Field
+}
+
+func (r *Agent) UnmarshalJSON(data []byte) (err error) {
+	return apijson.UnmarshalRoot(data, r)
+}
+
+func (r agentJSON) RawJSON() string {
+	return r.raw
+}
+
+type AgentMode string
+
+const (
+	AgentModeSubagent AgentMode = "subagent"
+	AgentModePrimary  AgentMode = "primary"
+	AgentModeAll      AgentMode = "all"
+)
+
+func (r AgentMode) IsKnown() bool {
+	switch r {
+	case AgentModeSubagent, AgentModePrimary, AgentModeAll:
+		return true
+	}
+	return false
+}
+
+type AgentModel struct {
+	ModelID    string         `json:"modelID,required"`
+	ProviderID string         `json:"providerID,required"`
+	JSON       agentModelJSON `json:"-"`
+}
+
+// agentModelJSON contains the JSON metadata for the struct [AgentModel]
+type agentModelJSON struct {
+	ModelID     apijson.Field
+	ProviderID  apijson.Field
+	raw         string
+	ExtraFields map[string]apijson.Field
+}
+
+func (r *AgentModel) UnmarshalJSON(data []byte) (err error) {
+	return apijson.UnmarshalRoot(data, r)
+}
+
+func (r agentModelJSON) RawJSON() string {
+	return r.raw
+}
+
 type App struct {
 	Git      bool    `json:"git,required"`
 	Hostname string  `json:"hostname,required"`
@@ -145,58 +217,6 @@ func (r appTimeJSON) RawJSON() string {
 	return r.raw
 }
 
-type Mode struct {
-	Name        string          `json:"name,required"`
-	Tools       map[string]bool `json:"tools,required"`
-	Model       ModeModel       `json:"model"`
-	Prompt      string          `json:"prompt"`
-	Temperature float64         `json:"temperature"`
-	TopP        float64         `json:"topP"`
-	JSON        modeJSON        `json:"-"`
-}
-
-// modeJSON contains the JSON metadata for the struct [Mode]
-type modeJSON struct {
-	Name        apijson.Field
-	Tools       apijson.Field
-	Model       apijson.Field
-	Prompt      apijson.Field
-	Temperature apijson.Field
-	TopP        apijson.Field
-	raw         string
-	ExtraFields map[string]apijson.Field
-}
-
-func (r *Mode) UnmarshalJSON(data []byte) (err error) {
-	return apijson.UnmarshalRoot(data, r)
-}
-
-func (r modeJSON) RawJSON() string {
-	return r.raw
-}
-
-type ModeModel struct {
-	ModelID    string        `json:"modelID,required"`
-	ProviderID string        `json:"providerID,required"`
-	JSON       modeModelJSON `json:"-"`
-}
-
-// modeModelJSON contains the JSON metadata for the struct [ModeModel]
-type modeModelJSON struct {
-	ModelID     apijson.Field
-	ProviderID  apijson.Field
-	raw         string
-	ExtraFields map[string]apijson.Field
-}
-
-func (r *ModeModel) UnmarshalJSON(data []byte) (err error) {
-	return apijson.UnmarshalRoot(data, r)
-}
-
-func (r modeModelJSON) RawJSON() string {
-	return r.raw
-}
-
 type Model struct {
 	ID          string                 `json:"id,required"`
 	Attachment  bool                   `json:"attachment,required"`

+ 15 - 15
packages/sdk/go/app_test.go

@@ -13,7 +13,7 @@ import (
 	"github.com/sst/opencode-sdk-go/option"
 )
 
-func TestAppGet(t *testing.T) {
+func TestAppAgents(t *testing.T) {
 	t.Skip("skipped: tests are disabled for the time being")
 	baseURL := "http://localhost:4010"
 	if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
@@ -25,7 +25,7 @@ func TestAppGet(t *testing.T) {
 	client := opencode.NewClient(
 		option.WithBaseURL(baseURL),
 	)
-	_, err := client.App.Get(context.TODO())
+	_, err := client.App.Agents(context.TODO())
 	if err != nil {
 		var apierr *opencode.Error
 		if errors.As(err, &apierr) {
@@ -35,7 +35,7 @@ func TestAppGet(t *testing.T) {
 	}
 }
 
-func TestAppInit(t *testing.T) {
+func TestAppGet(t *testing.T) {
 	t.Skip("skipped: tests are disabled for the time being")
 	baseURL := "http://localhost:4010"
 	if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
@@ -47,7 +47,7 @@ func TestAppInit(t *testing.T) {
 	client := opencode.NewClient(
 		option.WithBaseURL(baseURL),
 	)
-	_, err := client.App.Init(context.TODO())
+	_, err := client.App.Get(context.TODO())
 	if err != nil {
 		var apierr *opencode.Error
 		if errors.As(err, &apierr) {
@@ -57,7 +57,7 @@ func TestAppInit(t *testing.T) {
 	}
 }
 
-func TestAppLogWithOptionalParams(t *testing.T) {
+func TestAppInit(t *testing.T) {
 	t.Skip("skipped: tests are disabled for the time being")
 	baseURL := "http://localhost:4010"
 	if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
@@ -69,14 +69,7 @@ func TestAppLogWithOptionalParams(t *testing.T) {
 	client := opencode.NewClient(
 		option.WithBaseURL(baseURL),
 	)
-	_, err := client.App.Log(context.TODO(), opencode.AppLogParams{
-		Level:   opencode.F(opencode.AppLogParamsLevelDebug),
-		Message: opencode.F("message"),
-		Service: opencode.F("service"),
-		Extra: opencode.F(map[string]interface{}{
-			"foo": "bar",
-		}),
-	})
+	_, err := client.App.Init(context.TODO())
 	if err != nil {
 		var apierr *opencode.Error
 		if errors.As(err, &apierr) {
@@ -86,7 +79,7 @@ func TestAppLogWithOptionalParams(t *testing.T) {
 	}
 }
 
-func TestAppModes(t *testing.T) {
+func TestAppLogWithOptionalParams(t *testing.T) {
 	t.Skip("skipped: tests are disabled for the time being")
 	baseURL := "http://localhost:4010"
 	if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
@@ -98,7 +91,14 @@ func TestAppModes(t *testing.T) {
 	client := opencode.NewClient(
 		option.WithBaseURL(baseURL),
 	)
-	_, err := client.App.Modes(context.TODO())
+	_, err := client.App.Log(context.TODO(), opencode.AppLogParams{
+		Level:   opencode.F(opencode.AppLogParamsLevelDebug),
+		Message: opencode.F("message"),
+		Service: opencode.F("service"),
+		Extra: opencode.F(map[string]interface{}{
+			"foo": "bar",
+		}),
+	})
 	if err != nil {
 		var apierr *opencode.Error
 		if errors.As(err, &apierr) {

+ 262 - 44
packages/sdk/go/config.go

@@ -43,7 +43,7 @@ func (r *ConfigService) Get(ctx context.Context, opts ...option.RequestOption) (
 type Config struct {
 	// JSON schema reference for configuration validation
 	Schema string `json:"$schema"`
-	// Modes configuration, see https://opencode.ai/docs/modes
+	// Agent configuration, see https://opencode.ai/docs/agent
 	Agent ConfigAgent `json:"agent"`
 	// @deprecated Use 'share' field instead. Share newly created sessions
 	// automatically
@@ -63,7 +63,7 @@ type Config struct {
 	Lsp    map[string]ConfigLsp `json:"lsp"`
 	// MCP (Model Context Protocol) server configurations
 	Mcp map[string]ConfigMcp `json:"mcp"`
-	// Modes configuration, see https://opencode.ai/docs/modes
+	// @deprecated Use `agent` field instead.
 	Mode ConfigMode `json:"mode"`
 	// Model to use in the format of provider/model, eg anthropic/claude-2
 	Model      string           `json:"model"`
@@ -119,16 +119,20 @@ func (r configJSON) RawJSON() string {
 	return r.raw
 }
 
-// Modes configuration, see https://opencode.ai/docs/modes
+// Agent configuration, see https://opencode.ai/docs/agent
 type ConfigAgent struct {
+	Build       ConfigAgentBuild       `json:"build"`
 	General     ConfigAgentGeneral     `json:"general"`
+	Plan        ConfigAgentPlan        `json:"plan"`
 	ExtraFields map[string]ConfigAgent `json:"-,extras"`
 	JSON        configAgentJSON        `json:"-"`
 }
 
 // configAgentJSON contains the JSON metadata for the struct [ConfigAgent]
 type configAgentJSON struct {
+	Build       apijson.Field
 	General     apijson.Field
+	Plan        apijson.Field
 	raw         string
 	ExtraFields map[string]apijson.Field
 }
@@ -141,16 +145,82 @@ func (r configAgentJSON) RawJSON() string {
 	return r.raw
 }
 
+type ConfigAgentBuild struct {
+	// Description of when to use the agent
+	Description string               `json:"description"`
+	Disable     bool                 `json:"disable"`
+	Mode        ConfigAgentBuildMode `json:"mode"`
+	Model       string               `json:"model"`
+	Prompt      string               `json:"prompt"`
+	Temperature float64              `json:"temperature"`
+	Tools       map[string]bool      `json:"tools"`
+	TopP        float64              `json:"top_p"`
+	JSON        configAgentBuildJSON `json:"-"`
+}
+
+// configAgentBuildJSON contains the JSON metadata for the struct
+// [ConfigAgentBuild]
+type configAgentBuildJSON struct {
+	Description apijson.Field
+	Disable     apijson.Field
+	Mode        apijson.Field
+	Model       apijson.Field
+	Prompt      apijson.Field
+	Temperature apijson.Field
+	Tools       apijson.Field
+	TopP        apijson.Field
+	raw         string
+	ExtraFields map[string]apijson.Field
+}
+
+func (r *ConfigAgentBuild) UnmarshalJSON(data []byte) (err error) {
+	return apijson.UnmarshalRoot(data, r)
+}
+
+func (r configAgentBuildJSON) RawJSON() string {
+	return r.raw
+}
+
+type ConfigAgentBuildMode string
+
+const (
+	ConfigAgentBuildModeSubagent ConfigAgentBuildMode = "subagent"
+	ConfigAgentBuildModePrimary  ConfigAgentBuildMode = "primary"
+	ConfigAgentBuildModeAll      ConfigAgentBuildMode = "all"
+)
+
+func (r ConfigAgentBuildMode) IsKnown() bool {
+	switch r {
+	case ConfigAgentBuildModeSubagent, ConfigAgentBuildModePrimary, ConfigAgentBuildModeAll:
+		return true
+	}
+	return false
+}
+
 type ConfigAgentGeneral struct {
-	Description string                 `json:"description,required"`
+	// Description of when to use the agent
+	Description string                 `json:"description"`
+	Disable     bool                   `json:"disable"`
+	Mode        ConfigAgentGeneralMode `json:"mode"`
+	Model       string                 `json:"model"`
+	Prompt      string                 `json:"prompt"`
+	Temperature float64                `json:"temperature"`
+	Tools       map[string]bool        `json:"tools"`
+	TopP        float64                `json:"top_p"`
 	JSON        configAgentGeneralJSON `json:"-"`
-	ModeConfig
 }
 
 // configAgentGeneralJSON contains the JSON metadata for the struct
 // [ConfigAgentGeneral]
 type configAgentGeneralJSON struct {
 	Description apijson.Field
+	Disable     apijson.Field
+	Mode        apijson.Field
+	Model       apijson.Field
+	Prompt      apijson.Field
+	Temperature apijson.Field
+	Tools       apijson.Field
+	TopP        apijson.Field
 	raw         string
 	ExtraFields map[string]apijson.Field
 }
@@ -163,6 +233,73 @@ func (r configAgentGeneralJSON) RawJSON() string {
 	return r.raw
 }
 
+type ConfigAgentGeneralMode string
+
+const (
+	ConfigAgentGeneralModeSubagent ConfigAgentGeneralMode = "subagent"
+	ConfigAgentGeneralModePrimary  ConfigAgentGeneralMode = "primary"
+	ConfigAgentGeneralModeAll      ConfigAgentGeneralMode = "all"
+)
+
+func (r ConfigAgentGeneralMode) IsKnown() bool {
+	switch r {
+	case ConfigAgentGeneralModeSubagent, ConfigAgentGeneralModePrimary, ConfigAgentGeneralModeAll:
+		return true
+	}
+	return false
+}
+
+type ConfigAgentPlan struct {
+	// Description of when to use the agent
+	Description string              `json:"description"`
+	Disable     bool                `json:"disable"`
+	Mode        ConfigAgentPlanMode `json:"mode"`
+	Model       string              `json:"model"`
+	Prompt      string              `json:"prompt"`
+	Temperature float64             `json:"temperature"`
+	Tools       map[string]bool     `json:"tools"`
+	TopP        float64             `json:"top_p"`
+	JSON        configAgentPlanJSON `json:"-"`
+}
+
+// configAgentPlanJSON contains the JSON metadata for the struct [ConfigAgentPlan]
+type configAgentPlanJSON struct {
+	Description apijson.Field
+	Disable     apijson.Field
+	Mode        apijson.Field
+	Model       apijson.Field
+	Prompt      apijson.Field
+	Temperature apijson.Field
+	Tools       apijson.Field
+	TopP        apijson.Field
+	raw         string
+	ExtraFields map[string]apijson.Field
+}
+
+func (r *ConfigAgentPlan) UnmarshalJSON(data []byte) (err error) {
+	return apijson.UnmarshalRoot(data, r)
+}
+
+func (r configAgentPlanJSON) RawJSON() string {
+	return r.raw
+}
+
+type ConfigAgentPlanMode string
+
+const (
+	ConfigAgentPlanModeSubagent ConfigAgentPlanMode = "subagent"
+	ConfigAgentPlanModePrimary  ConfigAgentPlanMode = "primary"
+	ConfigAgentPlanModeAll      ConfigAgentPlanMode = "all"
+)
+
+func (r ConfigAgentPlanMode) IsKnown() bool {
+	switch r {
+	case ConfigAgentPlanModeSubagent, ConfigAgentPlanModePrimary, ConfigAgentPlanModeAll:
+		return true
+	}
+	return false
+}
+
 type ConfigExperimental struct {
 	Hook ConfigExperimentalHook `json:"hook"`
 	JSON configExperimentalJSON `json:"-"`
@@ -516,11 +653,11 @@ func (r ConfigMcpType) IsKnown() bool {
 	return false
 }
 
-// Modes configuration, see https://opencode.ai/docs/modes
+// @deprecated Use `agent` field instead.
 type ConfigMode struct {
-	Build       ModeConfig            `json:"build"`
-	Plan        ModeConfig            `json:"plan"`
-	ExtraFields map[string]ModeConfig `json:"-,extras"`
+	Build       ConfigModeBuild       `json:"build"`
+	Plan        ConfigModePlan        `json:"plan"`
+	ExtraFields map[string]ConfigMode `json:"-,extras"`
 	JSON        configModeJSON        `json:"-"`
 }
 
@@ -540,6 +677,108 @@ func (r configModeJSON) RawJSON() string {
 	return r.raw
 }
 
+type ConfigModeBuild struct {
+	// Description of when to use the agent
+	Description string              `json:"description"`
+	Disable     bool                `json:"disable"`
+	Mode        ConfigModeBuildMode `json:"mode"`
+	Model       string              `json:"model"`
+	Prompt      string              `json:"prompt"`
+	Temperature float64             `json:"temperature"`
+	Tools       map[string]bool     `json:"tools"`
+	TopP        float64             `json:"top_p"`
+	JSON        configModeBuildJSON `json:"-"`
+}
+
+// configModeBuildJSON contains the JSON metadata for the struct [ConfigModeBuild]
+type configModeBuildJSON struct {
+	Description apijson.Field
+	Disable     apijson.Field
+	Mode        apijson.Field
+	Model       apijson.Field
+	Prompt      apijson.Field
+	Temperature apijson.Field
+	Tools       apijson.Field
+	TopP        apijson.Field
+	raw         string
+	ExtraFields map[string]apijson.Field
+}
+
+func (r *ConfigModeBuild) UnmarshalJSON(data []byte) (err error) {
+	return apijson.UnmarshalRoot(data, r)
+}
+
+func (r configModeBuildJSON) RawJSON() string {
+	return r.raw
+}
+
+type ConfigModeBuildMode string
+
+const (
+	ConfigModeBuildModeSubagent ConfigModeBuildMode = "subagent"
+	ConfigModeBuildModePrimary  ConfigModeBuildMode = "primary"
+	ConfigModeBuildModeAll      ConfigModeBuildMode = "all"
+)
+
+func (r ConfigModeBuildMode) IsKnown() bool {
+	switch r {
+	case ConfigModeBuildModeSubagent, ConfigModeBuildModePrimary, ConfigModeBuildModeAll:
+		return true
+	}
+	return false
+}
+
+type ConfigModePlan struct {
+	// Description of when to use the agent
+	Description string             `json:"description"`
+	Disable     bool               `json:"disable"`
+	Mode        ConfigModePlanMode `json:"mode"`
+	Model       string             `json:"model"`
+	Prompt      string             `json:"prompt"`
+	Temperature float64            `json:"temperature"`
+	Tools       map[string]bool    `json:"tools"`
+	TopP        float64            `json:"top_p"`
+	JSON        configModePlanJSON `json:"-"`
+}
+
+// configModePlanJSON contains the JSON metadata for the struct [ConfigModePlan]
+type configModePlanJSON struct {
+	Description apijson.Field
+	Disable     apijson.Field
+	Mode        apijson.Field
+	Model       apijson.Field
+	Prompt      apijson.Field
+	Temperature apijson.Field
+	Tools       apijson.Field
+	TopP        apijson.Field
+	raw         string
+	ExtraFields map[string]apijson.Field
+}
+
+func (r *ConfigModePlan) UnmarshalJSON(data []byte) (err error) {
+	return apijson.UnmarshalRoot(data, r)
+}
+
+func (r configModePlanJSON) RawJSON() string {
+	return r.raw
+}
+
+type ConfigModePlanMode string
+
+const (
+	ConfigModePlanModeSubagent ConfigModePlanMode = "subagent"
+	ConfigModePlanModePrimary  ConfigModePlanMode = "primary"
+	ConfigModePlanModeAll      ConfigModePlanMode = "all"
+)
+
+func (r ConfigModePlanMode) IsKnown() bool {
+	switch r {
+	case ConfigModePlanModeSubagent, ConfigModePlanModePrimary, ConfigModePlanModeAll:
+		return true
+	}
+	return false
+}
+
 type ConfigPermission struct {
 	Bash ConfigPermissionBashUnion `json:"bash"`
 	Edit ConfigPermissionEdit      `json:"edit"`
@@ -588,11 +827,12 @@ type ConfigPermissionBashString string
 const (
 	ConfigPermissionBashStringAsk   ConfigPermissionBashString = "ask"
 	ConfigPermissionBashStringAllow ConfigPermissionBashString = "allow"
+	ConfigPermissionBashStringDeny  ConfigPermissionBashString = "deny"
 )
 
 func (r ConfigPermissionBashString) IsKnown() bool {
 	switch r {
-	case ConfigPermissionBashStringAsk, ConfigPermissionBashStringAllow:
+	case ConfigPermissionBashStringAsk, ConfigPermissionBashStringAllow, ConfigPermissionBashStringDeny:
 		return true
 	}
 	return false
@@ -609,11 +849,12 @@ type ConfigPermissionBashMapItem string
 const (
 	ConfigPermissionBashMapAsk   ConfigPermissionBashMapItem = "ask"
 	ConfigPermissionBashMapAllow ConfigPermissionBashMapItem = "allow"
+	ConfigPermissionBashMapDeny  ConfigPermissionBashMapItem = "deny"
 )
 
 func (r ConfigPermissionBashMapItem) IsKnown() bool {
 	switch r {
-	case ConfigPermissionBashMapAsk, ConfigPermissionBashMapAllow:
+	case ConfigPermissionBashMapAsk, ConfigPermissionBashMapAllow, ConfigPermissionBashMapDeny:
 		return true
 	}
 	return false
@@ -624,11 +865,12 @@ type ConfigPermissionEdit string
 const (
 	ConfigPermissionEditAsk   ConfigPermissionEdit = "ask"
 	ConfigPermissionEditAllow ConfigPermissionEdit = "allow"
+	ConfigPermissionEditDeny  ConfigPermissionEdit = "deny"
 )
 
 func (r ConfigPermissionEdit) IsKnown() bool {
 	switch r {
-	case ConfigPermissionEditAsk, ConfigPermissionEditAllow:
+	case ConfigPermissionEditAsk, ConfigPermissionEditAllow, ConfigPermissionEditDeny:
 		return true
 	}
 	return false
@@ -866,9 +1108,13 @@ type KeybindsConfig struct {
 	SessionShare string `json:"session_share,required"`
 	// Unshare current session
 	SessionUnshare string `json:"session_unshare,required"`
-	// Next mode
+	// Next agent
+	SwitchAgent string `json:"switch_agent,required"`
+	// Previous agent
+	SwitchAgentReverse string `json:"switch_agent_reverse,required"`
+	// @deprecated use switch_agent. Next mode
 	SwitchMode string `json:"switch_mode,required"`
-	// Previous Mode
+	// @deprecated use switch_agent_reverse. Previous mode
 	SwitchModeReverse string `json:"switch_mode_reverse,required"`
 	// List available themes
 	ThemeList string `json:"theme_list,required"`
@@ -913,6 +1159,8 @@ type keybindsConfigJSON struct {
 	SessionNew           apijson.Field
 	SessionShare         apijson.Field
 	SessionUnshare       apijson.Field
+	SwitchAgent          apijson.Field
+	SwitchAgentReverse   apijson.Field
 	SwitchMode           apijson.Field
 	SwitchModeReverse    apijson.Field
 	ThemeList            apijson.Field
@@ -1022,33 +1270,3 @@ func (r McpRemoteConfigType) IsKnown() bool {
 	}
 	return false
 }
-
-type ModeConfig struct {
-	Disable     bool            `json:"disable"`
-	Model       string          `json:"model"`
-	Prompt      string          `json:"prompt"`
-	Temperature float64         `json:"temperature"`
-	Tools       map[string]bool `json:"tools"`
-	TopP        float64         `json:"top_p"`
-	JSON        modeConfigJSON  `json:"-"`
-}
-
-// modeConfigJSON contains the JSON metadata for the struct [ModeConfig]
-type modeConfigJSON struct {
-	Disable     apijson.Field
-	Model       apijson.Field
-	Prompt      apijson.Field
-	Temperature apijson.Field
-	Tools       apijson.Field
-	TopP        apijson.Field
-	raw         string
-	ExtraFields map[string]apijson.Field
-}
-
-func (r *ModeConfig) UnmarshalJSON(data []byte) (err error) {
-	return apijson.UnmarshalRoot(data, r)
-}
-
-func (r modeConfigJSON) RawJSON() string {
-	return r.raw
-}

+ 5 - 3
packages/sdk/go/internal/requestconfig/requestconfig.go

@@ -133,11 +133,13 @@ func NewRequestConfig(ctx context.Context, method string, u string, body interfa
 	// Fallback to json serialization if none of the serialization functions that we expect
 	// to see is present.
 	if body != nil && !hasSerializationFunc {
-		content, err := json.Marshal(body)
-		if err != nil {
+		buf := new(bytes.Buffer)
+		enc := json.NewEncoder(buf)
+		enc.SetEscapeHTML(true)
+		if err := enc.Encode(body); err != nil {
 			return nil, err
 		}
-		reader = bytes.NewBuffer(content)
+		reader = buf
 	}
 
 	req, err := http.NewRequestWithContext(ctx, method, u, nil)

+ 132 - 14
packages/sdk/go/session.go

@@ -190,6 +190,113 @@ func (r *SessionService) Unshare(ctx context.Context, id string, opts ...option.
 	return
 }
 
+type AgentPart struct {
+	ID        string          `json:"id,required"`
+	MessageID string          `json:"messageID,required"`
+	Name      string          `json:"name,required"`
+	SessionID string          `json:"sessionID,required"`
+	Type      AgentPartType   `json:"type,required"`
+	Source    AgentPartSource `json:"source"`
+	JSON      agentPartJSON   `json:"-"`
+}
+
+// agentPartJSON contains the JSON metadata for the struct [AgentPart]
+type agentPartJSON struct {
+	ID          apijson.Field
+	MessageID   apijson.Field
+	Name        apijson.Field
+	SessionID   apijson.Field
+	Type        apijson.Field
+	Source      apijson.Field
+	raw         string
+	ExtraFields map[string]apijson.Field
+}
+
+func (r *AgentPart) UnmarshalJSON(data []byte) (err error) {
+	return apijson.UnmarshalRoot(data, r)
+}
+
+func (r agentPartJSON) RawJSON() string {
+	return r.raw
+}
+
+func (r AgentPart) implementsPart() {}
+
+type AgentPartType string
+
+const (
+	AgentPartTypeAgent AgentPartType = "agent"
+)
+
+func (r AgentPartType) IsKnown() bool {
+	switch r {
+	case AgentPartTypeAgent:
+		return true
+	}
+	return false
+}
+
+type AgentPartSource struct {
+	End   int64               `json:"end,required"`
+	Start int64               `json:"start,required"`
+	Value string              `json:"value,required"`
+	JSON  agentPartSourceJSON `json:"-"`
+}
+
+// agentPartSourceJSON contains the JSON metadata for the struct [AgentPartSource]
+type agentPartSourceJSON struct {
+	End         apijson.Field
+	Start       apijson.Field
+	Value       apijson.Field
+	raw         string
+	ExtraFields map[string]apijson.Field
+}
+
+func (r *AgentPartSource) UnmarshalJSON(data []byte) (err error) {
+	return apijson.UnmarshalRoot(data, r)
+}
+
+func (r agentPartSourceJSON) RawJSON() string {
+	return r.raw
+}
+
+type AgentPartInputParam struct {
+	Name   param.Field[string]                    `json:"name,required"`
+	Type   param.Field[AgentPartInputType]        `json:"type,required"`
+	ID     param.Field[string]                    `json:"id"`
+	Source param.Field[AgentPartInputSourceParam] `json:"source"`
+}
+
+func (r AgentPartInputParam) MarshalJSON() (data []byte, err error) {
+	return apijson.MarshalRoot(r)
+}
+
+func (r AgentPartInputParam) implementsSessionChatParamsPartUnion() {}
+
+type AgentPartInputType string
+
+const (
+	AgentPartInputTypeAgent AgentPartInputType = "agent"
+)
+
+func (r AgentPartInputType) IsKnown() bool {
+	switch r {
+	case AgentPartInputTypeAgent:
+		return true
+	}
+	return false
+}
+
+type AgentPartInputSourceParam struct {
+	End   param.Field[int64]  `json:"end,required"`
+	Start param.Field[int64]  `json:"start,required"`
+	Value param.Field[string] `json:"value,required"`
+}
+
+func (r AgentPartInputSourceParam) MarshalJSON() (data []byte, err error) {
+	return apijson.MarshalRoot(r)
+}
+
 type AssistantMessage struct {
 	ID         string                 `json:"id,required"`
 	Cost       float64                `json:"cost,required"`
@@ -855,11 +962,13 @@ type Part struct {
 	Cost      float64  `json:"cost"`
 	Filename  string   `json:"filename"`
 	// This field can have the runtime type of [[]string].
-	Files    interface{}    `json:"files"`
-	Hash     string         `json:"hash"`
-	Mime     string         `json:"mime"`
-	Snapshot string         `json:"snapshot"`
-	Source   FilePartSource `json:"source"`
+	Files    interface{} `json:"files"`
+	Hash     string      `json:"hash"`
+	Mime     string      `json:"mime"`
+	Name     string      `json:"name"`
+	Snapshot string      `json:"snapshot"`
+	// This field can have the runtime type of [FilePartSource], [AgentPartSource].
+	Source interface{} `json:"source"`
 	// This field can have the runtime type of [ToolPartState].
 	State     interface{} `json:"state"`
 	Synthetic bool        `json:"synthetic"`
@@ -886,6 +995,7 @@ type partJSON struct {
 	Files       apijson.Field
 	Hash        apijson.Field
 	Mime        apijson.Field
+	Name        apijson.Field
 	Snapshot    apijson.Field
 	Source      apijson.Field
 	State       apijson.Field
@@ -916,13 +1026,13 @@ func (r *Part) UnmarshalJSON(data []byte) (err error) {
 // for more type safety.
 //
 // Possible runtime types of the union are [TextPart], [FilePart], [ToolPart],
-// [StepStartPart], [StepFinishPart], [SnapshotPart], [PartPatchPart].
+// [StepStartPart], [StepFinishPart], [SnapshotPart], [PartPatchPart], [AgentPart].
 func (r Part) AsUnion() PartUnion {
 	return r.union
 }
 
 // Union satisfied by [TextPart], [FilePart], [ToolPart], [StepStartPart],
-// [StepFinishPart], [SnapshotPart] or [PartPatchPart].
+// [StepFinishPart], [SnapshotPart], [PartPatchPart] or [AgentPart].
 type PartUnion interface {
 	implementsPart()
 }
@@ -966,6 +1076,11 @@ func init() {
 			Type:               reflect.TypeOf(PartPatchPart{}),
 			DiscriminatorValue: "patch",
 		},
+		apijson.UnionVariant{
+			TypeFilter:         gjson.JSON,
+			Type:               reflect.TypeOf(AgentPart{}),
+			DiscriminatorValue: "agent",
+		},
 	)
 }
 
@@ -1025,11 +1140,12 @@ const (
 	PartTypeStepFinish PartType = "step-finish"
 	PartTypeSnapshot   PartType = "snapshot"
 	PartTypePatch      PartType = "patch"
+	PartTypeAgent      PartType = "agent"
 )
 
 func (r PartType) IsKnown() bool {
 	switch r {
-	case PartTypeText, PartTypeFile, PartTypeTool, PartTypeStepStart, PartTypeStepFinish, PartTypeSnapshot, PartTypePatch:
+	case PartTypeText, PartTypeFile, PartTypeTool, PartTypeStepStart, PartTypeStepFinish, PartTypeSnapshot, PartTypePatch, PartTypeAgent:
 		return true
 	}
 	return false
@@ -2080,8 +2196,8 @@ type SessionChatParams struct {
 	ModelID    param.Field[string]                       `json:"modelID,required"`
 	Parts      param.Field[[]SessionChatParamsPartUnion] `json:"parts,required"`
 	ProviderID param.Field[string]                       `json:"providerID,required"`
+	Agent      param.Field[string]                       `json:"agent"`
 	MessageID  param.Field[string]                       `json:"messageID"`
-	Mode       param.Field[string]                       `json:"mode"`
 	System     param.Field[string]                       `json:"system"`
 	Tools      param.Field[map[string]bool]              `json:"tools"`
 }
@@ -2095,7 +2211,8 @@ type SessionChatParamsPart struct {
 	ID        param.Field[string]                     `json:"id"`
 	Filename  param.Field[string]                     `json:"filename"`
 	Mime      param.Field[string]                     `json:"mime"`
-	Source    param.Field[FilePartSourceUnionParam]   `json:"source"`
+	Name      param.Field[string]                     `json:"name"`
+	Source    param.Field[interface{}]                `json:"source"`
 	Synthetic param.Field[bool]                       `json:"synthetic"`
 	Text      param.Field[string]                     `json:"text"`
 	Time      param.Field[interface{}]                `json:"time"`
@@ -2108,7 +2225,7 @@ func (r SessionChatParamsPart) MarshalJSON() (data []byte, err error) {
 
 func (r SessionChatParamsPart) implementsSessionChatParamsPartUnion() {}
 
-// Satisfied by [TextPartInputParam], [FilePartInputParam],
+// Satisfied by [TextPartInputParam], [FilePartInputParam], [AgentPartInputParam],
 // [SessionChatParamsPart].
 type SessionChatParamsPartUnion interface {
 	implementsSessionChatParamsPartUnion()
@@ -2117,13 +2234,14 @@ type SessionChatParamsPartUnion interface {
 type SessionChatParamsPartsType string
 
 const (
-	SessionChatParamsPartsTypeText SessionChatParamsPartsType = "text"
-	SessionChatParamsPartsTypeFile SessionChatParamsPartsType = "file"
+	SessionChatParamsPartsTypeText  SessionChatParamsPartsType = "text"
+	SessionChatParamsPartsTypeFile  SessionChatParamsPartsType = "file"
+	SessionChatParamsPartsTypeAgent SessionChatParamsPartsType = "agent"
 )
 
 func (r SessionChatParamsPartsType) IsKnown() bool {
 	switch r {
-	case SessionChatParamsPartsTypeText, SessionChatParamsPartsTypeFile:
+	case SessionChatParamsPartsTypeText, SessionChatParamsPartsTypeFile, SessionChatParamsPartsTypeAgent:
 		return true
 	}
 	return false

+ 1 - 1
packages/sdk/go/session_test.go

@@ -129,8 +129,8 @@ func TestSessionChatWithOptionalParams(t *testing.T) {
 				}),
 			}}),
 			ProviderID: opencode.F("providerID"),
+			Agent:      opencode.F("agent"),
 			MessageID:  opencode.F("msg"),
-			Mode:       opencode.F("mode"),
 			System:     opencode.F("system"),
 			Tools: opencode.F(map[string]bool{
 				"foo": true,

+ 1 - 1
packages/sdk/go/tui.go

@@ -47,7 +47,7 @@ func (r *TuiService) ClearPrompt(ctx context.Context, opts ...option.RequestOpti
 	return
 }
 
-// Execute a TUI command (e.g. switch_mode)
+// Execute a TUI command (e.g. switch_agent)
 func (r *TuiService) ExecuteCommand(ctx context.Context, body TuiExecuteCommandParams, opts ...option.RequestOption) (res *bool, err error) {
 	opts = append(r.Options[:], opts...)
 	path := "tui/execute-command"

+ 4 - 2
packages/sdk/stainless/stainless.yml

@@ -51,12 +51,12 @@ resources:
       logLevel: LogLevel
       provider: Provider
       model: Model
-      mode: Mode
+      agent: Agent
     methods:
       get: get /app
       init: post /app/init
       log: post /log
-      modes: get /mode
+      agents: get /agent
       providers: get /config/providers
 
   find:
@@ -99,6 +99,8 @@ resources:
       fileSource: FileSource
       symbolSource: SymbolSource
       toolPart: ToolPart
+      agentPart: AgentPart
+      agentPartInput: AgentPartInput
       stepStartPart: StepStartPart
       stepFinishPart: StepFinishPart
       snapshotPart: SnapshotPart

+ 6 - 6
packages/tui/cmd/opencode/main.go

@@ -31,7 +31,7 @@ func main() {
 
 	var model *string = flag.String("model", "", "model to begin with")
 	var prompt *string = flag.String("prompt", "", "prompt to begin with")
-	var mode *string = flag.String("mode", "", "mode to begin with")
+	var agent *string = flag.String("agent", "", "agent to begin with")
 	flag.Parse()
 
 	url := os.Getenv("OPENCODE_SERVER")
@@ -44,9 +44,9 @@ func main() {
 		os.Exit(1)
 	}
 
-	modesStr := os.Getenv("OPENCODE_MODES")
-	var modes []opencode.Mode
-	err = json.Unmarshal([]byte(modesStr), &modes)
+	agentsStr := os.Getenv("OPENCODE_AGENTS")
+	var agents []opencode.Agent
+	err = json.Unmarshal([]byte(agentsStr), &agents)
 	if err != nil {
 		slog.Error("Failed to unmarshal modes", "error", err)
 		os.Exit(1)
@@ -86,7 +86,7 @@ func main() {
 	logger := slog.New(apiHandler)
 	slog.SetDefault(logger)
 
-	slog.Debug("TUI launched", "app", appInfoStr, "modes", modesStr, "url", url)
+	slog.Debug("TUI launched", "app", appInfoStr, "modes", agentsStr, "url", url)
 
 	go func() {
 		err = clipboard.Init()
@@ -96,7 +96,7 @@ func main() {
 	}()
 
 	// Create main context for the application
-	app_, err := app.New(ctx, version, appInfo, modes, httpClient, model, prompt, mode)
+	app_, err := app.New(ctx, version, appInfo, agents, httpClient, model, prompt, agent)
 	if err != nil {
 		panic(err)
 	}

+ 91 - 50
packages/tui/internal/app/app.go

@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
+	"slices"
 	"strings"
 
 	"log/slog"
@@ -27,15 +28,14 @@ type Message struct {
 
 type App struct {
 	Info              opencode.App
-	Modes             []opencode.Mode
+	Agents            []opencode.Agent
 	Providers         []opencode.Provider
 	Version           string
 	StatePath         string
 	Config            *opencode.Config
 	Client            *opencode.Client
 	State             *State
-	ModeIndex         int
-	Mode              *opencode.Mode
+	AgentIndex        int
 	Provider          *opencode.Provider
 	Model             *opencode.Model
 	Session           *opencode.Session
@@ -45,11 +45,15 @@ type App struct {
 	Commands          commands.CommandRegistry
 	InitialModel      *string
 	InitialPrompt     *string
-	IntitialMode      *string
+	InitialAgent      *string
 	compactCancel     context.CancelFunc
 	IsLeaderSequence  bool
 }
 
+func (a *App) Agent() *opencode.Agent {
+	return &a.Agents[a.AgentIndex]
+}
+
 type SessionCreatedMsg = struct {
 	Session *opencode.Session
 }
@@ -83,11 +87,11 @@ func New(
 	ctx context.Context,
 	version string,
 	appInfo opencode.App,
-	modes []opencode.Mode,
+	agents []opencode.Agent,
 	httpClient *opencode.Client,
 	initialModel *string,
 	initialPrompt *string,
-	initialMode *string,
+	initialAgent *string,
 ) (*App, error) {
 	util.RootPath = appInfo.Path.Root
 	util.CwdPath = appInfo.Path.Cwd
@@ -108,8 +112,8 @@ func New(
 		SaveState(appStatePath, appState)
 	}
 
-	if appState.ModeModel == nil {
-		appState.ModeModel = make(map[string]ModeModel)
+	if appState.AgentModel == nil {
+		appState.AgentModel = make(map[string]AgentModel)
 	}
 
 	if configInfo.Theme != "" {
@@ -121,27 +125,29 @@ func New(
 		appState.Theme = themeEnv
 	}
 
-	var modeIndex int
-	var mode *opencode.Mode
+	agentIndex := slices.IndexFunc(agents, func(a opencode.Agent) bool {
+		return a.Mode != "subagent"
+	})
+	var agent *opencode.Agent
 	modeName := "build"
-	if appState.Mode != "" {
-		modeName = appState.Mode
+	if appState.Agent != "" {
+		modeName = appState.Agent
 	}
-	if initialMode != nil && *initialMode != "" {
-		modeName = *initialMode
+	if initialAgent != nil && *initialAgent != "" {
+		modeName = *initialAgent
 	}
-	for i, m := range modes {
+	for i, m := range agents {
 		if m.Name == modeName {
-			modeIndex = i
+			agentIndex = i
 			break
 		}
 	}
-	mode = &modes[modeIndex]
+	agent = &agents[agentIndex]
 
-	if mode.Model.ModelID != "" {
-		appState.ModeModel[mode.Name] = ModeModel{
-			ProviderID: mode.Model.ProviderID,
-			ModelID:    mode.Model.ModelID,
+	if agent.Model.ModelID != "" {
+		appState.AgentModel[agent.Name] = AgentModel{
+			ProviderID: agent.Model.ProviderID,
+			ModelID:    agent.Model.ModelID,
 		}
 	}
 
@@ -167,20 +173,19 @@ func New(
 
 	app := &App{
 		Info:          appInfo,
-		Modes:         modes,
+		Agents:        agents,
 		Version:       version,
 		StatePath:     appStatePath,
 		Config:        configInfo,
 		State:         appState,
 		Client:        httpClient,
-		ModeIndex:     modeIndex,
-		Mode:          mode,
+		AgentIndex:    agentIndex,
 		Session:       &opencode.Session{},
 		Messages:      []Message{},
 		Commands:      commands.LoadFromConfig(configInfo),
 		InitialModel:  initialModel,
 		InitialPrompt: initialPrompt,
-		IntitialMode:  initialMode,
+		InitialAgent:  initialAgent,
 	}
 
 	return app, nil
@@ -222,22 +227,24 @@ func SetClipboard(text string) tea.Cmd {
 
 func (a *App) cycleMode(forward bool) (*App, tea.Cmd) {
 	if forward {
-		a.ModeIndex++
-		if a.ModeIndex >= len(a.Modes) {
-			a.ModeIndex = 0
+		a.AgentIndex++
+		if a.AgentIndex >= len(a.Agents) {
+			a.AgentIndex = 0
 		}
 	} else {
-		a.ModeIndex--
-		if a.ModeIndex < 0 {
-			a.ModeIndex = len(a.Modes) - 1
+		a.AgentIndex--
+		if a.AgentIndex < 0 {
+			a.AgentIndex = len(a.Agents) - 1
 		}
 	}
-	a.Mode = &a.Modes[a.ModeIndex]
+	if a.Agent().Mode == "subagent" {
+		return a.cycleMode(forward)
+	}
 
-	modelID := a.Mode.Model.ModelID
-	providerID := a.Mode.Model.ProviderID
+	modelID := a.Agent().Model.ModelID
+	providerID := a.Agent().Model.ProviderID
 	if modelID == "" {
-		if model, ok := a.State.ModeModel[a.Mode.Name]; ok {
+		if model, ok := a.State.AgentModel[a.Agent().Name]; ok {
 			modelID = model.ModelID
 			providerID = model.ProviderID
 		}
@@ -258,20 +265,23 @@ func (a *App) cycleMode(forward bool) (*App, tea.Cmd) {
 		}
 	}
 
-	a.State.Mode = a.Mode.Name
+	a.State.Agent = a.Agent().Name
 	return a, a.SaveState()
 }
 
-func (a *App) SwitchMode() (*App, tea.Cmd) {
+func (a *App) SwitchAgent() (*App, tea.Cmd) {
 	return a.cycleMode(true)
 }
 
-func (a *App) SwitchModeReverse() (*App, tea.Cmd) {
+func (a *App) SwitchAgentReverse() (*App, tea.Cmd) {
 	return a.cycleMode(false)
 }
 
 // findModelByFullID finds a model by its full ID in the format "provider/model"
-func findModelByFullID(providers []opencode.Provider, fullModelID string) (*opencode.Provider, *opencode.Model) {
+func findModelByFullID(
+	providers []opencode.Provider,
+	fullModelID string,
+) (*opencode.Provider, *opencode.Model) {
 	modelParts := strings.SplitN(fullModelID, "/", 2)
 	if len(modelParts) < 2 {
 		return nil, nil
@@ -284,7 +294,10 @@ func findModelByFullID(providers []opencode.Provider, fullModelID string) (*open
 }
 
 // findModelByProviderAndModelID finds a model by provider ID and model ID
-func findModelByProviderAndModelID(providers []opencode.Provider, providerID, modelID string) (*opencode.Provider, *opencode.Model) {
+func findModelByProviderAndModelID(
+	providers []opencode.Provider,
+	providerID, modelID string,
+) (*opencode.Provider, *opencode.Model) {
 	for _, provider := range providers {
 		if provider.ID != providerID {
 			continue
@@ -330,7 +343,7 @@ func (a *App) InitializeProvider() tea.Cmd {
 	a.Providers = providers
 
 	// retains backwards compatibility with old state format
-	if model, ok := a.State.ModeModel[a.State.Mode]; ok {
+	if model, ok := a.State.AgentModel[a.State.Agent]; ok {
 		a.State.Provider = model.ProviderID
 		a.State.Model = model.ModelID
 	}
@@ -340,10 +353,17 @@ func (a *App) InitializeProvider() tea.Cmd {
 
 	// Priority 1: Command line --model flag (InitialModel)
 	if a.InitialModel != nil && *a.InitialModel != "" {
-		if provider, model := findModelByFullID(providers, *a.InitialModel); provider != nil && model != nil {
+		if provider, model := findModelByFullID(providers, *a.InitialModel); provider != nil &&
+			model != nil {
 			selectedProvider = provider
 			selectedModel = model
-			slog.Debug("Selected model from command line", "provider", provider.ID, "model", model.ID)
+			slog.Debug(
+				"Selected model from command line",
+				"provider",
+				provider.ID,
+				"model",
+				model.ID,
+			)
 		} else {
 			slog.Debug("Command line model not found", "model", *a.InitialModel)
 		}
@@ -351,7 +371,8 @@ func (a *App) InitializeProvider() tea.Cmd {
 
 	// Priority 2: Config file model setting
 	if selectedProvider == nil && a.Config.Model != "" {
-		if provider, model := findModelByFullID(providers, a.Config.Model); provider != nil && model != nil {
+		if provider, model := findModelByFullID(providers, a.Config.Model); provider != nil &&
+			model != nil {
 			selectedProvider = provider
 			selectedModel = model
 			slog.Debug("Selected model from config", "provider", provider.ID, "model", model.ID)
@@ -363,10 +384,17 @@ func (a *App) InitializeProvider() tea.Cmd {
 	// Priority 3: Recent model usage (most recently used model)
 	if selectedProvider == nil && len(a.State.RecentlyUsedModels) > 0 {
 		recentUsage := a.State.RecentlyUsedModels[0] // Most recent is first
-		if provider, model := findModelByProviderAndModelID(providers, recentUsage.ProviderID, recentUsage.ModelID); provider != nil && model != nil {
+		if provider, model := findModelByProviderAndModelID(providers, recentUsage.ProviderID, recentUsage.ModelID); provider != nil &&
+			model != nil {
 			selectedProvider = provider
 			selectedModel = model
-			slog.Debug("Selected model from recent usage", "provider", provider.ID, "model", model.ID)
+			slog.Debug(
+				"Selected model from recent usage",
+				"provider",
+				provider.ID,
+				"model",
+				model.ID,
+			)
 		} else {
 			slog.Debug("Recent model not found", "provider", recentUsage.ProviderID, "model", recentUsage.ModelID)
 		}
@@ -374,7 +402,8 @@ func (a *App) InitializeProvider() tea.Cmd {
 
 	// Priority 4: State-based model (backwards compatibility)
 	if selectedProvider == nil && a.State.Provider != "" && a.State.Model != "" {
-		if provider, model := findModelByProviderAndModelID(providers, a.State.Provider, a.State.Model); provider != nil && model != nil {
+		if provider, model := findModelByProviderAndModelID(providers, a.State.Provider, a.State.Model); provider != nil &&
+			model != nil {
 			selectedProvider = provider
 			selectedModel = model
 			slog.Debug("Selected model from state", "provider", provider.ID, "model", model.ID)
@@ -390,7 +419,13 @@ func (a *App) InitializeProvider() tea.Cmd {
 			if model := getDefaultModel(providersResponse, *provider); model != nil {
 				selectedProvider = provider
 				selectedModel = model
-				slog.Debug("Selected model from internal priority (Anthropic)", "provider", provider.ID, "model", model.ID)
+				slog.Debug(
+					"Selected model from internal priority (Anthropic)",
+					"provider",
+					provider.ID,
+					"model",
+					model.ID,
+				)
 			}
 		}
 
@@ -400,7 +435,13 @@ func (a *App) InitializeProvider() tea.Cmd {
 			if model := getDefaultModel(providersResponse, *provider); model != nil {
 				selectedProvider = provider
 				selectedModel = model
-				slog.Debug("Selected model from fallback (first available)", "provider", provider.ID, "model", model.ID)
+				slog.Debug(
+					"Selected model from fallback (first available)",
+					"provider",
+					provider.ID,
+					"model",
+					model.ID,
+				)
 			}
 		}
 	}
@@ -552,7 +593,7 @@ func (a *App) SendPrompt(ctx context.Context, prompt Prompt) (*App, tea.Cmd) {
 		_, err := a.Client.Session.Chat(ctx, a.Session.ID, opencode.SessionChatParams{
 			ProviderID: opencode.F(a.Provider.ID),
 			ModelID:    opencode.F(a.Model.ID),
-			Mode:       opencode.F(a.Mode.Name),
+			Agent:      opencode.F(a.Agent().Name),
 			MessageID:  opencode.F(messageID),
 			Parts:      opencode.F(message.ToSessionChatParams()),
 		})

+ 38 - 61
packages/tui/internal/app/prompt.go

@@ -55,6 +55,22 @@ func (p Prompt) ToMessage(
 		Text:      text,
 	}}
 	for _, attachment := range p.Attachments {
+		if attachment.Type == "agent" {
+			source, _ := attachment.GetAgentSource()
+			parts = append(parts, opencode.AgentPart{
+				ID:        id.Ascending(id.Part),
+				MessageID: messageID,
+				SessionID: sessionID,
+				Name:      source.Name,
+				Source: opencode.AgentPartSource{
+					Value: attachment.Display,
+					Start: int64(attachment.StartIndex),
+					End:   int64(attachment.EndIndex),
+				},
+			})
+			continue
+		}
+
 		text := opencode.FilePartSourceText{
 			Start: int64(attachment.StartIndex),
 			End:   int64(attachment.EndIndex),
@@ -122,6 +138,17 @@ func (m Message) ToPrompt() (*Prompt, error) {
 					continue
 				}
 				text += p.Text + " "
+			case opencode.AgentPart:
+				attachments = append(attachments, &attachment.Attachment{
+					ID:         p.ID,
+					Type:       "agent",
+					Display:    p.Source.Value,
+					StartIndex: int(p.Source.Start),
+					EndIndex:   int(p.Source.End),
+					Source: &attachment.AgentSource{
+						Name: p.Name,
+					},
+				})
 			case opencode.FilePart:
 				switch p.Source.Type {
 				case "file":
@@ -236,68 +263,18 @@ func (m Message) ToSessionChatParams() []opencode.SessionChatParamsPartUnion {
 				Filename: opencode.F(p.Filename),
 				Source:   opencode.F(source),
 			})
+		case opencode.AgentPart:
+			parts = append(parts, opencode.AgentPartInputParam{
+				ID:   opencode.F(p.ID),
+				Type: opencode.F(opencode.AgentPartInputTypeAgent),
+				Name: opencode.F(p.Name),
+				Source: opencode.F(opencode.AgentPartInputSourceParam{
+					Value: opencode.F(p.Source.Value),
+					Start: opencode.F(p.Source.Start),
+					End:   opencode.F(p.Source.End),
+				}),
+			})
 		}
 	}
 	return parts
 }
-
-func (p Prompt) ToSessionChatParams() []opencode.SessionChatParamsPartUnion {
-	parts := []opencode.SessionChatParamsPartUnion{
-		opencode.TextPartInputParam{
-			Type: opencode.F(opencode.TextPartInputTypeText),
-			Text: opencode.F(p.Text),
-		},
-	}
-	for _, att := range p.Attachments {
-		filePart := opencode.FilePartInputParam{
-			Type:     opencode.F(opencode.FilePartInputTypeFile),
-			Mime:     opencode.F(att.MediaType),
-			URL:      opencode.F(att.URL),
-			Filename: opencode.F(att.Filename),
-		}
-		switch att.Type {
-		case "file":
-			if fs, ok := att.GetFileSource(); ok {
-				filePart.Source = opencode.F(
-					opencode.FilePartSourceUnionParam(opencode.FileSourceParam{
-						Type: opencode.F(opencode.FileSourceTypeFile),
-						Path: opencode.F(fs.Path),
-						Text: opencode.F(opencode.FilePartSourceTextParam{
-							Start: opencode.F(int64(att.StartIndex)),
-							End:   opencode.F(int64(att.EndIndex)),
-							Value: opencode.F(att.Display),
-						}),
-					}),
-				)
-			}
-		case "symbol":
-			if ss, ok := att.GetSymbolSource(); ok {
-				filePart.Source = opencode.F(
-					opencode.FilePartSourceUnionParam(opencode.SymbolSourceParam{
-						Type: opencode.F(opencode.SymbolSourceTypeSymbol),
-						Path: opencode.F(ss.Path),
-						Name: opencode.F(ss.Name),
-						Kind: opencode.F(int64(ss.Kind)),
-						Range: opencode.F(opencode.SymbolSourceRangeParam{
-							Start: opencode.F(opencode.SymbolSourceRangeStartParam{
-								Line:      opencode.F(float64(ss.Range.Start.Line)),
-								Character: opencode.F(float64(ss.Range.Start.Char)),
-							}),
-							End: opencode.F(opencode.SymbolSourceRangeEndParam{
-								Line:      opencode.F(float64(ss.Range.End.Line)),
-								Character: opencode.F(float64(ss.Range.End.Char)),
-							}),
-						}),
-						Text: opencode.F(opencode.FilePartSourceTextParam{
-							Start: opencode.F(int64(att.StartIndex)),
-							End:   opencode.F(int64(att.EndIndex)),
-							Value: opencode.F(att.Display),
-						}),
-					}),
-				)
-			}
-		}
-		parts = append(parts, filePart)
-	}
-	return parts
-}

+ 13 - 13
packages/tui/internal/app/state.go

@@ -16,29 +16,29 @@ type ModelUsage struct {
 	LastUsed   time.Time `toml:"last_used"`
 }
 
-type ModeModel struct {
+type AgentModel struct {
 	ProviderID string `toml:"provider_id"`
 	ModelID    string `toml:"model_id"`
 }
 
 type State struct {
-	Theme              string               `toml:"theme"`
-	ScrollSpeed        *int                 `toml:"scroll_speed"`
-	ModeModel          map[string]ModeModel `toml:"mode_model"`
-	Provider           string               `toml:"provider"`
-	Model              string               `toml:"model"`
-	Mode               string               `toml:"mode"`
-	RecentlyUsedModels []ModelUsage         `toml:"recently_used_models"`
-	MessagesRight      bool                 `toml:"messages_right"`
-	SplitDiff          bool                 `toml:"split_diff"`
-	MessageHistory     []Prompt             `toml:"message_history"`
+	Theme              string                `toml:"theme"`
+	ScrollSpeed        *int                  `toml:"scroll_speed"`
+	AgentModel         map[string]AgentModel `toml:"agent_model"`
+	Provider           string                `toml:"provider"`
+	Model              string                `toml:"model"`
+	Agent              string                `toml:"agent"`
+	RecentlyUsedModels []ModelUsage          `toml:"recently_used_models"`
+	MessagesRight      bool                  `toml:"messages_right"`
+	SplitDiff          bool                  `toml:"split_diff"`
+	MessageHistory     []Prompt              `toml:"message_history"`
 }
 
 func NewState() *State {
 	return &State{
 		Theme:              "opencode",
-		Mode:               "build",
-		ModeModel:          make(map[string]ModeModel),
+		Agent:              "build",
+		AgentModel:         make(map[string]AgentModel),
 		RecentlyUsedModels: make([]ModelUsage, 0),
 		MessageHistory:     make([]Prompt, 0),
 	}

+ 24 - 0
packages/tui/internal/attachment/attachment.go

@@ -26,6 +26,10 @@ type SymbolRange struct {
 	End   Position `toml:"end"`
 }
 
+type AgentSource struct {
+	Name string `toml:"name"`
+}
+
 type Position struct {
 	Line int `toml:"line"`
 	Char int `toml:"char"`
@@ -76,6 +80,15 @@ func (a *Attachment) GetSymbolSource() (*SymbolSource, bool) {
 	return ss, ok
 }
 
+// GetAgentSource returns the source as AgentSource if the attachment is an agent type
+func (a *Attachment) GetAgentSource() (*AgentSource, bool) {
+	if a.Type != "agent" {
+		return nil, false
+	}
+	as, ok := a.Source.(*AgentSource)
+	return as, ok
+}
+
 // FromMap creates a TextSource from a map[string]any
 func (ts *TextSource) FromMap(sourceMap map[string]any) {
 	if value, ok := sourceMap["value"].(string); ok {
@@ -128,6 +141,13 @@ func (ss *SymbolSource) FromMap(sourceMap map[string]any) {
 	}
 }
 
+// FromMap creates an AgentSource from a map[string]any
+func (as *AgentSource) FromMap(sourceMap map[string]any) {
+	if name, ok := sourceMap["name"].(string); ok {
+		as.Name = name
+	}
+}
+
 // RestoreSourceType converts a map[string]any source back to the proper type
 func (a *Attachment) RestoreSourceType() {
 	if a.Source == nil {
@@ -149,6 +169,10 @@ func (a *Attachment) RestoreSourceType() {
 			ss := &SymbolSource{}
 			ss.FromMap(sourceMap)
 			a.Source = ss
+		case "agent":
+			as := &AgentSource{}
+			as.FromMap(sourceMap)
+			a.Source = as
 		}
 	}
 }

+ 6 - 6
packages/tui/internal/commands/command.go

@@ -107,8 +107,8 @@ func (r CommandRegistry) Matches(msg tea.KeyPressMsg, leader bool) []Command {
 
 const (
 	AppHelpCommand              CommandName = "app_help"
-	SwitchModeCommand           CommandName = "switch_mode"
-	SwitchModeReverseCommand    CommandName = "switch_mode_reverse"
+	SwitchAgentCommand          CommandName = "switch_agent"
+	SwitchAgentReverseCommand   CommandName = "switch_agent_reverse"
 	EditorOpenCommand           CommandName = "editor_open"
 	SessionNewCommand           CommandName = "session_new"
 	SessionListCommand          CommandName = "session_list"
@@ -181,13 +181,13 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
 			Trigger:     []string{"help"},
 		},
 		{
-			Name:        SwitchModeCommand,
-			Description: "next mode",
+			Name:        SwitchAgentCommand,
+			Description: "next agent",
 			Keybindings: parseBindings("tab"),
 		},
 		{
-			Name:        SwitchModeReverseCommand,
-			Description: "previous mode",
+			Name:        SwitchAgentReverseCommand,
+			Description: "previous agent",
 			Keybindings: parseBindings("shift+tab"),
 		},
 		{

+ 74 - 0
packages/tui/internal/completions/agents.go

@@ -0,0 +1,74 @@
+package completions
+
+import (
+	"context"
+	"log/slog"
+	"strings"
+
+	"github.com/sst/opencode-sdk-go"
+	"github.com/sst/opencode/internal/app"
+	"github.com/sst/opencode/internal/styles"
+	"github.com/sst/opencode/internal/theme"
+)
+
+type agentsContextGroup struct {
+	app *app.App
+}
+
+func (cg *agentsContextGroup) GetId() string {
+	return "agents"
+}
+
+func (cg *agentsContextGroup) GetEmptyMessage() string {
+	return "no matching agents"
+}
+
+func (cg *agentsContextGroup) GetChildEntries(
+	query string,
+) ([]CompletionSuggestion, error) {
+	items := make([]CompletionSuggestion, 0)
+
+	query = strings.TrimSpace(query)
+
+	agents, err := cg.app.Client.App.Agents(
+		context.Background(),
+	)
+	if err != nil {
+		slog.Error("Failed to get agent list", "error", err)
+		return items, err
+	}
+	if agents == nil {
+		return items, nil
+	}
+
+	for _, agent := range *agents {
+		if query != "" && !strings.Contains(strings.ToLower(agent.Name), strings.ToLower(query)) {
+			continue
+		}
+		if agent.Mode == opencode.AgentModePrimary || agent.Name == "general" {
+			continue
+		}
+
+		displayFunc := func(s styles.Style) string {
+			t := theme.CurrentTheme()
+			muted := s.Foreground(t.TextMuted()).Render
+			return s.Render(agent.Name) + muted(" (agent)")
+		}
+
+		item := CompletionSuggestion{
+			Display:    displayFunc,
+			Value:      agent.Name,
+			ProviderID: cg.GetId(),
+			RawData:    agent,
+		}
+		items = append(items, item)
+	}
+
+	return items, nil
+}
+
+func NewAgentsContextGroup(app *app.App) CompletionProvider {
+	return &agentsContextGroup{
+		app: app,
+	}
+}

+ 25 - 0
packages/tui/internal/components/chat/editor.go

@@ -288,6 +288,31 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			m.textarea.InsertAttachment(attachment)
 			m.textarea.InsertString(" ")
 			return m, nil
+		case "agents":
+			atIndex := m.textarea.LastRuneIndex('@')
+			if atIndex == -1 {
+				// Should not happen, but as a fallback, just insert.
+				m.textarea.InsertString(msg.Item.Value + " ")
+				return m, nil
+			}
+
+			cursorCol := m.textarea.CursorColumn()
+			m.textarea.ReplaceRange(atIndex, cursorCol, "")
+
+			name := msg.Item.Value
+			attachment := &attachment.Attachment{
+				ID:      uuid.NewString(),
+				Type:    "agent",
+				Display: "@" + name,
+				Source: &attachment.AgentSource{
+					Name: name,
+				},
+			}
+
+			m.textarea.InsertAttachment(attachment)
+			m.textarea.InsertString(" ")
+			return m, nil
+
 		default:
 			slog.Debug("Unknown provider", "provider", msg.Item.ProviderID)
 			return m, nil

+ 41 - 2
packages/tui/internal/components/chat/message.go

@@ -209,6 +209,7 @@ func renderText(
 	width int,
 	extra string,
 	fileParts []opencode.FilePart,
+	agentParts []opencode.AgentPart,
 	toolCalls ...opencode.ToolPart,
 ) string {
 	t := theme.CurrentTheme()
@@ -229,9 +230,47 @@ func renderText(
 
 		// Apply highlighting to filenames and base style to rest of text BEFORE wrapping
 		textLen := int64(len(text))
+
+		// Collect all parts to highlight (both file and agent parts)
+		type highlightPart struct {
+			start int64
+			end   int64
+			color compat.AdaptiveColor
+		}
+		var highlights []highlightPart
+
+		// Add file parts with secondary color
 		for _, filePart := range fileParts {
-			highlight := base.Foreground(t.Secondary())
-			start, end := filePart.Source.Text.Start, filePart.Source.Text.End
+			highlights = append(highlights, highlightPart{
+				start: filePart.Source.Text.Start,
+				end:   filePart.Source.Text.End,
+				color: t.Secondary(),
+			})
+		}
+
+		// Add agent parts with secondary color (same as file parts)
+		for _, agentPart := range agentParts {
+			highlights = append(highlights, highlightPart{
+				start: agentPart.Source.Start,
+				end:   agentPart.Source.End,
+				color: t.Secondary(),
+			})
+		}
+
+		// Sort highlights by start position
+		slices.SortFunc(highlights, func(a, b highlightPart) int {
+			if a.start < b.start {
+				return -1
+			}
+			if a.start > b.start {
+				return 1
+			}
+			return 0
+		})
+
+		for _, part := range highlights {
+			highlight := base.Foreground(part.color)
+			start, end := part.start, part.end
 
 			if end > textLen {
 				end = textLen

+ 8 - 0
packages/tui/internal/components/chat/messages.go

@@ -300,12 +300,17 @@ func (m *messagesComponent) renderView() tea.Cmd {
 						}
 						remainingParts := message.Parts[partIndex+1:]
 						fileParts := make([]opencode.FilePart, 0)
+						agentParts := make([]opencode.AgentPart, 0)
 						for _, part := range remainingParts {
 							switch part := part.(type) {
 							case opencode.FilePart:
 								if part.Source.Text.Start >= 0 && part.Source.Text.End >= part.Source.Text.Start {
 									fileParts = append(fileParts, part)
 								}
+							case opencode.AgentPart:
+								if part.Source.Start >= 0 && part.Source.End >= part.Source.Start {
+									agentParts = append(agentParts, part)
+								}
 							}
 						}
 						flexItems := []layout.FlexItem{}
@@ -355,6 +360,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
 								width,
 								files,
 								fileParts,
+								agentParts,
 							)
 							content = lipgloss.PlaceHorizontal(
 								m.width,
@@ -433,6 +439,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
 									width,
 									"",
 									[]opencode.FilePart{},
+									[]opencode.AgentPart{},
 									toolCallParts...,
 								)
 								content = lipgloss.PlaceHorizontal(
@@ -453,6 +460,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
 								width,
 								"",
 								[]opencode.FilePart{},
+								[]opencode.AgentPart{},
 								toolCallParts...,
 							)
 							content = lipgloss.PlaceHorizontal(

+ 37 - 19
packages/tui/internal/components/dialog/complete.go

@@ -66,11 +66,16 @@ func (c *completionDialogComponent) Init() tea.Cmd {
 
 func (c *completionDialogComponent) getAllCompletions(query string) tea.Cmd {
 	return func() tea.Msg {
-		allItems := make([]completions.CompletionSuggestion, 0)
+		// Collect results from all providers and preserve provider order
+		type providerItems struct {
+			idx   int
+			items []completions.CompletionSuggestion
+		}
+
+		itemsByProvider := make([]providerItems, 0, len(c.providers))
 		providersWithResults := 0
 
-		// Collect results from all providers
-		for _, provider := range c.providers {
+		for idx, provider := range c.providers {
 			items, err := provider.GetChildEntries(query)
 			if err != nil {
 				slog.Error(
@@ -84,33 +89,46 @@ func (c *completionDialogComponent) getAllCompletions(query string) tea.Cmd {
 			}
 			if len(items) > 0 {
 				providersWithResults++
-				allItems = append(allItems, items...)
+				itemsByProvider = append(itemsByProvider, providerItems{idx: idx, items: items})
 			}
 		}
 
-		// If there's a query, use fuzzy ranking to sort results
-		if query != "" && providersWithResults > 1 {
+		// If there's a query, fuzzy-rank within each provider, then concatenate by provider order
+		if query != "" && providersWithResults > 0 {
 			t := theme.CurrentTheme()
 			baseStyle := styles.NewStyle().Background(t.BackgroundElement())
-			// Create a slice of display values for fuzzy matching
-			displayValues := make([]string, len(allItems))
-			for i, item := range allItems {
-				displayValues[i] = item.Display(baseStyle)
-			}
 
-			matches := fuzzy.RankFindFold(query, displayValues)
-			sort.Sort(matches)
+			// Ensure stable provider order just in case
+			sort.SliceStable(itemsByProvider, func(i, j int) bool { return itemsByProvider[i].idx < itemsByProvider[j].idx })
 
-			// Reorder items based on fuzzy ranking
-			rankedItems := make([]completions.CompletionSuggestion, 0, len(matches))
-			for _, match := range matches {
-				rankedItems = append(rankedItems, allItems[match.OriginalIndex])
+			final := make([]completions.CompletionSuggestion, 0)
+			for _, entry := range itemsByProvider {
+				// Build display values for fuzzy matching within this provider
+				displayValues := make([]string, len(entry.items))
+				for i, item := range entry.items {
+					displayValues[i] = item.Display(baseStyle)
+				}
+
+				matches := fuzzy.RankFindFold(query, displayValues)
+				sort.Sort(matches)
+
+				// Reorder items for this provider based on fuzzy ranking
+				ranked := make([]completions.CompletionSuggestion, 0, len(matches))
+				for _, m := range matches {
+					ranked = append(ranked, entry.items[m.OriginalIndex])
+				}
+				final = append(final, ranked...)
 			}
 
-			return rankedItems
+			return final
 		}
 
-		return allItems
+		// No query or no results: just concatenate in provider order
+		all := make([]completions.CompletionSuggestion, 0)
+		for _, entry := range itemsByProvider {
+			all = append(all, entry.items...)
+		}
+		return all
 	}
 }
 func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {

+ 11 - 11
packages/tui/internal/components/status/status.go

@@ -121,7 +121,7 @@ func (m *statusComponent) View() string {
 
 	var modeBackground compat.AdaptiveColor
 	var modeForeground compat.AdaptiveColor
-	switch m.app.ModeIndex {
+	switch m.app.AgentIndex {
 	case 0:
 		modeBackground = t.BackgroundElement()
 		modeForeground = t.TextMuted()
@@ -148,31 +148,31 @@ func (m *statusComponent) View() string {
 		modeForeground = t.BackgroundPanel()
 	}
 
-	command := m.app.Commands[commands.SwitchModeCommand]
+	command := m.app.Commands[commands.SwitchAgentCommand]
 	kb := command.Keybindings[0]
 	key := kb.Key
 	if kb.RequiresLeader {
 		key = m.app.Config.Keybinds.Leader + " " + kb.Key
 	}
 
-	modeStyle := styles.NewStyle().Background(modeBackground).Foreground(modeForeground)
-	modeNameStyle := modeStyle.Bold(true).Render
-	modeDescStyle := modeStyle.Render
-	mode := modeNameStyle(strings.ToUpper(m.app.Mode.Name)) + modeDescStyle(" MODE")
-	mode = modeStyle.
+	agentStyle := styles.NewStyle().Background(modeBackground).Foreground(modeForeground)
+	agentNameStyle := agentStyle.Bold(true).Render
+	agentDescStyle := agentStyle.Render
+	agent := agentNameStyle(strings.ToUpper(m.app.Agent().Name)) + agentDescStyle(" AGENT")
+	agent = agentStyle.
 		Padding(0, 1).
 		BorderLeft(true).
 		BorderStyle(lipgloss.ThickBorder()).
 		BorderForeground(modeBackground).
 		BorderBackground(t.BackgroundPanel()).
-		Render(mode)
+		Render(agent)
 
 	faintStyle := styles.NewStyle().
 		Faint(true).
 		Background(t.BackgroundPanel()).
 		Foreground(t.TextMuted())
-	mode = faintStyle.Render(key+" ") + mode
-	modeWidth := lipgloss.Width(mode)
+	agent = faintStyle.Render(key+" ") + agent
+	modeWidth := lipgloss.Width(agent)
 
 	availableWidth := m.width - logoWidth - modeWidth
 	branchSuffix := ""
@@ -206,7 +206,7 @@ func (m *statusComponent) View() string {
 			View: logo + cwd,
 		},
 		layout.FlexItem{
-			View: mode,
+			View: agent,
 		},
 	)
 

+ 10 - 7
packages/tui/internal/tui/tui.go

@@ -69,6 +69,7 @@ type Model struct {
 	commandProvider      completions.CompletionProvider
 	fileProvider         completions.CompletionProvider
 	symbolsProvider      completions.CompletionProvider
+	agentsProvider       completions.CompletionProvider
 	showCompletionDialog bool
 	leaderBinding        *key.Binding
 	toastManager         *toast.ToastManager
@@ -211,8 +212,8 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			a.editor = updated.(chat.EditorComponent)
 			cmds = append(cmds, cmd)
 
-			// Set both file and symbols providers for @ completion
-			a.completions = dialog.NewCompletionDialogComponent("@", a.fileProvider, a.symbolsProvider)
+			// Set file, symbols, and agents providers for @ completion
+			a.completions = dialog.NewCompletionDialogComponent("@", a.agentsProvider, a.fileProvider, a.symbolsProvider)
 			updated, cmd = a.completions.Update(msg)
 			a.completions = updated.(dialog.CompletionDialog)
 			cmds = append(cmds, cmd)
@@ -585,7 +586,7 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	case app.ModelSelectedMsg:
 		a.app.Provider = &msg.Provider
 		a.app.Model = &msg.Model
-		a.app.State.ModeModel[a.app.Mode.Name] = app.ModeModel{
+		a.app.State.AgentModel[a.app.Agent().Name] = app.AgentModel{
 			ProviderID: msg.Provider.ID,
 			ModelID:    msg.Model.ID,
 		}
@@ -951,12 +952,12 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
 	case commands.AppHelpCommand:
 		helpDialog := dialog.NewHelpDialog(a.app)
 		a.modal = helpDialog
-	case commands.SwitchModeCommand:
-		updated, cmd := a.app.SwitchMode()
+	case commands.SwitchAgentCommand:
+		updated, cmd := a.app.SwitchAgent()
 		a.app = updated
 		cmds = append(cmds, cmd)
-	case commands.SwitchModeReverseCommand:
-		updated, cmd := a.app.SwitchModeReverse()
+	case commands.SwitchAgentReverseCommand:
+		updated, cmd := a.app.SwitchAgentReverse()
 		a.app = updated
 		cmds = append(cmds, cmd)
 	case commands.EditorOpenCommand:
@@ -1220,6 +1221,7 @@ func NewModel(app *app.App) tea.Model {
 	commandProvider := completions.NewCommandCompletionProvider(app)
 	fileProvider := completions.NewFileContextGroup(app)
 	symbolsProvider := completions.NewSymbolsContextGroup(app)
+	agentsProvider := completions.NewAgentsContextGroup(app)
 
 	messages := chat.NewMessagesComponent(app)
 	editor := chat.NewEditorComponent(app)
@@ -1240,6 +1242,7 @@ func NewModel(app *app.App) tea.Model {
 		commandProvider:      commandProvider,
 		fileProvider:         fileProvider,
 		symbolsProvider:      symbolsProvider,
+		agentsProvider:       agentsProvider,
 		leaderBinding:        leaderBinding,
 		showCompletionDialog: false,
 		toastManager:         toast.NewToastManager(),

+ 299 - 38
packages/web/src/content/docs/docs/agents.mdx

@@ -5,6 +5,52 @@ description: Configure and use specialized agents in opencode.
 
 Agents are specialized AI assistants that can be configured for specific tasks and workflows. They allow you to create focused tools with custom prompts, models, and tool access.
 
+:::tip
+Use the plan agent to analyze code and review suggestions without making any code changes.
+:::
+
+You can switch between agents during a session or configure them in your config file.
+
+---
+
+## Agent Types
+
+opencode has two types of agents:
+
+### Primary Agents
+
+Primary agents are the main assistants you interact with directly. You can cycle through them using the **Tab** key (or your configured `switch_agent` keybind). These agents handle your main conversation and can access all configured tools.
+
+**Built-in Primary Agents:**
+
+- **Build** - The default agent with all tools enabled. Standard for development work where you need full access to file operations and system commands.
+- **Plan** - A restricted agent for planning and analysis. Has `write`, `edit`, `patch`, and `bash` tools disabled by default. Useful for analyzing code and suggesting changes without making modifications.
+
+### Subagents
+
+Subagents are specialized assistants that primary agents can invoke for specific tasks. You can also manually invoke them by **@ mentioning** them in your messages (e.g., `@general help me search for this function`).
+
+**Built-in Subagents:**
+
+- **General** - General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. Use when searching for keywords or files and you're not confident you'll find the right match in the first few tries.
+
+---
+
+## Using Agents
+
+### Switching Primary Agents
+
+Use the **Tab** key to cycle through available primary agents during a session. You can also use your configured `switch_agent` keybind.
+
+### Invoking Subagents
+
+- **Automatic**: Primary agents will automatically use subagents for specialized tasks based on their descriptions
+- **Manual**: @ mention a subagent in your message: `@general search for authentication code`
+
+See also: [Formatters](/docs/formatters) for information about code formatting configuration.
+
+---
+
 ## Creating Agents
 
 You can create new agents using the `opencode agent create` command. This interactive command will:
@@ -21,39 +67,46 @@ opencode agent create
 
 The command will guide you through the process and automatically generate a well-structured agent based on your requirements.
 
-## Built-in Agents
-
-opencode comes with a built-in `general` agent:
-
-- **general** - General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. Use this when searching for keywords or files and you're not confident you'll find the right match in the first few tries.
-
 ## Configuration
 
-Agents can be configured in your `opencode.json` config file or as markdown files.
+You can customize the built-in agents or create your own through configuration. Agents can be configured in two ways:
 
 ### JSON Configuration
 
+Configure agents in your `opencode.json` config file:
+
 ```json title="opencode.json"
 {
   "$schema": "https://opencode.ai/config.json",
   "agent": {
+    "build": {
+      "mode": "primary",
+      "model": "anthropic/claude-sonnet-4-20250514",
+      "prompt": "{file:./prompts/build.txt}",
+      "tools": {
+        "write": true,
+        "edit": true,
+        "bash": true
+      }
+    },
+    "plan": {
+      "mode": "primary",
+      "model": "anthropic/claude-haiku-4-20250514",
+      "tools": {
+        "write": false,
+        "edit": false,
+        "bash": false
+      }
+    },
     "code-reviewer": {
       "description": "Reviews code for best practices and potential issues",
+      "mode": "subagent",
       "model": "anthropic/claude-sonnet-4-20250514",
       "prompt": "You are a code reviewer. Focus on security, performance, and maintainability.",
       "tools": {
         "write": false,
         "edit": false
       }
-    },
-    "test-writer": {
-      "description": "Specialized agent for writing comprehensive tests",
-      "prompt": "You are a test writing specialist. Write thorough, maintainable tests.",
-      "tools": {
-        "bash": true,
-        "read": true,
-        "write": true
-      }
     }
   }
 }
@@ -66,25 +119,113 @@ You can also define agents using markdown files. Place them in:
 - Global: `~/.config/opencode/agent/`
 - Project: `.opencode/agent/`
 
-```markdown title="~/.config/opencode/agent/code-reviewer.md"
+```markdown title="~/.config/opencode/agent/review.md"
 ---
-description: Reviews code for best practices and potential issues
+description: Reviews code for quality and best practices
+mode: subagent
 model: anthropic/claude-sonnet-4-20250514
+temperature: 0.1
 tools:
   write: false
   edit: false
+  bash: false
 ---
 
-You are a code reviewer with expertise in security, performance, and maintainability.
+You are in code review mode. Focus on:
 
-Focus on:
+- Code quality and best practices
+- Potential bugs and edge cases
+- Performance implications
+- Security considerations
+
+Provide constructive feedback without making direct changes.
+```
+
+The markdown file name becomes the agent name (e.g., `review.md` creates a `review` agent).
+
+Let's look at these configuration options in detail.
+
+---
+
+### Model
+
+Use the `model` config to override the default model for this agent. Useful for using different models optimized for different tasks. For example, a faster model for planning, a more capable model for implementation.
+
+```json title="opencode.json"
+{
+  "agent": {
+    "plan": {
+      "model": "anthropic/claude-haiku-4-20250514"
+    }
+  }
+}
+```
+
+---
+
+### Temperature
+
+Control the randomness and creativity of the AI's responses with the `temperature` config. Lower values make responses more focused and deterministic, while higher values increase creativity and variability.
 
-- Security vulnerabilities
-- Performance bottlenecks
-- Code maintainability
-- Best practices adherence
+```json title="opencode.json"
+{
+  "agent": {
+    "plan": {
+      "temperature": 0.1
+    },
+    "creative": {
+      "temperature": 0.8
+    }
+  }
+}
 ```
 
+Temperature values typically range from 0.0 to 1.0:
+
+- **0.0-0.2**: Very focused and deterministic responses, ideal for code analysis and planning
+- **0.3-0.5**: Balanced responses with some creativity, good for general development tasks
+- **0.6-1.0**: More creative and varied responses, useful for brainstorming and exploration
+
+```json title="opencode.json"
+{
+  "agent": {
+    "analyze": {
+      "temperature": 0.1,
+      "prompt": "{file:./prompts/analysis.txt}"
+    },
+    "build": {
+      "temperature": 0.3
+    },
+    "brainstorm": {
+      "temperature": 0.7,
+      "prompt": "{file:./prompts/creative.txt}"
+    }
+  }
+}
+```
+
+If no temperature is specified, opencode uses model-specific defaults (typically 0 for most models, 0.55 for Qwen models).
+
+---
+
+### Prompt
+
+Specify a custom system prompt file for this agent with the `prompt` config. The prompt file should contain instructions specific to the agent's purpose.
+
+```json title="opencode.json"
+{
+  "agent": {
+    "review": {
+      "prompt": "{file:./prompts/code-review.txt}"
+    }
+  }
+}
+```
+
+This path is relative to where the config file is located. So this works for both the global opencode config and the project specific config.
+
+---
+
 ## Agent Properties
 
 ### Required
@@ -96,39 +237,66 @@ Focus on:
 - **model** - Specific model to use (defaults to your configured model)
 - **prompt** - Custom system prompt for the agent
 - **tools** - Object specifying which tools the agent can access (true/false for each tool)
+- **temperature** - Control response randomness (0.0-1.0)
+- **mode** - Agent type: `"primary"` (can be cycled with Tab), `"subagent"` (invoked by @ mention), or `"all"` (both)
 - **disable** - Set to true to disable the agent
 
-## Tool Access
+### Tools
 
-By default, agents inherit the same tool access as the main assistant. You can restrict or enable specific tools:
+Control which tools are available in this agent with the `tools` config. You can enable or disable specific tools by setting them to `true` or `false`.
 
 ```json
 {
   "agent": {
-    "readonly-agent": {
-      "description": "Read-only agent for analysis",
+    "readonly": {
       "tools": {
         "write": false,
         "edit": false,
-        "bash": false
+        "bash": false,
+        "read": true,
+        "grep": true,
+        "glob": true
       }
     }
   }
 }
 ```
 
-Common tools you might want to control:
+If no tools are specified, all tools are enabled by default.
 
-- `write` - Create new files
-- `edit` - Modify existing files
-- `bash` - Execute shell commands
-- `read` - Read files
-- `glob` - Search for files
-- `grep` - Search file contents
+---
 
-## Using Agents
+#### Available tools
+
+Here are all the tools can be controlled through the agent config.
+
+| Tool        | Description             |
+| ----------- | ----------------------- |
+| `bash`      | Execute shell commands  |
+| `edit`      | Modify existing files   |
+| `write`     | Create new files        |
+| `read`      | Read file contents      |
+| `grep`      | Search file contents    |
+| `glob`      | Find files by pattern   |
+| `list`      | List directory contents |
+| `patch`     | Apply patches to files  |
+| `todowrite` | Manage todo lists       |
+| `todoread`  | Read todo lists         |
+| `webfetch`  | Fetch web content       |
+
+---
+
+## Tool Access
+
+By default, agents inherit the same tool access as the main assistant. You can restrict or enable specific tools as shown in the Tools section above.
+
+## Agent Modes
 
-Agents are automatically available through the Task tool when configured. The main assistant will use them for specialized tasks based on their descriptions.
+The `mode` property determines how an agent can be used:
+
+- **`"primary"`** - Can be cycled through with Tab key as your main assistant
+- **`"subagent"`** - Can be invoked by @ mentioning or automatically by primary agents
+- **`"all"`** - Can be used both as primary and subagent (default for custom agents)
 
 ## Best Practices
 
@@ -138,6 +306,97 @@ Agents are automatically available through the Task tool when configured. The ma
 4. **Consistent naming** - Use descriptive, consistent names for your agents
 5. **Project-specific agents** - Use `.opencode/agent/` for project-specific workflows
 
+## Custom Agents
+
+You can create your own custom agents by adding them to the configuration. Here are examples using both approaches:
+
+### Using JSON configuration
+
+```json title="opencode.json" {4-14}
+{
+  "$schema": "https://opencode.ai/config.json",
+  "agent": {
+    "docs": {
+      "prompt": "{file:./prompts/documentation.txt}",
+      "tools": {
+        "write": true,
+        "edit": true,
+        "bash": false,
+        "read": true,
+        "grep": true,
+        "glob": true
+      }
+    }
+  }
+}
+```
+
+### Using markdown files
+
+Create agent files in `.opencode/agent/` for project-specific agents or `~/.config/opencode/agent/` for global agents:
+
+```markdown title=".opencode/agent/debug.md"
+---
+temperature: 0.1
+tools:
+  bash: true
+  read: true
+  grep: true
+  write: false
+  edit: false
+---
+
+You are in debug mode. Your primary goal is to help investigate and diagnose issues.
+
+Focus on:
+
+- Understanding the problem through careful analysis
+- Using bash commands to inspect system state
+- Reading relevant files and logs
+- Searching for patterns and anomalies
+- Providing clear explanations of findings
+
+Do not make any changes to files. Only investigate and report.
+```
+
+```markdown title="~/.config/opencode/agent/refactor.md"
+---
+model: anthropic/claude-sonnet-4-20250514
+temperature: 0.2
+tools:
+  edit: true
+  read: true
+  grep: true
+  glob: true
+---
+
+You are in refactoring mode. Focus on improving code quality without changing functionality.
+
+Priorities:
+
+- Improve code readability and maintainability
+- Apply consistent naming conventions
+- Reduce code duplication
+- Optimize performance where appropriate
+- Ensure all tests continue to pass
+```
+
+---
+
+### Use cases
+
+Here are some common use cases for different agents.
+
+- **Build agent**: Full development work with all tools enabled
+- **Plan agent**: Analysis and planning without making changes
+- **Review agent**: Code review with read-only access plus documentation tools
+- **Debug agent**: Focused on investigation with bash and read tools enabled
+- **Docs agent**: Documentation writing with file operations but no system commands
+
+You might also find different models are good for different use cases.
+
+---
+
 ## Examples
 
 ### Documentation Agent
@@ -145,6 +404,7 @@ Agents are automatically available through the Task tool when configured. The ma
 ```markdown title="~/.config/opencode/agent/docs-writer.md"
 ---
 description: Writes and maintains project documentation
+mode: subagent
 tools:
   bash: false
 ---
@@ -164,6 +424,7 @@ Focus on:
 ```markdown title="~/.config/opencode/agent/security-auditor.md"
 ---
 description: Performs security audits and identifies vulnerabilities
+mode: subagent
 tools:
   write: false
   edit: false

+ 1 - 1
packages/web/src/content/docs/docs/keybinds.mdx

@@ -11,7 +11,7 @@ opencode has a list of keybinds that you can customize through the opencode conf
   "keybinds": {
     "leader": "ctrl+x",
     "app_help": "<leader>h",
-    "switch_mode": "tab",
+    "switch_agent": "tab",
 
     "editor_open": "<leader>e",
 

+ 0 - 331
packages/web/src/content/docs/docs/modes.mdx

@@ -1,331 +0,0 @@
----
-title: Modes
-description: Different modes for different use cases.
----
-
-Modes in opencode allow you to customize the behavior, tools, and prompts for different use cases.
-
-It comes with two built-in modes: **build** and **plan**. You can customize
-these or configure your own through the opencode config.
-
-:::tip
-Use the plan mode to analyze code and review suggestions without making any code
-changes.
-:::
-
-You can switch between modes during a session or configure them in your config file.
-
----
-
-## Built-in
-
-opencode comes with two built-in modes.
-
----
-
-### Build
-
-Build is the **default** mode with all tools enabled. This is the standard mode for development work where you need full access to file operations and system commands.
-
----
-
-### Plan
-
-A restricted mode designed for planning and analysis. In plan mode, the following tools are disabled by default:
-
-- `write` - Cannot create new files
-- `edit` - Cannot modify existing files
-- `patch` - Cannot apply patches
-- `bash` - Cannot execute shell commands
-
-This mode is useful when you want the AI to analyze code, suggest changes, or create plans without making any actual modifications to your codebase.
-
----
-
-## Switching
-
-You can switch between modes during a session using the _Tab_ key. Or your configured `switch_mode` keybind.
-
-See also: [Formatters](/docs/formatters) for information about code formatting configuration.
-
----
-
-## Configure
-
-You can customize the built-in modes or create your own through configuration. Modes can be configured in two ways:
-
-### JSON Configuration
-
-Configure modes in your `opencode.json` config file:
-
-```json title="opencode.json"
-{
-  "$schema": "https://opencode.ai/config.json",
-  "mode": {
-    "build": {
-      "model": "anthropic/claude-sonnet-4-20250514",
-      "prompt": "{file:./prompts/build.txt}",
-      "tools": {
-        "write": true,
-        "edit": true,
-        "bash": true
-      }
-    },
-    "plan": {
-      "model": "anthropic/claude-haiku-4-20250514",
-      "tools": {
-        "write": false,
-        "edit": false,
-        "bash": false
-      }
-    }
-  }
-}
-```
-
-### Markdown Configuration
-
-You can also define modes using markdown files. Place them in:
-
-- Global: `~/.config/opencode/mode/`
-- Project: `.opencode/mode/`
-
-```markdown title="~/.config/opencode/mode/review.md"
----
-model: anthropic/claude-sonnet-4-20250514
-temperature: 0.1
-tools:
-  write: false
-  edit: false
-  bash: false
----
-
-You are in code review mode. Focus on:
-
-- Code quality and best practices
-- Potential bugs and edge cases
-- Performance implications
-- Security considerations
-
-Provide constructive feedback without making direct changes.
-```
-
-The markdown file name becomes the mode name (e.g., `review.md` creates a `review` mode).
-
-Let's look at these configuration options in detail.
-
----
-
-### Model
-
-Use the `model` config to override the default model for this mode. Useful for using different models optimized for different tasks. For example, a faster model for planning, a more capable model for implementation.
-
-```json title="opencode.json"
-{
-  "mode": {
-    "plan": {
-      "model": "anthropic/claude-haiku-4-20250514"
-    }
-  }
-}
-```
-
----
-
-### Temperature
-
-Control the randomness and creativity of the AI's responses with the `temperature` config. Lower values make responses more focused and deterministic, while higher values increase creativity and variability.
-
-```json title="opencode.json"
-{
-  "mode": {
-    "plan": {
-      "temperature": 0.1
-    },
-    "creative": {
-      "temperature": 0.8
-    }
-  }
-}
-```
-
-Temperature values typically range from 0.0 to 1.0:
-
-- **0.0-0.2**: Very focused and deterministic responses, ideal for code analysis and planning
-- **0.3-0.5**: Balanced responses with some creativity, good for general development tasks
-- **0.6-1.0**: More creative and varied responses, useful for brainstorming and exploration
-
-```json title="opencode.json"
-{
-  "mode": {
-    "analyze": {
-      "temperature": 0.1,
-      "prompt": "{file:./prompts/analysis.txt}"
-    },
-    "build": {
-      "temperature": 0.3
-    },
-    "brainstorm": {
-      "temperature": 0.7,
-      "prompt": "{file:./prompts/creative.txt}"
-    }
-  }
-}
-```
-
-If no temperature is specified, opencode uses model-specific defaults (typically 0 for most models, 0.55 for Qwen models).
-
----
-
-### Prompt
-
-Specify a custom system prompt file for this mode with the `prompt` config. The prompt file should contain instructions specific to the mode's purpose.
-
-```json title="opencode.json"
-{
-  "mode": {
-    "review": {
-      "prompt": "{file:./prompts/code-review.txt}"
-    }
-  }
-}
-```
-
-This path is relative to where the config file is located. So this works for
-both the global opencode config and the project specific config.
-
----
-
-### Tools
-
-Control which tools are available in this mode with the `tools` config. You can enable or disable specific tools by setting them to `true` or `false`.
-
-```json
-{
-  "mode": {
-    "readonly": {
-      "tools": {
-        "write": false,
-        "edit": false,
-        "bash": false,
-        "read": true,
-        "grep": true,
-        "glob": true
-      }
-    }
-  }
-}
-```
-
-If no tools are specified, all tools are enabled by default.
-
----
-
-#### Available tools
-
-Here are all the tools can be controlled through the mode config.
-
-| Tool        | Description             |
-| ----------- | ----------------------- |
-| `bash`      | Execute shell commands  |
-| `edit`      | Modify existing files   |
-| `write`     | Create new files        |
-| `read`      | Read file contents      |
-| `grep`      | Search file contents    |
-| `glob`      | Find files by pattern   |
-| `list`      | List directory contents |
-| `patch`     | Apply patches to files  |
-| `todowrite` | Manage todo lists       |
-| `todoread`  | Read todo lists         |
-| `webfetch`  | Fetch web content       |
-
----
-
-## Custom modes
-
-You can create your own custom modes by adding them to the configuration. Here are examples using both approaches:
-
-### Using JSON configuration
-
-```json title="opencode.json" {4-14}
-{
-  "$schema": "https://opencode.ai/config.json",
-  "mode": {
-    "docs": {
-      "prompt": "{file:./prompts/documentation.txt}",
-      "tools": {
-        "write": true,
-        "edit": true,
-        "bash": false,
-        "read": true,
-        "grep": true,
-        "glob": true
-      }
-    }
-  }
-}
-```
-
-### Using markdown files
-
-Create mode files in `.opencode/mode/` for project-specific modes or `~/.config/opencode/mode/` for global modes:
-
-```markdown title=".opencode/mode/debug.md"
----
-temperature: 0.1
-tools:
-  bash: true
-  read: true
-  grep: true
-  write: false
-  edit: false
----
-
-You are in debug mode. Your primary goal is to help investigate and diagnose issues.
-
-Focus on:
-
-- Understanding the problem through careful analysis
-- Using bash commands to inspect system state
-- Reading relevant files and logs
-- Searching for patterns and anomalies
-- Providing clear explanations of findings
-
-Do not make any changes to files. Only investigate and report.
-```
-
-```markdown title="~/.config/opencode/mode/refactor.md"
----
-model: anthropic/claude-sonnet-4-20250514
-temperature: 0.2
-tools:
-  edit: true
-  read: true
-  grep: true
-  glob: true
----
-
-You are in refactoring mode. Focus on improving code quality without changing functionality.
-
-Priorities:
-
-- Improve code readability and maintainability
-- Apply consistent naming conventions
-- Reduce code duplication
-- Optimize performance where appropriate
-- Ensure all tests continue to pass
-```
-
----
-
-### Use cases
-
-Here are some common use cases for different modes.
-
-- **Build mode**: Full development work with all tools enabled
-- **Plan mode**: Analysis and planning without making changes
-- **Review mode**: Code review with read-only access plus documentation tools
-- **Debug mode**: Focused on investigation with bash and read tools enabled
-- **Docs mode**: Documentation writing with file operations but no system commands
-
-You might also find different models are good for different use cases.

Някои файлове не бяха показани, защото твърде много файлове са промени