diff options
| author | Dax Raad <[email protected]> | 2025-07-09 15:44:59 -0400 |
|---|---|---|
| committer | Dax Raad <[email protected]> | 2025-07-09 21:59:38 -0400 |
| commit | a826936702251df6a88d90f32f8570e68a4e7995 (patch) | |
| tree | 7a3ef69e4f9088d03f540673799773a16d95f3b9 /packages | |
| parent | fd4a5d5a63fc6079612460c4c8750f02f9983842 (diff) | |
| download | opencode-a826936702251df6a88d90f32f8570e68a4e7995.tar.gz opencode-a826936702251df6a88d90f32f8570e68a4e7995.zip | |
modes concept
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/opencode/src/config/config.ts | 19 | ||||
| -rw-r--r-- | packages/opencode/src/server/server.ts | 21 | ||||
| -rw-r--r-- | packages/opencode/src/session/index.ts | 27 | ||||
| -rw-r--r-- | packages/opencode/src/session/message-v2.ts | 1 | ||||
| -rw-r--r-- | packages/opencode/src/session/mode.ts | 74 | ||||
| -rw-r--r-- | packages/opencode/src/session/prompt/plan.txt | 3 |
6 files changed, 140 insertions, 5 deletions
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 7c248da83..9d6ca2ca4 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -55,6 +55,17 @@ export namespace Config { export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote]) export type Mcp = z.infer<typeof Mcp> + export const Mode = z + .object({ + model: z.string().optional(), + prompt: z.string().optional(), + tools: z.record(z.string(), z.boolean()).optional(), + }) + .openapi({ + ref: "ModeConfig", + }) + export type Mode = z.infer<typeof Mode> + export const Keybinds = z .object({ leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"), @@ -99,6 +110,7 @@ export namespace Config { .openapi({ ref: "KeybindsConfig", }) + export const Info = z .object({ $schema: z.string().optional().describe("JSON schema reference for configuration validation"), @@ -108,6 +120,13 @@ export namespace Config { autoupdate: z.boolean().optional().describe("Automatically update to the latest version"), disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"), model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(), + mode: z + .object({ + build: Mode.optional(), + plan: Mode.optional(), + }) + .catchall(Mode) + .optional(), log_level: Log.Level.optional().describe("Minimum log level to write to log files"), provider: z .record( diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 38a808974..db6a8fdf6 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -16,6 +16,7 @@ import { Config } from "../config/config" import { File } from "../file" import { LSP } from "../lsp" import { MessageV2 } from "../session/message-v2" +import { Mode } from "../session/mode" const ERRORS = { 400: { @@ -681,6 +682,26 @@ export namespace Server { return c.json(true) }, ) + .get( + "/mode", + describeRoute({ + description: "List all modes", + responses: { + 200: { + description: "List of modes", + content: { + "application/json": { + schema: resolver(Mode.Info.array()), + }, + }, + }, + }, + }), + async (c) => { + const modes = await Mode.list() + return c.json(modes) + }, + ) return result } diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index a1a1d183a..0e1861f28 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -15,6 +15,7 @@ import { } from "ai" import PROMPT_INITIALIZE from "../session/prompt/initialize.txt" +import PROMPT_PLAN from "../session/prompt/plan.txt" import { App } from "../app/app" import { Bus } from "../bus" @@ -29,12 +30,12 @@ import type { ModelsDev } from "../provider/models" import { Share } from "../share/share" import { Snapshot } from "../snapshot" import { Storage } from "../storage/storage" -import type { Tool } from "../tool/tool" import { Log } from "../util/log" import { NamedError } from "../util/error" import { SystemPrompt } from "./system" import { FileTime } from "../file/time" import { MessageV2 } from "./message-v2" +import { Mode } from "./mode" export namespace Session { const log = Log.create({ service: "session" }) @@ -281,13 +282,13 @@ export namespace Session { sessionID: string providerID: string modelID: string + mode?: string parts: MessageV2.UserPart[] - system?: string[] - tools?: Tool.Info[] }) { using abort = lock(input.sessionID) const l = log.clone().tag("session", input.sessionID) l.info("chatting") + const model = await Provider.getModel(input.providerID, input.modelID) let msgs = await messages(input.sessionID) const session = await get(input.sessionID) @@ -364,6 +365,7 @@ export namespace Session { return [ { type: "text", + synthetic: true, text: ["Called the Read tool on " + url.pathname, "<results>", text, "</results>"].join("\n"), }, ] @@ -373,6 +375,7 @@ export namespace Session { { type: "text", text: `Called the Read tool with the following input: {\"filePath\":\"${url.pathname}\"}`, + synthetic: true, }, { type: "file", @@ -386,6 +389,14 @@ export namespace Session { return [part] }), ).then((x) => x.flat()) + + if (true) + input.parts.push({ + type: "text", + text: PROMPT_PLAN, + synthetic: true, + }) + if (msgs.length === 0 && !session.parentID) { generateText({ maxOutputTokens: input.providerID === "google" ? 1024 : 20, @@ -431,9 +442,13 @@ export namespace Session { await updateMessage(msg) msgs.push(msg) - const system = input.system ?? SystemPrompt.provider(input.providerID, input.modelID) + const mode = await Mode.get(input.mode ?? "build") + let system = mode.prompt ? [mode.prompt] : SystemPrompt.provider(input.providerID, input.modelID) system.push(...(await SystemPrompt.environment())) system.push(...(await SystemPrompt.custom())) + // max 2 system prompt messages for caching purposes + const [first, ...rest] = system + system = [first, rest.join("\n")] const next: MessageV2.Info = { id: Identifier.ascending("message"), @@ -462,7 +477,8 @@ export namespace Session { const tools: Record<string, AITool> = {} for (const item of await Provider.tools(input.providerID)) { - tools[item.id.replaceAll(".", "_")] = tool({ + if (mode.tools[item.id] === false) continue + tools[item.id] = tool({ id: item.id as any, description: item.description, inputSchema: item.parameters as ZodSchema, @@ -494,6 +510,7 @@ export namespace Session { } for (const [key, item] of Object.entries(await MCP.tools())) { + if (mode.tools[key] === false) continue const execute = item.execute if (!execute) continue item.execute = async (args, opts) => { diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 8b09e68e6..fba34f4c0 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -76,6 +76,7 @@ export namespace MessageV2 { .object({ type: z.literal("text"), text: z.string(), + synthetic: z.boolean().optional(), }) .openapi({ ref: "TextPart", diff --git a/packages/opencode/src/session/mode.ts b/packages/opencode/src/session/mode.ts new file mode 100644 index 000000000..d2f548580 --- /dev/null +++ b/packages/opencode/src/session/mode.ts @@ -0,0 +1,74 @@ +import { mergeDeep } from "remeda" +import { App } from "../app/app" +import { Config } from "../config/config" +import z from "zod" + +export namespace Mode { + export const Info = z + .object({ + name: z.string(), + model: z + .object({ + modelID: z.string(), + providerID: z.string(), + }) + .optional(), + prompt: z.string().optional(), + tools: z + .object({ + write: z.boolean().optional(), + edit: z.boolean().optional(), + patch: z.boolean().optional(), + }) + .optional(), + }) + .openapi({ + ref: "Mode", + }) + export type Info = z.infer<typeof Info> + const state = App.state("mode", async () => { + const cfg = await Config.get() + const mode = mergeDeep( + { + build: {}, + plan: { + tools: { + write: false, + edit: false, + patch: false, + }, + }, + }, + cfg.mode ?? {}, + ) + const result: Record<string, Info> = {} + for (const [key, value] of Object.entries(mode)) { + let item = result[key] + if (!item) + item = result[key] = { + name: key, + tools: {}, + } + const model = value.model ?? cfg.model + if (model) { + const [providerID, ...rest] = model.split("/") + const modelID = rest.join("/") + item.model = { + modelID, + providerID, + } + } + if (value.prompt) item.prompt = await Bun.file(value.prompt).text() + if (value.tools) item.tools = value.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)) + } +} diff --git a/packages/opencode/src/session/prompt/plan.txt b/packages/opencode/src/session/prompt/plan.txt new file mode 100644 index 000000000..fffbfffc0 --- /dev/null +++ b/packages/opencode/src/session/prompt/plan.txt @@ -0,0 +1,3 @@ +<system-reminder> +Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received (for example, to make edits). +</system-reminder> |
