mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-22 02:04:22 +01:00
modes concept
This commit is contained in:
@@ -55,6 +55,17 @@ export namespace Config {
|
|||||||
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
|
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
|
||||||
export type Mcp = z.infer<typeof Mcp>
|
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
|
export const Keybinds = z
|
||||||
.object({
|
.object({
|
||||||
leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"),
|
leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"),
|
||||||
@@ -99,6 +110,7 @@ export namespace Config {
|
|||||||
.openapi({
|
.openapi({
|
||||||
ref: "KeybindsConfig",
|
ref: "KeybindsConfig",
|
||||||
})
|
})
|
||||||
|
|
||||||
export const Info = z
|
export const Info = z
|
||||||
.object({
|
.object({
|
||||||
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
|
$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"),
|
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"),
|
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(),
|
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"),
|
log_level: Log.Level.optional().describe("Minimum log level to write to log files"),
|
||||||
provider: z
|
provider: z
|
||||||
.record(
|
.record(
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { Config } from "../config/config"
|
|||||||
import { File } from "../file"
|
import { File } from "../file"
|
||||||
import { LSP } from "../lsp"
|
import { LSP } from "../lsp"
|
||||||
import { MessageV2 } from "../session/message-v2"
|
import { MessageV2 } from "../session/message-v2"
|
||||||
|
import { Mode } from "../session/mode"
|
||||||
|
|
||||||
const ERRORS = {
|
const ERRORS = {
|
||||||
400: {
|
400: {
|
||||||
@@ -681,6 +682,26 @@ export namespace Server {
|
|||||||
return c.json(true)
|
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
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from "ai"
|
} from "ai"
|
||||||
|
|
||||||
import PROMPT_INITIALIZE from "../session/prompt/initialize.txt"
|
import PROMPT_INITIALIZE from "../session/prompt/initialize.txt"
|
||||||
|
import PROMPT_PLAN from "../session/prompt/plan.txt"
|
||||||
|
|
||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
import { Bus } from "../bus"
|
import { Bus } from "../bus"
|
||||||
@@ -29,12 +30,12 @@ import type { ModelsDev } from "../provider/models"
|
|||||||
import { Share } from "../share/share"
|
import { Share } from "../share/share"
|
||||||
import { Snapshot } from "../snapshot"
|
import { Snapshot } from "../snapshot"
|
||||||
import { Storage } from "../storage/storage"
|
import { Storage } from "../storage/storage"
|
||||||
import type { Tool } from "../tool/tool"
|
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { NamedError } from "../util/error"
|
import { NamedError } from "../util/error"
|
||||||
import { SystemPrompt } from "./system"
|
import { SystemPrompt } from "./system"
|
||||||
import { FileTime } from "../file/time"
|
import { FileTime } from "../file/time"
|
||||||
import { MessageV2 } from "./message-v2"
|
import { MessageV2 } from "./message-v2"
|
||||||
|
import { Mode } from "./mode"
|
||||||
|
|
||||||
export namespace Session {
|
export namespace Session {
|
||||||
const log = Log.create({ service: "session" })
|
const log = Log.create({ service: "session" })
|
||||||
@@ -281,13 +282,13 @@ export namespace Session {
|
|||||||
sessionID: string
|
sessionID: string
|
||||||
providerID: string
|
providerID: string
|
||||||
modelID: string
|
modelID: string
|
||||||
|
mode?: string
|
||||||
parts: MessageV2.UserPart[]
|
parts: MessageV2.UserPart[]
|
||||||
system?: string[]
|
|
||||||
tools?: Tool.Info[]
|
|
||||||
}) {
|
}) {
|
||||||
using abort = lock(input.sessionID)
|
using abort = lock(input.sessionID)
|
||||||
const l = log.clone().tag("session", input.sessionID)
|
const l = log.clone().tag("session", input.sessionID)
|
||||||
l.info("chatting")
|
l.info("chatting")
|
||||||
|
|
||||||
const model = await Provider.getModel(input.providerID, input.modelID)
|
const model = await Provider.getModel(input.providerID, input.modelID)
|
||||||
let msgs = await messages(input.sessionID)
|
let msgs = await messages(input.sessionID)
|
||||||
const session = await get(input.sessionID)
|
const session = await get(input.sessionID)
|
||||||
@@ -364,6 +365,7 @@ export namespace Session {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
|
synthetic: true,
|
||||||
text: ["Called the Read tool on " + url.pathname, "<results>", text, "</results>"].join("\n"),
|
text: ["Called the Read tool on " + url.pathname, "<results>", text, "</results>"].join("\n"),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -373,6 +375,7 @@ export namespace Session {
|
|||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: `Called the Read tool with the following input: {\"filePath\":\"${url.pathname}\"}`,
|
text: `Called the Read tool with the following input: {\"filePath\":\"${url.pathname}\"}`,
|
||||||
|
synthetic: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "file",
|
type: "file",
|
||||||
@@ -386,6 +389,14 @@ export namespace Session {
|
|||||||
return [part]
|
return [part]
|
||||||
}),
|
}),
|
||||||
).then((x) => x.flat())
|
).then((x) => x.flat())
|
||||||
|
|
||||||
|
if (true)
|
||||||
|
input.parts.push({
|
||||||
|
type: "text",
|
||||||
|
text: PROMPT_PLAN,
|
||||||
|
synthetic: true,
|
||||||
|
})
|
||||||
|
|
||||||
if (msgs.length === 0 && !session.parentID) {
|
if (msgs.length === 0 && !session.parentID) {
|
||||||
generateText({
|
generateText({
|
||||||
maxOutputTokens: input.providerID === "google" ? 1024 : 20,
|
maxOutputTokens: input.providerID === "google" ? 1024 : 20,
|
||||||
@@ -431,9 +442,13 @@ export namespace Session {
|
|||||||
await updateMessage(msg)
|
await updateMessage(msg)
|
||||||
msgs.push(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.environment()))
|
||||||
system.push(...(await SystemPrompt.custom()))
|
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 = {
|
const next: MessageV2.Info = {
|
||||||
id: Identifier.ascending("message"),
|
id: Identifier.ascending("message"),
|
||||||
@@ -462,7 +477,8 @@ export namespace Session {
|
|||||||
const tools: Record<string, AITool> = {}
|
const tools: Record<string, AITool> = {}
|
||||||
|
|
||||||
for (const item of await Provider.tools(input.providerID)) {
|
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,
|
id: item.id as any,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
inputSchema: item.parameters as ZodSchema,
|
inputSchema: item.parameters as ZodSchema,
|
||||||
@@ -494,6 +510,7 @@ export namespace Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const [key, item] of Object.entries(await MCP.tools())) {
|
for (const [key, item] of Object.entries(await MCP.tools())) {
|
||||||
|
if (mode.tools[key] === false) continue
|
||||||
const execute = item.execute
|
const execute = item.execute
|
||||||
if (!execute) continue
|
if (!execute) continue
|
||||||
item.execute = async (args, opts) => {
|
item.execute = async (args, opts) => {
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ export namespace MessageV2 {
|
|||||||
.object({
|
.object({
|
||||||
type: z.literal("text"),
|
type: z.literal("text"),
|
||||||
text: z.string(),
|
text: z.string(),
|
||||||
|
synthetic: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
.openapi({
|
.openapi({
|
||||||
ref: "TextPart",
|
ref: "TextPart",
|
||||||
|
|||||||
74
packages/opencode/src/session/mode.ts
Normal file
74
packages/opencode/src/session/mode.ts
Normal file
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
3
packages/opencode/src/session/prompt/plan.txt
Normal file
3
packages/opencode/src/session/prompt/plan.txt
Normal file
@@ -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>
|
||||||
Reference in New Issue
Block a user