claudetool: add simplified patch support
For weaker models.
Also, improve fallback parsing introduced earlier.
diff --git a/claudetool/patch.go b/claudetool/patch.go
index bd01fa8..f398ac6 100644
--- a/claudetool/patch.go
+++ b/claudetool/patch.go
@@ -31,7 +31,11 @@
Callback PatchCallback // may be nil
// Pwd is the working directory for resolving relative paths
Pwd string
+ // Simplified indicates whether to use the simplified input schema.
+ // Helpful for weaker models.
+ Simplified bool
// ClipboardEnabled controls whether clipboard functionality is enabled.
+ // Ignored if Simplified is true.
// NB: The actual implementation of the patch tool is unchanged,
// this flag merely extends the description and input schema to include the clipboard operations.
ClipboardEnabled bool
@@ -43,7 +47,10 @@
func (p *PatchTool) Tool() *llm.Tool {
description := PatchBaseDescription + PatchUsageNotes
schema := PatchStandardInputSchema
- if p.ClipboardEnabled {
+ switch {
+ case p.Simplified:
+ schema = PatchStandardSimplifiedSchema
+ case p.ClipboardEnabled:
description = PatchBaseDescription + PatchClipboardDescription + PatchUsageNotes
schema = PatchClipboardInputSchema
}
@@ -130,6 +137,36 @@
}
`
+ PatchStandardSimplifiedSchema = `{
+ "type": "object",
+ "required": ["path", "patch"],
+ "properties": {
+ "path": {
+ "type": "string",
+ "description": "Path to the file to patch"
+ },
+ "patch": {
+ "type": "object",
+ "required": ["operation", "newText"],
+ "properties": {
+ "operation": {
+ "type": "string",
+ "enum": ["replace", "append_eof", "prepend_bof", "overwrite"],
+ "description": "Type of operation to perform"
+ },
+ "oldText": {
+ "type": "string",
+ "description": "Text to locate for the operation (must be unique in file, required for replace)"
+ },
+ "newText": {
+ "type": "string",
+ "description": "The new text to use (empty for deletions)"
+ }
+ }
+ }
+ }
+}`
+
PatchClipboardInputSchema = `
{
"type": "object",
@@ -199,8 +236,14 @@
// PatchInputOne is a simplified version of PatchInput for single patch operations.
type PatchInputOne struct {
- Path string `json:"path"`
- Patches PatchRequest `json:"patches"`
+ Path string `json:"path"`
+ Patches *PatchRequest `json:"patches"`
+}
+
+// PatchInputOneSingular is PatchInputOne with a better name for the singular case.
+type PatchInputOneSingular struct {
+ Path string `json:"path"`
+ Patch *PatchRequest `json:"patch"`
}
type PatchInputOneString struct {
@@ -253,17 +296,21 @@
func (p *PatchTool) patchParse(m json.RawMessage) (PatchInput, error) {
var input PatchInput
originalErr := json.Unmarshal(m, &input)
- if originalErr == nil {
+ if originalErr == nil && len(input.Patches) > 0 {
return input, nil
}
var inputOne PatchInputOne
- if err := json.Unmarshal(m, &inputOne); err == nil {
- return PatchInput{Path: inputOne.Path, Patches: []PatchRequest{inputOne.Patches}}, nil
+ if err := json.Unmarshal(m, &inputOne); err == nil && inputOne.Patches != nil {
+ return PatchInput{Path: inputOne.Path, Patches: []PatchRequest{*inputOne.Patches}}, nil
+ }
+ var inputOneSingular PatchInputOneSingular
+ if err := json.Unmarshal(m, &inputOneSingular); err == nil && inputOneSingular.Patch != nil {
+ return PatchInput{Path: inputOneSingular.Path, Patches: []PatchRequest{*inputOneSingular.Patch}}, nil
}
var inputOneString PatchInputOneString
if err := json.Unmarshal(m, &inputOneString); err == nil {
var onePatch PatchRequest
- if err := json.Unmarshal([]byte(inputOneString.Patches), &onePatch); err == nil {
+ if err := json.Unmarshal([]byte(inputOneString.Patches), &onePatch); err == nil && onePatch.Operation != "" {
return PatchInput{Path: inputOneString.Path, Patches: []PatchRequest{onePatch}}, nil
}
var patches []PatchRequest
diff --git a/claudetool/patch_test.go b/claudetool/patch_test.go
index 93bbe1c..6a8d090 100644
--- a/claudetool/patch_test.go
+++ b/claudetool/patch_test.go
@@ -320,7 +320,7 @@
// Test single patch format (PatchInputOne)
inputOne := PatchInputOne{
Path: testFile,
- Patches: PatchRequest{
+ Patches: &PatchRequest{
Operation: "overwrite",
NewText: "Single patch format\n",
},
diff --git a/llm/ant/ant.go b/llm/ant/ant.go
index 046456f..7be2911 100644
--- a/llm/ant/ant.go
+++ b/llm/ant/ant.go
@@ -565,3 +565,8 @@
}
}
}
+
+// For debugging only, Claude can definitely handle the full patch tool.
+// func (s *Service) UseSimplifiedPatch() bool {
+// return true
+// }
diff --git a/llm/llm.go b/llm/llm.go
index ffaad3e..19a1d8d 100644
--- a/llm/llm.go
+++ b/llm/llm.go
@@ -21,6 +21,18 @@
TokenContextWindow() int
}
+type SimplifiedPatcher interface {
+ // UseSimplifiedPatch reports whether the service should use the simplified patch input schema.
+ UseSimplifiedPatch() bool
+}
+
+func UseSimplifiedPatch(svc Service) bool {
+ if sp, ok := svc.(SimplifiedPatcher); ok {
+ return sp.UseSimplifiedPatch()
+ }
+ return false
+}
+
// MustSchema validates that schema is a valid JSON schema and returns it as a json.RawMessage.
// It panics if the schema is invalid.
// The schema must have at least type="object" and a properties key.
diff --git a/llm/oai/oai.go b/llm/oai/oai.go
index c561095..6a32a74 100644
--- a/llm/oai/oai.go
+++ b/llm/oai/oai.go
@@ -37,11 +37,12 @@
)
type Model struct {
- UserName string // provided by the user to identify this model (e.g. "gpt4.1")
- ModelName string // provided to the service provide to specify which model to use (e.g. "gpt-4.1-2025-04-14")
- URL string
- APIKeyEnv string // environment variable name for the API key
- IsReasoningModel bool // whether this model is a reasoning model (e.g. O3, O4-mini)
+ UserName string // provided by the user to identify this model (e.g. "gpt4.1")
+ ModelName string // provided to the service provide to specify which model to use (e.g. "gpt-4.1-2025-04-14")
+ URL string
+ APIKeyEnv string // environment variable name for the API key
+ IsReasoningModel bool // whether this model is a reasoning model (e.g. O3, O4-mini)
+ UseSimplifiedPatch bool // whether to use the simplified patch input schema; defaults to false
}
var (
@@ -210,17 +211,19 @@
}
Qwen3CoderFireworks = Model{
- UserName: "qwen3-coder-fireworks",
- ModelName: "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct",
- URL: FireworksURL,
- APIKeyEnv: FireworksAPIKeyEnv,
+ UserName: "qwen3-coder-fireworks",
+ ModelName: "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct",
+ URL: FireworksURL,
+ APIKeyEnv: FireworksAPIKeyEnv,
+ UseSimplifiedPatch: true,
}
// Qwen is a skaband-specific model name for Qwen3-Coder
// Provider details (URL and APIKeyEnv) are handled by skaband
Qwen = Model{
- UserName: "qwen",
- ModelName: "qwen", // skaband will map this to the actual provider model
+ UserName: "qwen",
+ ModelName: "qwen", // skaband will map this to the actual provider model
+ UseSimplifiedPatch: true,
}
)
@@ -783,3 +786,7 @@
}
}
}
+
+func (s *Service) UseSimplifiedPatch() bool {
+ return s.Model.UseSimplifiedPatch
+}
diff --git a/loop/agent.go b/loop/agent.go
index 310564d..85a4afe 100644
--- a/loop/agent.go
+++ b/loop/agent.go
@@ -1394,6 +1394,7 @@
patchTool := &claudetool.PatchTool{
Callback: a.patchCallback,
Pwd: a.workingDir,
+ Simplified: llm.UseSimplifiedPatch(a.config.Service),
ClipboardEnabled: experiment.Enabled("clipboard"),
}