mirror of
https://github.com/sst/opencode.git
synced 2025-08-22 22:14:14 +00:00
Merge 9c247a72e3
into ba2e86c7ef
This commit is contained in:
commit
c69f200452
5 changed files with 453 additions and 88 deletions
11
README.md
11
README.md
|
@ -48,6 +48,17 @@ OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bas
|
|||
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
|
||||
```
|
||||
|
||||
### Morph Fast Apply (Optional)
|
||||
|
||||
For faster, more accurate code edits, opencode can integrate with [Morph](https://morphllm.com) - an AI model specialized for code merging at 4500+ tokens/second with 98.8% accuracy.
|
||||
|
||||
```bash
|
||||
# Enable Morph Fast Apply
|
||||
export MORPH_API_KEY=your_api_key
|
||||
```
|
||||
|
||||
When enabled, opencode's edit tool automatically uses Morph's intelligent code merging instead of search-and-replace, supporting multiple edits and `// ... existing code ...` syntax.
|
||||
|
||||
### Documentation
|
||||
|
||||
For more info on how to configure opencode [**head over to our docs**](https://opencode.ai/docs).
|
||||
|
|
|
@ -9,7 +9,10 @@ import { Tool } from "./tool"
|
|||
import { LSP } from "../lsp"
|
||||
import { createTwoFilesPatch } from "diff"
|
||||
import { Permission } from "../permission"
|
||||
// @ts-ignore
|
||||
import DESCRIPTION from "./edit.txt"
|
||||
// @ts-ignore
|
||||
import MORPH_DESCRIPTION from "./edit-morph.txt"
|
||||
import { App } from "../app/app"
|
||||
import { File } from "../file"
|
||||
import { Bus } from "../bus"
|
||||
|
@ -17,65 +20,165 @@ import { FileTime } from "../file/time"
|
|||
import { Filesystem } from "../util/filesystem"
|
||||
import { Agent } from "../agent/agent"
|
||||
|
||||
export const EditTool = Tool.define("edit", {
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
filePath: z.string().describe("The absolute path to the file to modify"),
|
||||
oldString: z.string().describe("The text to replace"),
|
||||
newString: z.string().describe("The text to replace it with (must be different from oldString)"),
|
||||
replaceAll: z.boolean().optional().describe("Replace all occurrences of oldString (default false)"),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
if (!params.filePath) {
|
||||
throw new Error("filePath is required")
|
||||
// OpenAI-compatible client for Morph API
|
||||
class MorphClient {
|
||||
private apiKey: string
|
||||
private baseURL = "https://api.morphllm.com/v1"
|
||||
|
||||
constructor(apiKey: string) {
|
||||
this.apiKey = apiKey
|
||||
}
|
||||
|
||||
async apply(instruction: string, initialCode: string, codeEdit: string): Promise<string> {
|
||||
const response = await fetch(`${this.baseURL}/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${this.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "morph-v3-large",
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: `<instruction>${instruction}</instruction>\n<code>${initialCode}</code>\n<update>${codeEdit}</update>`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`Morph API error: ${response.status} ${response.statusText}\n${errorText}`)
|
||||
}
|
||||
|
||||
if (params.oldString === params.newString) {
|
||||
throw new Error("oldString and newString must be different")
|
||||
const result = await response.json()
|
||||
return result.choices[0].message.content
|
||||
}
|
||||
}
|
||||
|
||||
async function executeMorphEdit(
|
||||
params: { target_file: string; instructions: string; code_edit: string },
|
||||
ctx: any,
|
||||
morphApiKey: string
|
||||
) {
|
||||
if (!params.target_file) {
|
||||
throw new Error("target_file is required")
|
||||
}
|
||||
|
||||
if (!params.instructions) {
|
||||
throw new Error("instructions is required")
|
||||
}
|
||||
|
||||
if (!params.code_edit) {
|
||||
throw new Error("code_edit is required")
|
||||
}
|
||||
|
||||
const app = App.info()
|
||||
const filePath = path.isAbsolute(params.target_file) ? params.target_file : path.join(app.path.cwd, params.target_file)
|
||||
|
||||
if (!Filesystem.contains(app.path.cwd, filePath)) {
|
||||
throw new Error(`File ${filePath} is not in the current working directory`)
|
||||
}
|
||||
|
||||
const agent = await Agent.get(ctx.agent)
|
||||
|
||||
// Read the existing file
|
||||
const file = Bun.file(filePath)
|
||||
const stats = await file.stat().catch(() => {})
|
||||
if (!stats) throw new Error(`File ${filePath} not found`)
|
||||
if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filePath}`)
|
||||
|
||||
await FileTime.assert(ctx.sessionID, filePath)
|
||||
const initialCode = await file.text()
|
||||
|
||||
// Use Morph API to apply the edit
|
||||
const morphClient = new MorphClient(morphApiKey)
|
||||
let mergedCode: string
|
||||
|
||||
try {
|
||||
mergedCode = await morphClient.apply(params.instructions, initialCode, params.code_edit)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to apply edit with Morph: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
|
||||
const diff = trimDiff(createTwoFilesPatch(filePath, filePath, initialCode, mergedCode))
|
||||
|
||||
// Check permissions if needed
|
||||
if (agent.permission.edit === "ask") {
|
||||
await Permission.ask({
|
||||
type: "edit",
|
||||
sessionID: ctx.sessionID,
|
||||
messageID: ctx.messageID,
|
||||
callID: ctx.callID,
|
||||
title: "🚀 Morph Fast Apply: " + filePath,
|
||||
metadata: {
|
||||
filePath,
|
||||
diff,
|
||||
morphApplied: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Write the merged code to file
|
||||
await Bun.write(filePath, mergedCode)
|
||||
await Bus.publish(File.Event.Edited, {
|
||||
file: filePath,
|
||||
})
|
||||
|
||||
FileTime.read(ctx.sessionID, filePath)
|
||||
|
||||
let output = ""
|
||||
await LSP.touchFile(filePath, true)
|
||||
const diagnostics = await LSP.diagnostics()
|
||||
for (const [file, issues] of Object.entries(diagnostics)) {
|
||||
if (issues.length === 0) continue
|
||||
if (file === filePath) {
|
||||
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`
|
||||
continue
|
||||
}
|
||||
output += `\n<project_diagnostics>\n${file}\n${issues
|
||||
.filter((item) => item.severity === 1)
|
||||
.map(LSP.Diagnostic.pretty)
|
||||
.join("\n")}\n</project_diagnostics>\n`
|
||||
}
|
||||
|
||||
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)) {
|
||||
throw new Error(`File ${filePath} is not in the current working directory`)
|
||||
}
|
||||
return {
|
||||
metadata: {
|
||||
diagnostics,
|
||||
diff,
|
||||
morphApplied: true,
|
||||
},
|
||||
title: `🚀 ${path.relative(app.path.root, filePath)}`,
|
||||
output: output || `Successfully applied edit using Morph Fast Apply:\n\n${diff}`,
|
||||
}
|
||||
}
|
||||
|
||||
const agent = await Agent.get(ctx.agent)
|
||||
let diff = ""
|
||||
let contentOld = ""
|
||||
let contentNew = ""
|
||||
await (async () => {
|
||||
if (params.oldString === "") {
|
||||
contentNew = params.newString
|
||||
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
|
||||
if (agent.permission.edit === "ask") {
|
||||
await Permission.ask({
|
||||
type: "edit",
|
||||
sessionID: ctx.sessionID,
|
||||
messageID: ctx.messageID,
|
||||
callID: ctx.callID,
|
||||
title: "Edit this file: " + filePath,
|
||||
metadata: {
|
||||
filePath,
|
||||
diff,
|
||||
},
|
||||
})
|
||||
}
|
||||
await Bun.write(filePath, params.newString)
|
||||
await Bus.publish(File.Event.Edited, {
|
||||
file: filePath,
|
||||
})
|
||||
return
|
||||
}
|
||||
async function executeRegularEdit(
|
||||
params: { filePath: string; oldString: string; newString: string; replaceAll?: boolean },
|
||||
ctx: any
|
||||
) {
|
||||
if (!params.filePath) {
|
||||
throw new Error("filePath is required")
|
||||
}
|
||||
|
||||
const file = Bun.file(filePath)
|
||||
const stats = await file.stat().catch(() => {})
|
||||
if (!stats) throw new Error(`File ${filePath} not found`)
|
||||
if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filePath}`)
|
||||
await FileTime.assert(ctx.sessionID, filePath)
|
||||
contentOld = await file.text()
|
||||
contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)
|
||||
if (params.oldString === params.newString) {
|
||||
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)) {
|
||||
throw new Error(`File ${filePath} is not in the current working directory`)
|
||||
}
|
||||
|
||||
const agent = await Agent.get(ctx.agent)
|
||||
let diff = ""
|
||||
let contentOld = ""
|
||||
let contentNew = ""
|
||||
await (async () => {
|
||||
if (params.oldString === "") {
|
||||
contentNew = params.newString
|
||||
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
|
||||
if (agent.permission.edit === "ask") {
|
||||
await Permission.ask({
|
||||
|
@ -83,7 +186,6 @@ export const EditTool = Tool.define("edit", {
|
|||
sessionID: ctx.sessionID,
|
||||
messageID: ctx.messageID,
|
||||
callID: ctx.callID,
|
||||
pattern: filePath,
|
||||
title: "Edit this file: " + filePath,
|
||||
metadata: {
|
||||
filePath,
|
||||
|
@ -91,43 +193,105 @@ export const EditTool = Tool.define("edit", {
|
|||
},
|
||||
})
|
||||
}
|
||||
|
||||
await file.write(contentNew)
|
||||
await Bun.write(filePath, params.newString)
|
||||
await Bus.publish(File.Event.Edited, {
|
||||
file: filePath,
|
||||
})
|
||||
contentNew = await file.text()
|
||||
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
|
||||
})()
|
||||
|
||||
FileTime.read(ctx.sessionID, filePath)
|
||||
|
||||
let output = ""
|
||||
await LSP.touchFile(filePath, true)
|
||||
const diagnostics = await LSP.diagnostics()
|
||||
for (const [file, issues] of Object.entries(diagnostics)) {
|
||||
if (issues.length === 0) continue
|
||||
if (file === filePath) {
|
||||
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`
|
||||
continue
|
||||
}
|
||||
output += `\n<project_diagnostics>\n${file}\n${issues
|
||||
// TODO: may want to make more leniant for eslint
|
||||
.filter((item) => item.severity === 1)
|
||||
.map(LSP.Diagnostic.pretty)
|
||||
.join("\n")}\n</project_diagnostics>\n`
|
||||
return
|
||||
}
|
||||
|
||||
const file = Bun.file(filePath)
|
||||
const stats = await file.stat().catch(() => {})
|
||||
if (!stats) throw new Error(`File ${filePath} not found`)
|
||||
if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filePath}`)
|
||||
await FileTime.assert(ctx.sessionID, filePath)
|
||||
contentOld = await file.text()
|
||||
contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)
|
||||
|
||||
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
|
||||
if (agent.permission.edit === "ask") {
|
||||
await Permission.ask({
|
||||
type: "edit",
|
||||
sessionID: ctx.sessionID,
|
||||
messageID: ctx.messageID,
|
||||
callID: ctx.callID,
|
||||
pattern: filePath,
|
||||
title: "Edit this file: " + filePath,
|
||||
metadata: {
|
||||
filePath,
|
||||
diff,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
await file.write(contentNew)
|
||||
await Bus.publish(File.Event.Edited, {
|
||||
file: filePath,
|
||||
})
|
||||
contentNew = await file.text()
|
||||
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
|
||||
})()
|
||||
|
||||
FileTime.read(ctx.sessionID, filePath)
|
||||
|
||||
let output = ""
|
||||
await LSP.touchFile(filePath, true)
|
||||
const diagnostics = await LSP.diagnostics()
|
||||
for (const [file, issues] of Object.entries(diagnostics)) {
|
||||
if (issues.length === 0) continue
|
||||
if (file === filePath) {
|
||||
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`
|
||||
continue
|
||||
}
|
||||
output += `\n<project_diagnostics>\n${file}\n${issues
|
||||
// TODO: may want to make more leniant for eslint
|
||||
.filter((item) => item.severity === 1)
|
||||
.map(LSP.Diagnostic.pretty)
|
||||
.join("\n")}\n</project_diagnostics>\n`
|
||||
}
|
||||
|
||||
return {
|
||||
metadata: {
|
||||
diagnostics,
|
||||
diff,
|
||||
},
|
||||
title: `${path.relative(app.path.root, filePath)}`,
|
||||
output,
|
||||
}
|
||||
}
|
||||
|
||||
export const EditTool = Tool.define("edit", (async () => {
|
||||
const morphApiKey = process.env['MORPH_API_KEY']
|
||||
|
||||
if (morphApiKey) {
|
||||
// Morph Fast Apply mode
|
||||
return {
|
||||
metadata: {
|
||||
diagnostics,
|
||||
diff,
|
||||
},
|
||||
title: `${path.relative(app.path.root, filePath)}`,
|
||||
output,
|
||||
description: MORPH_DESCRIPTION as string,
|
||||
parameters: z.object({
|
||||
target_file: z.string().describe("The target file to modify"),
|
||||
instructions: z.string().describe("A single sentence written in the first person describing what you're changing. Used to help disambiguate uncertainty in the edit."),
|
||||
code_edit: z.string().describe("Specify ONLY the precise lines of code that you wish to edit. Use `// ... existing code ...` for unchanged sections."),
|
||||
}),
|
||||
async execute(params: any, ctx: any) {
|
||||
return executeMorphEdit(params, ctx, morphApiKey)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// Regular search-and-replace mode
|
||||
return {
|
||||
description: DESCRIPTION as string,
|
||||
parameters: z.object({
|
||||
filePath: z.string().describe("The absolute path to the file to modify"),
|
||||
oldString: z.string().describe("The text to replace"),
|
||||
newString: z.string().describe("The text to replace it with (must be different from oldString)"),
|
||||
replaceAll: z.boolean().optional().describe("Replace all occurrences of oldString (default false)"),
|
||||
}),
|
||||
async execute(params: any, ctx: any) {
|
||||
return executeRegularEdit(params, ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}) as any)
|
||||
|
||||
export type Replacer = (content: string, find: string) => Generator<string, void, unknown>
|
||||
|
||||
|
@ -606,7 +770,8 @@ export function replace(content: string, oldString: string, newString: string, r
|
|||
// ContextAwareReplacer,
|
||||
// MultiOccurrenceReplacer,
|
||||
]) {
|
||||
for (const search of replacer(content, oldString)) {
|
||||
const searches = Array.from(replacer(content, oldString))
|
||||
for (const search of searches) {
|
||||
const index = content.indexOf(search)
|
||||
if (index === -1) continue
|
||||
if (replaceAll) {
|
||||
|
|
164
packages/opencode/src/tool/morphedit.ts
Normal file
164
packages/opencode/src/tool/morphedit.ts
Normal file
|
@ -0,0 +1,164 @@
|
|||
import { z } from "zod"
|
||||
import * as path from "path"
|
||||
import { Tool } from "./tool"
|
||||
import { createTwoFilesPatch } from "diff"
|
||||
import { Permission } from "../permission"
|
||||
import DESCRIPTION from "./morphedit.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 { Agent } from "../agent/agent"
|
||||
import { LSP } from "../lsp"
|
||||
|
||||
// OpenAI-compatible client for Morph API
|
||||
class MorphClient {
|
||||
private apiKey: string
|
||||
private baseURL = "https://api.morphllm.com/v1"
|
||||
|
||||
constructor(apiKey: string) {
|
||||
this.apiKey = apiKey
|
||||
}
|
||||
|
||||
async apply(instruction: string, initialCode: string, codeEdit: string): Promise<string> {
|
||||
const response = await fetch(`${this.baseURL}/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${this.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "morph-v3-large",
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: `<instruction>${instruction}</instruction>\n<code>${initialCode}</code>\n<update>${codeEdit}</update>`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`Morph API error: ${response.status} ${response.statusText}\n${errorText}`)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
return result.choices[0].message.content
|
||||
}
|
||||
}
|
||||
|
||||
function trimDiff(diff: string): string {
|
||||
return diff
|
||||
.split("\n")
|
||||
.slice(4) // Remove the header lines
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
export const MorphEditTool = Tool.define("edit", {
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
target_file: z.string().describe("The target file to modify"),
|
||||
instructions: z.string().describe("A single sentence written in the first person describing what you're changing. Used to help disambiguate uncertainty in the edit."),
|
||||
code_edit: z.string().describe("Specify ONLY the precise lines of code that you wish to edit. Use `// ... existing code ...` for unchanged sections."),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
if (!params.target_file) {
|
||||
throw new Error("target_file is required")
|
||||
}
|
||||
|
||||
if (!params.instructions) {
|
||||
throw new Error("instructions is required")
|
||||
}
|
||||
|
||||
if (!params.code_edit) {
|
||||
throw new Error("code_edit is required")
|
||||
}
|
||||
|
||||
// Check for Morph API key
|
||||
const morphApiKey = process.env['MORPH_API_KEY']
|
||||
if (!morphApiKey) {
|
||||
throw new Error("MORPH_API_KEY environment variable is required for morphedit tool")
|
||||
}
|
||||
|
||||
const app = App.info()
|
||||
const filePath = path.isAbsolute(params.target_file) ? params.target_file : path.join(app.path.cwd, params.target_file)
|
||||
|
||||
if (!Filesystem.contains(app.path.cwd, filePath)) {
|
||||
throw new Error(`File ${filePath} is not in the current working directory`)
|
||||
}
|
||||
|
||||
const agent = await Agent.get(ctx.agent)
|
||||
|
||||
// Read the existing file
|
||||
const file = Bun.file(filePath)
|
||||
const stats = await file.stat().catch(() => {})
|
||||
if (!stats) throw new Error(`File ${filePath} not found`)
|
||||
if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filePath}`)
|
||||
|
||||
await FileTime.assert(ctx.sessionID, filePath)
|
||||
const initialCode = await file.text()
|
||||
|
||||
// Use Morph API to apply the edit
|
||||
const morphClient = new MorphClient(morphApiKey)
|
||||
let mergedCode: string
|
||||
|
||||
try {
|
||||
mergedCode = await morphClient.apply(params.instructions, initialCode, params.code_edit)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to apply edit with Morph: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
|
||||
const diff = trimDiff(createTwoFilesPatch(filePath, filePath, initialCode, mergedCode))
|
||||
|
||||
// Check permissions if needed
|
||||
if (agent.permission.edit === "ask") {
|
||||
await Permission.ask({
|
||||
type: "edit",
|
||||
sessionID: ctx.sessionID,
|
||||
messageID: ctx.messageID,
|
||||
callID: ctx.callID,
|
||||
title: "🚀 Morph Fast Apply: " + filePath,
|
||||
metadata: {
|
||||
filePath,
|
||||
diff,
|
||||
morphApplied: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Write the merged code to file
|
||||
await Bun.write(filePath, mergedCode)
|
||||
await Bus.publish(File.Event.Edited, {
|
||||
file: filePath,
|
||||
})
|
||||
|
||||
FileTime.read(ctx.sessionID, filePath)
|
||||
|
||||
let output = ""
|
||||
await LSP.touchFile(filePath, true)
|
||||
const diagnostics = await LSP.diagnostics()
|
||||
for (const [file, issues] of Object.entries(diagnostics)) {
|
||||
if (issues.length === 0) continue
|
||||
if (file === filePath) {
|
||||
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`
|
||||
continue
|
||||
}
|
||||
output += `\n<project_diagnostics>\n${file}\n${issues
|
||||
.filter((item) => item.severity === 1)
|
||||
.map(LSP.Diagnostic.pretty)
|
||||
.join("\n")}\n</project_diagnostics>\n`
|
||||
}
|
||||
|
||||
return {
|
||||
metadata: {
|
||||
diagnostics,
|
||||
diff,
|
||||
morphApplied: true,
|
||||
},
|
||||
title: `🚀 ${path.relative(app.path.root, filePath)}`,
|
||||
output: output || `Successfully applied edit using Morph Fast Apply:\n\n${diff}`,
|
||||
}
|
||||
},
|
||||
})
|
21
packages/opencode/src/tool/morphedit.txt
Normal file
21
packages/opencode/src/tool/morphedit.txt
Normal file
|
@ -0,0 +1,21 @@
|
|||
MORPH FAST APPLY - Use this tool to make an edit to an existing file.
|
||||
|
||||
This will be read by a less intelligent model, which will quickly apply the edit. You should make it clear what the edit is, while also minimizing the unchanged code you write.
|
||||
When writing the edit, you should specify each edit in sequence, with the special comment // ... existing code ... to represent unchanged code in between edited lines.
|
||||
|
||||
For example:
|
||||
|
||||
// ... existing code ...
|
||||
FIRST_EDIT
|
||||
// ... existing code ...
|
||||
SECOND_EDIT
|
||||
// ... existing code ...
|
||||
THIRD_EDIT
|
||||
// ... existing code ...
|
||||
|
||||
You should still bias towards repeating as few lines of the original file as possible to convey the change.
|
||||
But, each edit should contain sufficient context of unchanged lines around the code you're editing to resolve ambiguity.
|
||||
DO NOT omit spans of pre-existing code (or comments) without using the // ... existing code ... comment to indicate its absence. If you omit the existing code comment, the model may inadvertently delete these lines.
|
||||
If you plan on deleting a section, you must provide context before and after to delete it. If the initial code is ```code \n Block 1 \n Block 2 \n Block 3 \n code```, and you want to remove Block 2, you would output ```// ... existing code ... \n Block 1 \n Block 3 \n // ... existing code ...```.
|
||||
Make sure it is clear what the edit should be, and where it should be applied.
|
||||
Make edits to a file in a single edit_file instead of multiple edit_file calls to the same file. The apply model can handle many distinct edits at once.
|
|
@ -1,6 +1,7 @@
|
|||
import z from "zod"
|
||||
import { BashTool } from "./bash"
|
||||
import { EditTool } from "./edit"
|
||||
import { MorphEditTool } from "./morphedit"
|
||||
import { GlobTool } from "./glob"
|
||||
import { GrepTool } from "./grep"
|
||||
import { ListTool } from "./ls"
|
||||
|
@ -14,10 +15,13 @@ import { InvalidTool } from "./invalid"
|
|||
import type { Agent } from "../agent/agent"
|
||||
|
||||
export namespace ToolRegistry {
|
||||
// Use Morph Fast Apply as the default edit tool if MORPH_API_KEY is present
|
||||
const DefaultEditTool = process.env['MORPH_API_KEY'] ? MorphEditTool : EditTool
|
||||
|
||||
const ALL = [
|
||||
InvalidTool,
|
||||
BashTool,
|
||||
EditTool,
|
||||
DefaultEditTool,
|
||||
WebFetchTool,
|
||||
GlobTool,
|
||||
GrepTool,
|
||||
|
@ -45,21 +49,21 @@ export namespace ToolRegistry {
|
|||
if (providerID === "openai") {
|
||||
return result.map((t) => ({
|
||||
...t,
|
||||
parameters: optionalToNullable(t.parameters),
|
||||
parameters: optionalToNullable(t.parameters as z.ZodTypeAny),
|
||||
}))
|
||||
}
|
||||
|
||||
if (providerID === "azure") {
|
||||
return result.map((t) => ({
|
||||
...t,
|
||||
parameters: optionalToNullable(t.parameters),
|
||||
parameters: optionalToNullable(t.parameters as z.ZodTypeAny),
|
||||
}))
|
||||
}
|
||||
|
||||
if (providerID === "google") {
|
||||
return result.map((t) => ({
|
||||
...t,
|
||||
parameters: sanitizeGeminiParameters(t.parameters),
|
||||
parameters: sanitizeGeminiParameters(t.parameters as z.ZodTypeAny),
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue