mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-03 23:53:46 +00:00
Refactor to support multiple instances inside single opencode process (#2360)
This release has a bunch of minor breaking changes if you are using opencode plugins or sdk 1. storage events have been removed (we might bring this back but had some issues) 2. concept of `app` is gone - there is a new concept called `project` and endpoints to list projects and get the current project 3. plugin receives `directory` which is cwd and `worktree` which is where the root of the project is if it's a git repo 4. the session.chat function has been renamed to session.prompt in sdk. it no longer requires model to be passed in (model is now an object) 5. every endpoint takes an optional `directory` parameter to operate as though opencode is running in that directory
This commit is contained in:
@@ -3,13 +3,13 @@ import { exec } from "child_process"
|
||||
|
||||
import { Tool } from "./tool"
|
||||
import DESCRIPTION from "./bash.txt"
|
||||
import { App } from "../app/app"
|
||||
import { Permission } from "../permission"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { lazy } from "../util/lazy"
|
||||
import { Log } from "../util/log"
|
||||
import { Wildcard } from "../util/wildcard"
|
||||
import { $ } from "bun"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Agent } from "../agent/agent"
|
||||
|
||||
const MAX_OUTPUT_LENGTH = 30_000
|
||||
@@ -56,7 +56,6 @@ export const BashTool = Tool.define("bash", {
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT)
|
||||
const app = App.info()
|
||||
const tree = await parser().then((p) => p.parse(params.command))
|
||||
const permissions = await Agent.get(ctx.agent).then((x) => x.permission.bash)
|
||||
|
||||
@@ -88,9 +87,9 @@ export const BashTool = Tool.define("bash", {
|
||||
.text()
|
||||
.then((x) => x.trim())
|
||||
log.info("resolved path", { arg, resolved })
|
||||
if (resolved && !Filesystem.contains(app.path.cwd, resolved)) {
|
||||
if (resolved && !Filesystem.contains(Instance.directory, resolved)) {
|
||||
throw new Error(
|
||||
`This command references paths outside of ${app.path.cwd} so it is not allowed to be executed.`,
|
||||
`This command references paths outside of ${Instance.directory} so it is not allowed to be executed.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -123,7 +122,7 @@ export const BashTool = Tool.define("bash", {
|
||||
}
|
||||
|
||||
const process = exec(params.command, {
|
||||
cwd: app.path.cwd,
|
||||
cwd: Instance.directory,
|
||||
signal: ctx.abort,
|
||||
timeout,
|
||||
})
|
||||
|
||||
@@ -10,11 +10,11 @@ import { LSP } from "../lsp"
|
||||
import { createTwoFilesPatch } from "diff"
|
||||
import { Permission } from "../permission"
|
||||
import DESCRIPTION from "./edit.txt"
|
||||
import { App } from "../app/app"
|
||||
import { File } from "../file"
|
||||
import { Bus } from "../bus"
|
||||
import { FileTime } from "../file/time"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Agent } from "../agent/agent"
|
||||
|
||||
export const EditTool = Tool.define("edit", {
|
||||
@@ -34,9 +34,8 @@ export const EditTool = Tool.define("edit", {
|
||||
throw new Error("oldString and newString must be different")
|
||||
}
|
||||
|
||||
const app = App.info()
|
||||
const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath)
|
||||
if (!Filesystem.contains(app.path.cwd, filePath)) {
|
||||
const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath)
|
||||
if (!Filesystem.contains(Instance.directory, filePath)) {
|
||||
throw new Error(`File ${filePath} is not in the current working directory`)
|
||||
}
|
||||
|
||||
@@ -123,7 +122,7 @@ export const EditTool = Tool.define("edit", {
|
||||
diagnostics,
|
||||
diff,
|
||||
},
|
||||
title: `${path.relative(app.path.root, filePath)}`,
|
||||
title: `${path.relative(Instance.worktree, filePath)}`,
|
||||
output,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { z } from "zod"
|
||||
import path from "path"
|
||||
import { Tool } from "./tool"
|
||||
import { App } from "../app/app"
|
||||
import DESCRIPTION from "./glob.txt"
|
||||
import { Ripgrep } from "../file/ripgrep"
|
||||
import { Instance } from "../project/instance"
|
||||
|
||||
export const GlobTool = Tool.define("glob", {
|
||||
description: DESCRIPTION,
|
||||
@@ -17,9 +17,8 @@ export const GlobTool = Tool.define("glob", {
|
||||
),
|
||||
}),
|
||||
async execute(params) {
|
||||
const app = App.info()
|
||||
let search = params.path ?? app.path.cwd
|
||||
search = path.isAbsolute(search) ? search : path.resolve(app.path.cwd, search)
|
||||
let search = params.path ?? Instance.directory
|
||||
search = path.isAbsolute(search) ? search : path.resolve(Instance.directory, search)
|
||||
|
||||
const limit = 100
|
||||
const files = []
|
||||
@@ -55,7 +54,7 @@ export const GlobTool = Tool.define("glob", {
|
||||
}
|
||||
|
||||
return {
|
||||
title: path.relative(app.path.root, search),
|
||||
title: path.relative(Instance.worktree, search),
|
||||
metadata: {
|
||||
count: files.length,
|
||||
truncated,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { z } from "zod"
|
||||
import { Tool } from "./tool"
|
||||
import { App } from "../app/app"
|
||||
import { Ripgrep } from "../file/ripgrep"
|
||||
|
||||
import DESCRIPTION from "./grep.txt"
|
||||
import { Instance } from "../project/instance"
|
||||
|
||||
export const GrepTool = Tool.define("grep", {
|
||||
description: DESCRIPTION,
|
||||
@@ -17,8 +17,7 @@ export const GrepTool = Tool.define("grep", {
|
||||
throw new Error("pattern is required")
|
||||
}
|
||||
|
||||
const app = App.info()
|
||||
const searchPath = params.path || app.path.cwd
|
||||
const searchPath = params.path || Instance.directory
|
||||
|
||||
const rgPath = await Ripgrep.filepath()
|
||||
const args = ["-n", params.pattern]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { z } from "zod"
|
||||
import { Tool } from "./tool"
|
||||
import { App } from "../app/app"
|
||||
import * as path from "path"
|
||||
import DESCRIPTION from "./ls.txt"
|
||||
import { Instance } from "../project/instance"
|
||||
|
||||
export const IGNORE_PATTERNS = [
|
||||
"node_modules/",
|
||||
@@ -40,8 +40,7 @@ export const ListTool = Tool.define("list", {
|
||||
ignore: z.array(z.string()).describe("List of glob patterns to ignore").optional(),
|
||||
}),
|
||||
async execute(params) {
|
||||
const app = App.info()
|
||||
const searchPath = path.resolve(app.path.cwd, params.path || ".")
|
||||
const searchPath = path.resolve(Instance.directory, params.path || ".")
|
||||
|
||||
const glob = new Bun.Glob("**/*")
|
||||
const files = []
|
||||
@@ -102,7 +101,7 @@ export const ListTool = Tool.define("list", {
|
||||
const output = `${searchPath}/\n` + renderDir(".", 0)
|
||||
|
||||
return {
|
||||
title: path.relative(app.path.root, searchPath),
|
||||
title: path.relative(Instance.worktree, searchPath),
|
||||
metadata: {
|
||||
count: files.length,
|
||||
truncated: files.length >= LIMIT,
|
||||
|
||||
@@ -2,8 +2,8 @@ import { z } from "zod"
|
||||
import { Tool } from "./tool"
|
||||
import path from "path"
|
||||
import { LSP } from "../lsp"
|
||||
import { App } from "../app/app"
|
||||
import DESCRIPTION from "./lsp-diagnostics.txt"
|
||||
import { Instance } from "../project/instance"
|
||||
|
||||
export const LspDiagnosticTool = Tool.define("lsp_diagnostics", {
|
||||
description: DESCRIPTION,
|
||||
@@ -11,13 +11,12 @@ export const LspDiagnosticTool = Tool.define("lsp_diagnostics", {
|
||||
path: z.string().describe("The path to the file to get diagnostics."),
|
||||
}),
|
||||
execute: async (args) => {
|
||||
const app = App.info()
|
||||
const normalized = path.isAbsolute(args.path) ? args.path : path.join(app.path.cwd, args.path)
|
||||
const normalized = path.isAbsolute(args.path) ? args.path : path.join(Instance.directory, args.path)
|
||||
await LSP.touchFile(normalized, true)
|
||||
const diagnostics = await LSP.diagnostics()
|
||||
const file = diagnostics[normalized]
|
||||
return {
|
||||
title: path.relative(app.path.root, normalized),
|
||||
title: path.relative(Instance.worktree, normalized),
|
||||
metadata: {
|
||||
diagnostics,
|
||||
},
|
||||
|
||||
@@ -2,8 +2,8 @@ import { z } from "zod"
|
||||
import { Tool } from "./tool"
|
||||
import path from "path"
|
||||
import { LSP } from "../lsp"
|
||||
import { App } from "../app/app"
|
||||
import DESCRIPTION from "./lsp-hover.txt"
|
||||
import { Instance } from "../project/instance"
|
||||
|
||||
export const LspHoverTool = Tool.define("lsp_hover", {
|
||||
description: DESCRIPTION,
|
||||
@@ -13,8 +13,7 @@ export const LspHoverTool = Tool.define("lsp_hover", {
|
||||
character: z.number().describe("The character number to get diagnostics."),
|
||||
}),
|
||||
execute: async (args) => {
|
||||
const app = App.info()
|
||||
const file = path.isAbsolute(args.file) ? args.file : path.join(app.path.cwd, args.file)
|
||||
const file = path.isAbsolute(args.file) ? args.file : path.join(Instance.directory, args.file)
|
||||
await LSP.touchFile(file, true)
|
||||
const result = await LSP.hover({
|
||||
...args,
|
||||
@@ -22,7 +21,7 @@ export const LspHoverTool = Tool.define("lsp_hover", {
|
||||
})
|
||||
|
||||
return {
|
||||
title: path.relative(app.path.root, file) + ":" + args.line + ":" + args.character,
|
||||
title: path.relative(Instance.worktree, file) + ":" + args.line + ":" + args.character,
|
||||
metadata: {
|
||||
result,
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Tool } from "./tool"
|
||||
import { EditTool } from "./edit"
|
||||
import DESCRIPTION from "./multiedit.txt"
|
||||
import path from "path"
|
||||
import { App } from "../app/app"
|
||||
import { Instance } from "../project/instance"
|
||||
|
||||
export const MultiEditTool = Tool.define("multiedit", {
|
||||
description: DESCRIPTION,
|
||||
@@ -35,9 +35,8 @@ export const MultiEditTool = Tool.define("multiedit", {
|
||||
)
|
||||
results.push(result)
|
||||
}
|
||||
const app = App.info()
|
||||
return {
|
||||
title: path.relative(app.path.root, params.filePath),
|
||||
title: path.relative(Instance.worktree, params.filePath),
|
||||
metadata: {
|
||||
results: results.map((r) => r.metadata),
|
||||
},
|
||||
|
||||
@@ -5,8 +5,8 @@ import { Tool } from "./tool"
|
||||
import { LSP } from "../lsp"
|
||||
import { FileTime } from "../file/time"
|
||||
import DESCRIPTION from "./read.txt"
|
||||
import { App } from "../app/app"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { Instance } from "../project/instance"
|
||||
|
||||
const DEFAULT_READ_LIMIT = 2000
|
||||
const MAX_LINE_LENGTH = 2000
|
||||
@@ -23,8 +23,7 @@ export const ReadTool = Tool.define("read", {
|
||||
if (!path.isAbsolute(filepath)) {
|
||||
filepath = path.join(process.cwd(), filepath)
|
||||
}
|
||||
const app = App.info()
|
||||
if (!ctx.extra?.["bypassCwdCheck"] && !Filesystem.contains(app.path.cwd, filepath)) {
|
||||
if (!ctx.extra?.["bypassCwdCheck"] && !Filesystem.contains(Instance.directory, filepath)) {
|
||||
throw new Error(`File ${filepath} is not in the current working directory`)
|
||||
}
|
||||
|
||||
@@ -77,7 +76,7 @@ export const ReadTool = Tool.define("read", {
|
||||
FileTime.read(ctx.sessionID, filepath)
|
||||
|
||||
return {
|
||||
title: path.relative(App.info().path.root, filepath),
|
||||
title: path.relative(Instance.worktree, filepath),
|
||||
output,
|
||||
metadata: {
|
||||
preview,
|
||||
|
||||
@@ -51,11 +51,13 @@ export const TaskTool = Tool.define("task", async () => {
|
||||
ctx.abort.addEventListener("abort", () => {
|
||||
Session.abort(session.id)
|
||||
})
|
||||
const result = await Session.chat({
|
||||
const result = await Session.prompt({
|
||||
messageID,
|
||||
sessionID: session.id,
|
||||
modelID: model.modelID,
|
||||
providerID: model.providerID,
|
||||
model: {
|
||||
modelID: model.modelID,
|
||||
providerID: model.providerID,
|
||||
},
|
||||
agent: agent.name,
|
||||
tools: {
|
||||
todowrite: false,
|
||||
@@ -75,9 +77,9 @@ export const TaskTool = Tool.define("task", async () => {
|
||||
return {
|
||||
title: params.description,
|
||||
metadata: {
|
||||
summary: result.parts.filter((x) => x.type === "tool"),
|
||||
summary: result.parts.filter((x: any) => x.type === "tool"),
|
||||
},
|
||||
output: result.parts.findLast((x) => x.type === "text")?.text ?? "",
|
||||
output: (result.parts.findLast((x: any) => x.type === "text") as any)?.text ?? "",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod"
|
||||
import { Tool } from "./tool"
|
||||
import DESCRIPTION_WRITE from "./todowrite.txt"
|
||||
import { App } from "../app/app"
|
||||
import { Instance } from "../project/instance"
|
||||
|
||||
const TodoInfo = z.object({
|
||||
content: z.string().describe("Brief description of the task"),
|
||||
@@ -11,12 +11,14 @@ const TodoInfo = z.object({
|
||||
})
|
||||
type TodoInfo = z.infer<typeof TodoInfo>
|
||||
|
||||
const state = App.state("todo-tool", () => {
|
||||
const todos: {
|
||||
[sessionId: string]: TodoInfo[]
|
||||
} = {}
|
||||
return todos
|
||||
})
|
||||
const state = Instance.state(
|
||||
() => {
|
||||
const todos: {
|
||||
[sessionId: string]: TodoInfo[]
|
||||
} = {}
|
||||
return todos
|
||||
},
|
||||
)
|
||||
|
||||
export const TodoWriteTool = Tool.define("todowrite", {
|
||||
description: DESCRIPTION_WRITE,
|
||||
|
||||
@@ -4,11 +4,11 @@ import { Tool } from "./tool"
|
||||
import { LSP } from "../lsp"
|
||||
import { Permission } from "../permission"
|
||||
import DESCRIPTION from "./write.txt"
|
||||
import { App } from "../app/app"
|
||||
import { Bus } from "../bus"
|
||||
import { File } from "../file"
|
||||
import { FileTime } from "../file/time"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Agent } from "../agent/agent"
|
||||
|
||||
export const WriteTool = Tool.define("write", {
|
||||
@@ -18,9 +18,8 @@ export const WriteTool = Tool.define("write", {
|
||||
content: z.string().describe("The content to write to the file"),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
const app = App.info()
|
||||
const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath)
|
||||
if (!Filesystem.contains(app.path.cwd, filepath)) {
|
||||
const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath)
|
||||
if (!Filesystem.contains(Instance.directory, filepath)) {
|
||||
throw new Error(`File ${filepath} is not in the current working directory`)
|
||||
}
|
||||
|
||||
@@ -62,7 +61,7 @@ export const WriteTool = Tool.define("write", {
|
||||
}
|
||||
|
||||
return {
|
||||
title: path.relative(app.path.root, filepath),
|
||||
title: path.relative(Instance.worktree, filepath),
|
||||
metadata: {
|
||||
diagnostics,
|
||||
filepath,
|
||||
|
||||
Reference in New Issue
Block a user