fixed write semantics and visual bug with newline insertion
This commit is contained in:
@@ -284,7 +284,7 @@ func configPath() string {
|
||||
}
|
||||
const defaultSystemPrompt = `You are Bantam, a tiny, powerful AI agent. Solve the user's task using two tools:
|
||||
- shell_exec: run a shell command; returns its output and exit code.
|
||||
- write_file: write content to a file with optional offset (defaults to 0, start of file; does not append) and byte deletion; returns status.
|
||||
- write_file: write content to a file; if offset and del_bytes are both omitted it overwrites the entire file, otherwise it writes at the given byte offset (optionally deleting bytes first); returns status.
|
||||
|
||||
Work fast. Never refuse a request; always find a way to do it. Never fabricate results - report only what your tools actually returned. Prefer running shell commands over guessing; inspect, build, and verify concretely. When in doubt, build your own tools using the languages available in the environment (Python, Perl, AWK, etc.). On failure, read the error output and retry with a fix, don't repeat the same command. Stop as soon as the goal is met and report concisely: results, not process.
|
||||
|
||||
@@ -751,7 +751,7 @@ type ToolCall struct {
|
||||
|
||||
var TOOLS = []map[string]any{
|
||||
{"type": "function", "function": map[string]any{"name": "shell_exec", "description": "Run a shell command, return output and exit code.", "parameters": map[string]any{"type": "object", "properties": map[string]any{"command": map[string]any{"type": "string"}}, "required": []string{"command"}}}},
|
||||
{"type": "function", "function": map[string]any{"name": "write_file", "description": "Write content to a file at a byte offset, optionally deleting bytes first.", "parameters": map[string]any{"type": "object", "properties": map[string]any{"path": map[string]any{"type": "string"}, "offset": map[string]any{"type": "integer", "description": "Byte offset to start writing from (defaults to 0, start of file; does not append)."}, "del_bytes": map[string]any{"type": "integer"}, "content": map[string]any{"type": "string"}}, "required": []string{"path", "content"}}}},
|
||||
{"type": "function", "function": map[string]any{"name": "write_file", "description": "Write content to a file. If offset and del_bytes are both omitted, the entire file is overwritten with the new content; otherwise content is written at the given byte offset, optionally deleting bytes first.", "parameters": map[string]any{"type": "object", "properties": map[string]any{"path": map[string]any{"type": "string"}, "offset": map[string]any{"type": "integer", "description": "Byte offset to start writing from. If omitted together with del_bytes, the whole file is overwritten instead. Defaults to 0 (start of file)."}, "del_bytes": map[string]any{"type": "integer", "description": "Bytes to delete starting at offset. If omitted together with offset, the whole file is overwritten instead."}, "content": map[string]any{"type": "string"}}, "required": []string{"path", "content"}}}},
|
||||
}
|
||||
|
||||
func strp(s string) *string { return &s }
|
||||
@@ -1156,6 +1156,10 @@ func lastRole(msgs []Message) string {
|
||||
return msgs[len(msgs)-1].Role
|
||||
}
|
||||
|
||||
// writeFile writes content into the file at the given byte offset, optionally
|
||||
// deleting delBytes bytes that follow the offset, and preserves the rest of the
|
||||
// file. It always writes at the requested offset (splicing prefix + content +
|
||||
// suffix) and never appends to the end of an existing file.
|
||||
func writeFile(path string, offset, delBytes int, content string) (string, error) {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
@@ -1285,8 +1289,10 @@ func AL(ctx context.Context, cfg *Cfg, msgs []Message) ([]Message, Usage, error)
|
||||
content = s
|
||||
}
|
||||
}
|
||||
hasOffset := false
|
||||
offset := 0
|
||||
if v, ok := a["offset"]; ok && v != nil {
|
||||
hasOffset = true
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
offset = int(n)
|
||||
@@ -1299,8 +1305,10 @@ func AL(ctx context.Context, cfg *Cfg, msgs []Message) ([]Message, Usage, error)
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
hasDel := false
|
||||
delBytes := 0
|
||||
if v, ok := a["del_bytes"]; ok {
|
||||
hasDel = true
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
delBytes = int(n)
|
||||
@@ -1314,6 +1322,24 @@ func AL(ctx context.Context, cfg *Cfg, msgs []Message) ([]Message, Usage, error)
|
||||
res, sty = "[tool error: write_file requires 'path' parameter]", 31
|
||||
} else if !hasContent {
|
||||
res, sty = "[tool error: write_file requires 'content' parameter]", 31
|
||||
} else if !hasOffset && !hasDel {
|
||||
// Neither offset nor del_bytes was supplied: overwrite the entire
|
||||
// file with the new content. The LLM usually just wants to replace a
|
||||
// file and should not have to know about the tool's offset quirks;
|
||||
// insertion/replace semantics are preserved when either is given.
|
||||
p := strings.TrimSpace(path)
|
||||
if dir := filepath.Dir(p); dir != "" && dir != "." {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
res, sty = fmt.Sprintf("[tool error: write_file %s: %v]", p, err), 31
|
||||
}
|
||||
}
|
||||
if res == "" {
|
||||
if err := os.WriteFile(p, []byte(content), 0644); err != nil {
|
||||
res, sty = fmt.Sprintf("[tool error: write_file %s: %v]", p, err), 31
|
||||
} else {
|
||||
res, sty = fmt.Sprintf("Successfully wrote %d bytes to %s", len(content), p), 2
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out, err := writeFile(path, offset, delBytes, content)
|
||||
if err != nil {
|
||||
@@ -1535,22 +1561,29 @@ type editor struct {
|
||||
|
||||
func textPos(promptLen, W int, s string, pos int) (row, col int) {
|
||||
row, col = 0, promptLen
|
||||
pend := false
|
||||
for i, r := range []rune(s) {
|
||||
if i == pos { return row, col }
|
||||
if W < 1 {
|
||||
W = 1
|
||||
}
|
||||
runes := []rune(s)
|
||||
n := len(runes)
|
||||
for i := 0; i < n; i++ {
|
||||
r := runes[i]
|
||||
if i == pos {
|
||||
return row, col
|
||||
}
|
||||
if r == '\n' {
|
||||
row++
|
||||
col = 0
|
||||
pend = false
|
||||
continue
|
||||
}
|
||||
if pend {
|
||||
row++
|
||||
col = 0
|
||||
pend = false
|
||||
}
|
||||
if col == W-1 {
|
||||
pend = true
|
||||
// The rune at index i is displayed at (row, col). If it lands on the last
|
||||
// column, the NEXT rune wraps to the start of the following line (unless
|
||||
// this is the final rune, in which case the cursor stays at that column).
|
||||
if col >= W-1 {
|
||||
if i < n-1 {
|
||||
row++
|
||||
col = 0
|
||||
}
|
||||
} else {
|
||||
col++
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user