Compare commits

...
15 Commits
Author SHA1 Message Date
Dax Raad 10ae43a121 wip: sync
publish / publish (push) Has been cancelled
2025-07-25 15:52:27 -04:00
Dax Raad c85b970903 wip: drop 2025-07-25 15:51:02 -04:00
Dax Raad 7044662cfa handle uploaded text/plain 2025-07-25 15:48:42 -04:00
kehanzhang 92656fdf29 fix(headless): respect mode passed to /message endpoint (#1300) 2025-07-25 15:26:49 -04:00
Dax Raad c65e7aff86 docs: mode temperature 2025-07-25 13:45:04 -04:00
Dax Raad e97613ef9f allow temperature to be configured per mode
publish / publish (push) Has been cancelled
2025-07-25 13:29:44 -04:00
Dominik Engelhardt 827469c725 fix: apply content-level caching for non-anthropic providers (#1305) 2025-07-25 12:19:44 -04:00
Yihui Khuu 613b5fbe48 feat: add csharp lsp (#1312) 2025-07-25 12:17:06 -04:00
Dax Raad 7ed05962db fix issue with trailing whitespace error in assistant message 2025-07-25 10:56:16 -04:00
Dax Raad 250a86ec52 fix reading model from config
publish / publish (push) Has been cancelled
2025-07-25 10:53:37 -04:00
Yihui Khuu 0795a577e0 fix: header width to display header in one line when sharing disabled (#1310) 2025-07-25 09:32:06 -05:00
Dax Raad 8e5607f9c0 fix double system prompt 2025-07-25 10:28:42 -04:00
Dax Raad d6b3bb0807 disable todo tools by default in agent
publish / publish (push) Has been cancelled
2025-07-25 10:23:23 -04:00
Dax Raad f307a5ce0b fix symlinked agents
publish / publish (push) Has been cancelled
2025-07-25 10:20:16 -04:00
GitHub Action 151c7ed5a2 ignore: update download stats 2025-07-25 2025-07-25 12:04:21 +00:00
28 changed files with 396 additions and 218 deletions
+1
View File
@@ -26,3 +26,4 @@
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) | | 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) | | 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) | | 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
+4 -1
View File
@@ -44,7 +44,10 @@ export namespace Agent {
item = result[key] = { item = result[key] = {
name: key, name: key,
description: "", description: "",
tools: {}, tools: {
todowrite: false,
todoread: false,
},
} }
const model = value.model ?? cfg.model const model = value.model ?? cfg.model
if (model) item.model = Provider.parseModel(model) if (model) item.model = Provider.parseModel(model)
+1
View File
@@ -99,6 +99,7 @@ export namespace Config {
export const Mode = z export const Mode = z
.object({ .object({
model: z.string().optional(), model: z.string().optional(),
temperature: z.number().optional(),
prompt: z.string().optional(), prompt: z.string().optional(),
tools: z.record(z.string(), z.boolean()).optional(), tools: z.record(z.string(), z.boolean()).optional(),
disable: z.boolean().optional(), disable: z.boolean().optional(),
+39
View File
@@ -322,4 +322,43 @@ export namespace LSPServer {
} }
}, },
} }
export const CSharp: Info = {
id: "csharp",
root: NearestRoot([".sln", ".csproj", "global.json"]),
extensions: [".cs"],
async spawn(_, root) {
let bin = Bun.which("csharp-ls", {
PATH: process.env["PATH"] + ":" + Global.Path.bin,
})
if (!bin) {
if (!Bun.which("dotnet")) {
log.error(".NET SDK is required to install csharp-ls")
return
}
log.info("installing csharp-ls via dotnet tool")
const proc = Bun.spawn({
cmd: ["dotnet", "tool", "install", "csharp-ls", "--tool-path", Global.Path.bin],
stdout: "pipe",
stderr: "pipe",
stdin: "pipe",
})
const exit = await proc.exited
if (exit !== 0) {
log.error("Failed to install csharp-ls")
return
}
bin = path.join(Global.Path.bin, "csharp-ls" + (process.platform === "win32" ? ".exe" : ""))
log.info(`installed csharp-ls`, { bin })
}
return {
process: spawn(bin, {
cwd: root,
}),
}
},
}
} }
-142
View File
@@ -5,22 +5,11 @@ import { mergeDeep, sortBy } from "remeda"
import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai" import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
import { Log } from "../util/log" import { Log } from "../util/log"
import { BunProc } from "../bun" import { BunProc } from "../bun"
import { BashTool } from "../tool/bash"
import { EditTool } from "../tool/edit"
import { WebFetchTool } from "../tool/webfetch"
import { GlobTool } from "../tool/glob"
import { GrepTool } from "../tool/grep"
import { ListTool } from "../tool/ls"
import { PatchTool } from "../tool/patch"
import { ReadTool } from "../tool/read"
import { WriteTool } from "../tool/write"
import { TodoReadTool, TodoWriteTool } from "../tool/todo"
import { AuthAnthropic } from "../auth/anthropic" import { AuthAnthropic } from "../auth/anthropic"
import { AuthCopilot } from "../auth/copilot" import { AuthCopilot } from "../auth/copilot"
import { ModelsDev } from "./models" import { ModelsDev } from "./models"
import { NamedError } from "../util/error" import { NamedError } from "../util/error"
import { Auth } from "../auth" import { Auth } from "../auth"
import { TaskTool } from "../tool/task"
export namespace Provider { export namespace Provider {
const log = Log.create({ service: "provider" }) const log = Log.create({ service: "provider" })
@@ -468,137 +457,6 @@ export namespace Provider {
} }
} }
const TOOLS = [
BashTool,
EditTool,
WebFetchTool,
GlobTool,
GrepTool,
ListTool,
// LspDiagnosticTool,
// LspHoverTool,
PatchTool,
ReadTool,
// MultiEditTool,
WriteTool,
TodoWriteTool,
TodoReadTool,
TaskTool,
]
export async function tools(providerID: string) {
const result = await Promise.all(TOOLS.map((t) => t()))
switch (providerID) {
case "anthropic":
return result.filter((t) => t.id !== "patch")
case "openai":
return result.map((t) => ({
...t,
parameters: optionalToNullable(t.parameters),
}))
case "azure":
return result.map((t) => ({
...t,
parameters: optionalToNullable(t.parameters),
}))
case "google":
return result.map((t) => ({
...t,
parameters: sanitizeGeminiParameters(t.parameters),
}))
default:
return result
}
}
function sanitizeGeminiParameters(schema: z.ZodTypeAny, visited = new Set()): z.ZodTypeAny {
if (!schema || visited.has(schema)) {
return schema
}
visited.add(schema)
if (schema instanceof z.ZodDefault) {
const innerSchema = schema.removeDefault()
// Handle Gemini's incompatibility with `default` on `anyOf` (unions).
if (innerSchema instanceof z.ZodUnion) {
// The schema was `z.union(...).default(...)`, which is not allowed.
// We strip the default and return the sanitized union.
return sanitizeGeminiParameters(innerSchema, visited)
}
// Otherwise, the default is on a regular type, which is allowed.
// We recurse on the inner type and then re-apply the default.
return sanitizeGeminiParameters(innerSchema, visited).default(schema._def.defaultValue())
}
if (schema instanceof z.ZodOptional) {
return z.optional(sanitizeGeminiParameters(schema.unwrap(), visited))
}
if (schema instanceof z.ZodObject) {
const newShape: Record<string, z.ZodTypeAny> = {}
for (const [key, value] of Object.entries(schema.shape)) {
newShape[key] = sanitizeGeminiParameters(value as z.ZodTypeAny, visited)
}
return z.object(newShape)
}
if (schema instanceof z.ZodArray) {
return z.array(sanitizeGeminiParameters(schema.element, visited))
}
if (schema instanceof z.ZodUnion) {
// This schema corresponds to `anyOf` in JSON Schema.
// We recursively sanitize each option in the union.
const sanitizedOptions = schema.options.map((option: z.ZodTypeAny) => sanitizeGeminiParameters(option, visited))
return z.union(sanitizedOptions as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]])
}
if (schema instanceof z.ZodString) {
const newSchema = z.string({ description: schema.description })
const safeChecks = ["min", "max", "length", "regex", "startsWith", "endsWith", "includes", "trim"]
// rome-ignore lint/suspicious/noExplicitAny: <explanation>
;(newSchema._def as any).checks = (schema._def as z.ZodStringDef).checks.filter((check) =>
safeChecks.includes(check.kind),
)
return newSchema
}
return schema
}
function optionalToNullable(schema: z.ZodTypeAny): z.ZodTypeAny {
if (schema instanceof z.ZodObject) {
const shape = schema.shape
const newShape: Record<string, z.ZodTypeAny> = {}
for (const [key, value] of Object.entries(shape)) {
const zodValue = value as z.ZodTypeAny
if (zodValue instanceof z.ZodOptional) {
newShape[key] = zodValue.unwrap().nullable()
} else {
newShape[key] = optionalToNullable(zodValue)
}
}
return z.object(newShape)
}
if (schema instanceof z.ZodArray) {
return z.array(optionalToNullable(schema.element))
}
if (schema instanceof z.ZodUnion) {
return z.union(
schema.options.map((option: z.ZodTypeAny) => optionalToNullable(option)) as [
z.ZodTypeAny,
z.ZodTypeAny,
...z.ZodTypeAny[],
],
)
}
return schema
}
export const ModelNotFoundError = NamedError.create( export const ModelNotFoundError = NamedError.create(
"ProviderModelNotFoundError", "ProviderModelNotFoundError",
z.object({ z.object({
+34 -12
View File
@@ -7,24 +7,46 @@ export namespace ProviderTransform {
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2) const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
const final = msgs.filter((msg) => msg.role !== "system").slice(-2) const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
const providerOptions = {
anthropic: {
cacheControl: { type: "ephemeral" },
},
openrouter: {
cache_control: { type: "ephemeral" },
},
bedrock: {
cachePoint: { type: "ephemeral" },
},
openaiCompatible: {
cache_control: { type: "ephemeral" },
},
}
for (const msg of unique([...system, ...final])) { for (const msg of unique([...system, ...final])) {
const shouldUseContentOptions = providerID !== "anthropic" && Array.isArray(msg.content) && msg.content.length > 0
if (shouldUseContentOptions) {
const lastContent = msg.content[msg.content.length - 1]
if (lastContent && typeof lastContent === "object") {
lastContent.providerOptions = {
...lastContent.providerOptions,
...providerOptions,
}
continue
}
}
msg.providerOptions = { msg.providerOptions = {
...msg.providerOptions, ...msg.providerOptions,
anthropic: { ...providerOptions,
cacheControl: { type: "ephemeral" },
},
openrouter: {
cache_control: { type: "ephemeral" },
},
bedrock: {
cachePoint: { type: "ephemeral" },
},
openaiCompatible: {
cache_control: { type: "ephemeral" },
},
} }
} }
} }
return msgs return msgs
} }
export function temperature(_providerID: string, modelID: string) {
if (modelID.includes("qwen")) return 0.55
return 0
}
} }
+43 -8
View File
@@ -39,7 +39,8 @@ import { MessageV2 } from "./message-v2"
import { Mode } from "./mode" import { Mode } from "./mode"
import { LSP } from "../lsp" import { LSP } from "../lsp"
import { ReadTool } from "../tool/read" import { ReadTool } from "../tool/read"
import { splitWhen } from "remeda" import { mergeDeep, pipe, splitWhen } from "remeda"
import { ToolRegistry } from "../tool/registry"
export namespace Session { export namespace Session {
const log = Log.create({ service: "session" }) const log = Log.create({ service: "session" })
@@ -387,6 +388,34 @@ export namespace Session {
if (part.type === "file") { if (part.type === "file") {
const url = new URL(part.url) const url = new URL(part.url)
switch (url.protocol) { switch (url.protocol) {
case "data:":
if (part.mime === "text/plain") {
return [
{
id: Identifier.ascending("part"),
messageID: userMsg.id,
sessionID: input.sessionID,
type: "text",
synthetic: true,
text: `Called the Read tool with the following input: ${JSON.stringify({ filePath: part.filename })}`,
},
{
id: Identifier.ascending("part"),
messageID: userMsg.id,
sessionID: input.sessionID,
type: "text",
synthetic: true,
text: Buffer.from(part.url, "base64url").toString(),
},
{
...part,
id: part.id ?? Identifier.ascending("part"),
messageID: userMsg.id,
sessionID: input.sessionID,
},
]
}
break
case "file:": case "file:":
// have to normalize, symbol search returns absolute paths // have to normalize, symbol search returns absolute paths
// Decode the pathname since URL constructor doesn't automatically decode it // Decode the pathname since URL constructor doesn't automatically decode it
@@ -430,7 +459,7 @@ export namespace Session {
} }
} }
const args = { filePath, offset, limit } const args = { filePath, offset, limit }
const result = await ReadTool().then((t) => const result = await ReadTool.init().then((t) =>
t.execute(args, { t.execute(args, {
sessionID: input.sessionID, sessionID: input.sessionID,
abort: new AbortController().signal, abort: new AbortController().signal,
@@ -626,7 +655,6 @@ export namespace Session {
return SystemPrompt.provider(input.modelID) return SystemPrompt.provider(input.modelID)
})(), })(),
) )
system.push(...(mode.prompt ? [mode.prompt] : SystemPrompt.provider(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 // max 2 system prompt messages for caching purposes
@@ -661,10 +689,13 @@ export namespace Session {
const processor = createProcessor(assistantMsg, model.info) const processor = createProcessor(assistantMsg, model.info)
for (const item of await Provider.tools(input.providerID)) { const enabledTools = pipe(
if (mode.tools[item.id] === false) continue mode.tools,
if (input.tools?.[item.id] === false) continue mergeDeep(ToolRegistry.enabled(input.providerID, input.modelID)),
if (session.parentID && item.id === "task") continue mergeDeep(input.tools ?? {}),
)
for (const item of await ToolRegistry.tools(input.providerID, input.modelID)) {
if (enabledTools[item.id] === false) continue
tools[item.id] = tool({ tools[item.id] = tool({
id: item.id as any, id: item.id as any,
description: item.description, description: item.description,
@@ -765,6 +796,7 @@ export namespace Session {
}, },
modelID: input.modelID, modelID: input.modelID,
providerID: input.providerID, providerID: input.providerID,
mode: inputMode,
time: { time: {
created: Date.now(), created: Date.now(),
}, },
@@ -792,7 +824,9 @@ export namespace Session {
), ),
...MessageV2.toModelMessage(msgs), ...MessageV2.toModelMessage(msgs),
], ],
temperature: model.info.temperature ? 0 : undefined, temperature: model.info.temperature
? (mode.temperature ?? ProviderTransform.temperature(input.providerID, input.modelID))
: undefined,
tools: model.info.tool_call === false ? undefined : tools, tools: model.info.tool_call === false ? undefined : tools,
model: wrapLanguageModel({ model: wrapLanguageModel({
model: model.language, model: model.language,
@@ -1005,6 +1039,7 @@ export namespace Session {
start: Date.now(), start: Date.now(),
end: Date.now(), end: Date.now(),
} }
currentText.text = currentText.text.trimEnd()
await updatePart(currentText) await updatePart(currentText)
} }
currentText = undefined currentText = undefined
+6 -4
View File
@@ -7,6 +7,7 @@ export namespace Mode {
export const Info = z export const Info = z
.object({ .object({
name: z.string(), name: z.string(),
temperature: z.number().optional(),
model: z model: z
.object({ .object({
modelID: z.string(), modelID: z.string(),
@@ -22,13 +23,16 @@ export namespace Mode {
export type Info = z.infer<typeof Info> export type Info = z.infer<typeof Info>
const state = App.state("mode", async () => { const state = App.state("mode", async () => {
const cfg = await Config.get() const cfg = await Config.get()
const model = cfg.model ? Provider.parseModel(cfg.model) : undefined
const result: Record<string, Info> = { const result: Record<string, Info> = {
build: { build: {
model,
name: "build", name: "build",
tools: {}, tools: {},
}, },
plan: { plan: {
name: "plan", name: "plan",
model,
tools: { tools: {
write: false, write: false,
edit: false, edit: false,
@@ -45,11 +49,9 @@ export namespace Mode {
tools: {}, tools: {},
} }
item.name = key item.name = key
const model = value.model ?? cfg.model if (value.model) item.model = Provider.parseModel(value.model)
if (model) {
item.model = Provider.parseModel(model)
}
if (value.prompt) item.prompt = value.prompt if (value.prompt) item.prompt = value.prompt
if (value.temperature) item.temperature = value.temperature
if (value.tools) if (value.tools)
item.tools = { item.tools = {
...value.tools, ...value.tools,
+1 -2
View File
@@ -7,8 +7,7 @@ const MAX_OUTPUT_LENGTH = 30000
const DEFAULT_TIMEOUT = 1 * 60 * 1000 const DEFAULT_TIMEOUT = 1 * 60 * 1000
const MAX_TIMEOUT = 10 * 60 * 1000 const MAX_TIMEOUT = 10 * 60 * 1000
export const BashTool = Tool.define({ export const BashTool = Tool.define("bash", {
id: "bash",
description: DESCRIPTION, description: DESCRIPTION,
parameters: z.object({ parameters: z.object({
command: z.string().describe("The command to execute"), command: z.string().describe("The command to execute"),
+1 -2
View File
@@ -14,8 +14,7 @@ import { File } from "../file"
import { Bus } from "../bus" import { Bus } from "../bus"
import { FileTime } from "../file/time" import { FileTime } from "../file/time"
export const EditTool = Tool.define({ export const EditTool = Tool.define("edit", {
id: "edit",
description: DESCRIPTION, description: DESCRIPTION,
parameters: z.object({ parameters: z.object({
filePath: z.string().describe("The absolute path to the file to modify"), filePath: z.string().describe("The absolute path to the file to modify"),
+1 -2
View File
@@ -5,8 +5,7 @@ import { App } from "../app/app"
import DESCRIPTION from "./glob.txt" import DESCRIPTION from "./glob.txt"
import { Ripgrep } from "../file/ripgrep" import { Ripgrep } from "../file/ripgrep"
export const GlobTool = Tool.define({ export const GlobTool = Tool.define("glob", {
id: "glob",
description: DESCRIPTION, description: DESCRIPTION,
parameters: z.object({ parameters: z.object({
pattern: z.string().describe("The glob pattern to match files against"), pattern: z.string().describe("The glob pattern to match files against"),
+1 -2
View File
@@ -5,8 +5,7 @@ import { Ripgrep } from "../file/ripgrep"
import DESCRIPTION from "./grep.txt" import DESCRIPTION from "./grep.txt"
export const GrepTool = Tool.define({ export const GrepTool = Tool.define("grep", {
id: "grep",
description: DESCRIPTION, description: DESCRIPTION,
parameters: z.object({ parameters: z.object({
pattern: z.string().describe("The regex pattern to search for in file contents"), pattern: z.string().describe("The regex pattern to search for in file contents"),
+1 -2
View File
@@ -33,8 +33,7 @@ export const IGNORE_PATTERNS = [
const LIMIT = 100 const LIMIT = 100
export const ListTool = Tool.define({ export const ListTool = Tool.define("list", {
id: "list",
description: DESCRIPTION, description: DESCRIPTION,
parameters: z.object({ parameters: z.object({
path: z.string().describe("The absolute path to the directory to list (must be absolute, not relative)").optional(), path: z.string().describe("The absolute path to the directory to list (must be absolute, not relative)").optional(),
@@ -5,8 +5,7 @@ import { LSP } from "../lsp"
import { App } from "../app/app" import { App } from "../app/app"
import DESCRIPTION from "./lsp-diagnostics.txt" import DESCRIPTION from "./lsp-diagnostics.txt"
export const LspDiagnosticTool = Tool.define({ export const LspDiagnosticTool = Tool.define("lsp_diagnostics", {
id: "lsp_diagnostics",
description: DESCRIPTION, description: DESCRIPTION,
parameters: z.object({ parameters: z.object({
path: z.string().describe("The path to the file to get diagnostics."), path: z.string().describe("The path to the file to get diagnostics."),
+1 -2
View File
@@ -5,8 +5,7 @@ import { LSP } from "../lsp"
import { App } from "../app/app" import { App } from "../app/app"
import DESCRIPTION from "./lsp-hover.txt" import DESCRIPTION from "./lsp-hover.txt"
export const LspHoverTool = Tool.define({ export const LspHoverTool = Tool.define("lsp_hover", {
id: "lsp_hover",
description: DESCRIPTION, description: DESCRIPTION,
parameters: z.object({ parameters: z.object({
file: z.string().describe("The path to the file to get diagnostics."), file: z.string().describe("The path to the file to get diagnostics."),
+2 -3
View File
@@ -5,8 +5,7 @@ import DESCRIPTION from "./multiedit.txt"
import path from "path" import path from "path"
import { App } from "../app/app" import { App } from "../app/app"
export const MultiEditTool = Tool.define({ export const MultiEditTool = Tool.define("multiedit", {
id: "multiedit",
description: DESCRIPTION, description: DESCRIPTION,
parameters: z.object({ parameters: z.object({
filePath: z.string().describe("The absolute path to the file to modify"), filePath: z.string().describe("The absolute path to the file to modify"),
@@ -22,7 +21,7 @@ export const MultiEditTool = Tool.define({
.describe("Array of edit operations to perform sequentially on the file"), .describe("Array of edit operations to perform sequentially on the file"),
}), }),
async execute(params, ctx) { async execute(params, ctx) {
const tool = await EditTool() const tool = await EditTool.init()
const results = [] const results = []
for (const [, edit] of params.edits.entries()) { for (const [, edit] of params.edits.entries()) {
const result = await tool.execute( const result = await tool.execute(
+1 -2
View File
@@ -210,8 +210,7 @@ async function applyCommit(
} }
} }
export const PatchTool = Tool.define({ export const PatchTool = Tool.define("patch", {
id: "patch",
description: DESCRIPTION, description: DESCRIPTION,
parameters: PatchParams, parameters: PatchParams,
execute: async (params, ctx) => { execute: async (params, ctx) => {
+1 -2
View File
@@ -10,8 +10,7 @@ import { App } from "../app/app"
const DEFAULT_READ_LIMIT = 2000 const DEFAULT_READ_LIMIT = 2000
const MAX_LINE_LENGTH = 2000 const MAX_LINE_LENGTH = 2000
export const ReadTool = Tool.define({ export const ReadTool = Tool.define("read", {
id: "read",
description: DESCRIPTION, description: DESCRIPTION,
parameters: z.object({ parameters: z.object({
filePath: z.string().describe("The path to the file to read"), filePath: z.string().describe("The path to the file to read"),
+170
View File
@@ -0,0 +1,170 @@
import z from "zod"
import { BashTool } from "./bash"
import { EditTool } from "./edit"
import { GlobTool } from "./glob"
import { GrepTool } from "./grep"
import { ListTool } from "./ls"
import { PatchTool } from "./patch"
import { ReadTool } from "./read"
import { TaskTool } from "./task"
import { TodoWriteTool, TodoReadTool } from "./todo"
import { WebFetchTool } from "./webfetch"
import { WriteTool } from "./write"
export namespace ToolRegistry {
const ALL = [
BashTool,
EditTool,
WebFetchTool,
GlobTool,
GrepTool,
ListTool,
PatchTool,
ReadTool,
WriteTool,
TodoWriteTool,
TodoReadTool,
TaskTool,
]
export function ids() {
return ALL.map((t) => t.id)
}
export async function tools(providerID: string, _modelID: string) {
const result = await Promise.all(
ALL.map(async (t) => ({
id: t.id,
...(await t.init()),
})),
)
if (providerID === "openai") {
return result.map((t) => ({
...t,
parameters: optionalToNullable(t.parameters),
}))
}
if (providerID === "azure") {
return result.map((t) => ({
...t,
parameters: optionalToNullable(t.parameters),
}))
}
if (providerID === "google") {
return result.map((t) => ({
...t,
parameters: sanitizeGeminiParameters(t.parameters),
}))
}
return result
}
export function enabled(_providerID: string, modelID: string): Record<string, boolean> {
if (modelID.includes("claude")) {
return {
patch: false,
}
}
if (modelID.includes("qwen")) {
return {
patch: false,
todowrite: false,
todoread: false,
}
}
return {}
}
function sanitizeGeminiParameters(schema: z.ZodTypeAny, visited = new Set()): z.ZodTypeAny {
if (!schema || visited.has(schema)) {
return schema
}
visited.add(schema)
if (schema instanceof z.ZodDefault) {
const innerSchema = schema.removeDefault()
// Handle Gemini's incompatibility with `default` on `anyOf` (unions).
if (innerSchema instanceof z.ZodUnion) {
// The schema was `z.union(...).default(...)`, which is not allowed.
// We strip the default and return the sanitized union.
return sanitizeGeminiParameters(innerSchema, visited)
}
// Otherwise, the default is on a regular type, which is allowed.
// We recurse on the inner type and then re-apply the default.
return sanitizeGeminiParameters(innerSchema, visited).default(schema._def.defaultValue())
}
if (schema instanceof z.ZodOptional) {
return z.optional(sanitizeGeminiParameters(schema.unwrap(), visited))
}
if (schema instanceof z.ZodObject) {
const newShape: Record<string, z.ZodTypeAny> = {}
for (const [key, value] of Object.entries(schema.shape)) {
newShape[key] = sanitizeGeminiParameters(value as z.ZodTypeAny, visited)
}
return z.object(newShape)
}
if (schema instanceof z.ZodArray) {
return z.array(sanitizeGeminiParameters(schema.element, visited))
}
if (schema instanceof z.ZodUnion) {
// This schema corresponds to `anyOf` in JSON Schema.
// We recursively sanitize each option in the union.
const sanitizedOptions = schema.options.map((option: z.ZodTypeAny) => sanitizeGeminiParameters(option, visited))
return z.union(sanitizedOptions as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]])
}
if (schema instanceof z.ZodString) {
const newSchema = z.string({ description: schema.description })
const safeChecks = ["min", "max", "length", "regex", "startsWith", "endsWith", "includes", "trim"]
// rome-ignore lint/suspicious/noExplicitAny: <explanation>
;(newSchema._def as any).checks = (schema._def as z.ZodStringDef).checks.filter((check) =>
safeChecks.includes(check.kind),
)
return newSchema
}
return schema
}
function optionalToNullable(schema: z.ZodTypeAny): z.ZodTypeAny {
if (schema instanceof z.ZodObject) {
const shape = schema.shape
const newShape: Record<string, z.ZodTypeAny> = {}
for (const [key, value] of Object.entries(shape)) {
const zodValue = value as z.ZodTypeAny
if (zodValue instanceof z.ZodOptional) {
newShape[key] = zodValue.unwrap().nullable()
} else {
newShape[key] = optionalToNullable(zodValue)
}
}
return z.object(newShape)
}
if (schema instanceof z.ZodArray) {
return z.array(optionalToNullable(schema.element))
}
if (schema instanceof z.ZodUnion) {
return z.union(
schema.options.map((option: z.ZodTypeAny) => optionalToNullable(option)) as [
z.ZodTypeAny,
z.ZodTypeAny,
...z.ZodTypeAny[],
],
)
}
return schema
}
}
+5 -3
View File
@@ -7,11 +7,10 @@ import { MessageV2 } from "../session/message-v2"
import { Identifier } from "../id/id" import { Identifier } from "../id/id"
import { Agent } from "../agent/agent" import { Agent } from "../agent/agent"
export const TaskTool = Tool.define(async () => { export const TaskTool = Tool.define("task", async () => {
const agents = await Agent.list() const agents = await Agent.list()
const description = DESCRIPTION.replace("{agents}", agents.map((a) => `- ${a.name}: ${a.description}`).join("\n")) const description = DESCRIPTION.replace("{agents}", agents.map((a) => `- ${a.name}: ${a.description}`).join("\n"))
return { return {
id: "task",
description, description,
parameters: z.object({ parameters: z.object({
description: z.string().describe("A short (3-5 words) description of the task"), description: z.string().describe("A short (3-5 words) description of the task"),
@@ -53,7 +52,10 @@ export const TaskTool = Tool.define(async () => {
providerID: model.providerID, providerID: model.providerID,
mode: msg.mode, mode: msg.mode,
system: agent.prompt, system: agent.prompt,
tools: agent.tools, tools: {
...agent.tools,
task: false,
},
parts: [ parts: [
{ {
id: Identifier.ascending("part"), id: Identifier.ascending("part"),
+2 -4
View File
@@ -18,8 +18,7 @@ const state = App.state("todo-tool", () => {
return todos return todos
}) })
export const TodoWriteTool = Tool.define({ export const TodoWriteTool = Tool.define("todowrite", {
id: "todowrite",
description: DESCRIPTION_WRITE, description: DESCRIPTION_WRITE,
parameters: z.object({ parameters: z.object({
todos: z.array(TodoInfo).describe("The updated todo list"), todos: z.array(TodoInfo).describe("The updated todo list"),
@@ -37,8 +36,7 @@ export const TodoWriteTool = Tool.define({
}, },
}) })
export const TodoReadTool = Tool.define({ export const TodoReadTool = Tool.define("todoread", {
id: "todoread",
description: "Use this tool to read your todo list", description: "Use this tool to read your todo list",
parameters: z.object({}), parameters: z.object({}),
async execute(_params, opts) { async execute(_params, opts) {
+21 -12
View File
@@ -12,21 +12,30 @@ export namespace Tool {
} }
export interface Info<Parameters extends StandardSchemaV1 = StandardSchemaV1, M extends Metadata = Metadata> { export interface Info<Parameters extends StandardSchemaV1 = StandardSchemaV1, M extends Metadata = Metadata> {
id: string id: string
description: string init: () => Promise<{
parameters: Parameters description: string
execute( parameters: Parameters
args: StandardSchemaV1.InferOutput<Parameters>, execute(
ctx: Context, args: StandardSchemaV1.InferOutput<Parameters>,
): Promise<{ ctx: Context,
title: string ): Promise<{
metadata: M title: string
output: string metadata: M
output: string
}>
}> }>
} }
export function define<Parameters extends StandardSchemaV1, Result extends Metadata>( export function define<Parameters extends StandardSchemaV1, Result extends Metadata>(
input: Info<Parameters, Result> | (() => Promise<Info<Parameters, Result>>), id: string,
): () => Promise<Info<Parameters, Result>> { init: Info<Parameters, Result>["init"] | Awaited<ReturnType<Info<Parameters, Result>["init"]>>,
return input instanceof Function ? input : async () => input ): Info<Parameters, Result> {
return {
id,
init: async () => {
if (init instanceof Function) return init()
return init
},
}
} }
} }
+1 -2
View File
@@ -7,8 +7,7 @@ const MAX_RESPONSE_SIZE = 5 * 1024 * 1024 // 5MB
const DEFAULT_TIMEOUT = 30 * 1000 // 30 seconds const DEFAULT_TIMEOUT = 30 * 1000 // 30 seconds
const MAX_TIMEOUT = 120 * 1000 // 2 minutes const MAX_TIMEOUT = 120 * 1000 // 2 minutes
export const WebFetchTool = Tool.define({ export const WebFetchTool = Tool.define("webfetch", {
id: "webfetch",
description: DESCRIPTION, description: DESCRIPTION,
parameters: z.object({ parameters: z.object({
url: z.string().describe("The URL to fetch content from"), url: z.string().describe("The URL to fetch content from"),
+1 -2
View File
@@ -9,8 +9,7 @@ import { Bus } from "../bus"
import { File } from "../file" import { File } from "../file"
import { FileTime } from "../file/time" import { FileTime } from "../file/time"
export const WriteTool = Tool.define({ export const WriteTool = Tool.define("write", {
id: "write",
description: DESCRIPTION, description: DESCRIPTION,
parameters: z.object({ parameters: z.object({
filePath: z.string().describe("The absolute path to the file to write (must be absolute, not relative)"), filePath: z.string().describe("The absolute path to the file to write (must be absolute, not relative)"),
+1
View File
@@ -51,6 +51,7 @@ export namespace Filesystem {
cwd: current, cwd: current,
absolute: true, absolute: true,
onlyFiles: true, onlyFiles: true,
followSymlinks: true,
dot: true, dot: true,
})) { })) {
result.push(match) result.push(match)
+2 -2
View File
@@ -9,8 +9,8 @@ const ctx = {
abort: AbortSignal.any([]), abort: AbortSignal.any([]),
metadata: () => {}, metadata: () => {},
} }
const glob = await GlobTool() const glob = await GlobTool.init()
const list = await ListTool() const list = await ListTool.init()
describe("tool.glob", () => { describe("tool.glob", () => {
test("truncate", async () => { test("truncate", async () => {
@@ -670,15 +670,21 @@ func (m *messagesComponent) renderHeader() string {
isSubscriptionModel := m.app.Model != nil && isSubscriptionModel := m.app.Model != nil &&
m.app.Model.Cost.Input == 0 && m.app.Model.Cost.Output == 0 m.app.Model.Cost.Input == 0 && m.app.Model.Cost.Output == 0
sessionInfoText := formatTokensAndCost(tokens, contextWindow, cost, isSubscriptionModel)
sessionInfo = styles.NewStyle(). sessionInfo = styles.NewStyle().
Foreground(t.TextMuted()). Foreground(t.TextMuted()).
Background(t.Background()). Background(t.Background()).
Render(formatTokensAndCost(tokens, contextWindow, cost, isSubscriptionModel)) Render(sessionInfoText)
shareEnabled := m.app.Config.Share != opencode.ConfigShareDisabled shareEnabled := m.app.Config.Share != opencode.ConfigShareDisabled
headerTextWidth := headerWidth
if !shareEnabled {
// +1 is to ensure there is always at least one space between header and session info
headerTextWidth -= len(sessionInfoText) + 1
}
headerText := util.ToMarkdown( headerText := util.ToMarkdown(
"# "+m.app.Session.Title, "# "+m.app.Session.Title,
headerWidth, headerTextWidth,
t.Background(), t.Background(),
) )
@@ -705,11 +711,9 @@ func (m *messagesComponent) renderHeader() string {
items..., items...,
) )
var headerLines []string headerLines := []string{headerRow}
if shareEnabled { if shareEnabled {
headerLines = []string{headerText, headerRow} headerLines = []string{headerText, headerRow}
} else {
headerLines = []string{headerRow}
} }
header := strings.Join(headerLines, "\n") header := strings.Join(headerLines, "\n")
@@ -97,6 +97,51 @@ Use the `model` config to override the default model for this mode. Useful for u
--- ---
### 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 ### 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. 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.