tool set refactor

This commit is contained in:
Luxferre
2026-09-01 09:17:47 +03:00
parent 8991ef099c
commit 0eb05354df
5 changed files with 257 additions and 203 deletions
+97 -37
View File
@@ -27,8 +27,6 @@ import (
"unicode/utf8"
)
const MAX_DEPTH = 5 // subagent recursion depth limit
var (
COL bool // ANSI color enabled
hist []string // line history (mirrors ~/.bantam_history)
@@ -255,9 +253,9 @@ 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.
- run_subagent: delegate a sub-task to a child agent; returns its reply.
- write_file: write content to a file with optional offset and byte deletion; 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. Delegate large or independent sub-tasks to run_subagent. Stop as soon as the goal is met and report concisely: results, not process.
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.
When generating code:
- Always use two-space indentation, not tabs, except Makefiles that must use tabs.
@@ -265,8 +263,7 @@ When generating code:
- Write optimally and with as few third-party dependencies as possible.
- Always test.
- No emojis in code or documentation.
- Respect AGENTS.md, GEMINI.md, CLAUDE.md contents in the project.`
- Respect AGENTS.md contents in the project.`
func toolsDir(cfg *Cfg) string {
if v := strings.TrimSpace(os.Getenv("BANTAM_TOOLS_DIR")); v != "" {
@@ -278,13 +275,6 @@ func toolsDir(cfg *Cfg) string {
return ""
}
func prompt(path string) string {
if d, err := os.ReadFile(path); err == nil {
return strings.TrimSpace(string(d))
}
return defaultSystemPrompt
}
func c(t string, cs ...int) string {
if !COL || len(cs) == 0 { return t }
s := make([]string, len(cs))
@@ -730,7 +720,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": "run_subagent", "description": "Run a child agent with a prompt.", "parameters": map[string]any{"type": "object", "properties": map[string]any{"prompt": map[string]any{"type": "string"}}, "required": []string{"prompt"}}}},
{"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"}, "del_bytes": map[string]any{"type": "integer"}, "content": map[string]any{"type": "string"}}, "required": []string{"path", "content"}}}},
}
func strp(s string) *string { return &s }
@@ -1113,7 +1103,54 @@ func lastRole(msgs []Message) string {
return msgs[len(msgs)-1].Role
}
func AL(ctx context.Context, cfg *Cfg, msgs []Message, sp string, depth int) ([]Message, Usage, error) {
func writeFile(path string, offset, delBytes int, content string) (string, error) {
path = strings.TrimSpace(path)
if path == "" {
return "", errors.New("path is required")
}
if offset < 0 {
offset = 0
}
if delBytes < 0 {
delBytes = 0
}
var data []byte
if fileExists(path) {
var err error
data, err = os.ReadFile(path)
if err != nil {
return "", err
}
} else {
dir := filepath.Dir(path)
if dir != "" && dir != "." {
if err := os.MkdirAll(dir, 0755); err != nil {
return "", err
}
}
}
if offset > len(data) {
padding := make([]byte, offset-len(data))
data = append(data, padding...)
}
prefix := data[:offset]
var suffix []byte
endDel := offset + delBytes
if endDel < len(data) {
suffix = data[endDel:]
}
contentBytes := []byte(content)
newData := make([]byte, 0, len(prefix)+len(contentBytes)+len(suffix))
newData = append(newData, prefix...)
newData = append(newData, contentBytes...)
newData = append(newData, suffix...)
if err := os.WriteFile(path, newData, 0644); err != nil {
return "", err
}
return fmt.Sprintf("Successfully wrote %d bytes to %s", len(contentBytes), path), nil
}
func AL(ctx context.Context, cfg *Cfg, msgs []Message) ([]Message, Usage, error) {
done := false
var turnUsage Usage
for i := 0; i < cfg.MaxALIterations && !done; i++ {
@@ -1184,25 +1221,48 @@ func AL(ctx context.Context, cfg *Cfg, msgs []Message, sp string, depth int) ([]
cmd = filterText(cmd)
res = shell(ctx, cmd, cfg.ShellTimeout)
if err := ctx.Err(); err != nil { return msgs, turnUsage, err }
case "run_subagent":
pr, _ := a["prompt"].(string)
pr = filterText(pr)
if depth >= MAX_DEPTH {
res, sty = fmt.Sprintf("[subagent depth limit (%d) reached, child not spawned]", MAX_DEPTH), 31
} else {
subMsgs := []Message{
{Role: "system", Content: strp(sp + "\n\nImportant: this is a child agent")},
{Role: "user", Content: strp(pr)},
case "write_file":
path, _ := a["path"].(string)
path = filterText(path)
contentVal, hasContent := a["content"]
var content string
if hasContent && contentVal != nil {
if s, ok := contentVal.(string); ok {
content = s
}
if subr, subu, err := AL(ctx, cfg, subMsgs, sp, depth+1); err != nil {
if errors.Is(err, context.Canceled) || ctx.Err() != nil {
return msgs, turnUsage, err
}
res, sty = "[subagent error: "+err.Error()+"]", 31
}
offset := 0
if v, ok := a["offset"]; ok {
switch n := v.(type) {
case float64:
offset = int(n)
case int:
offset = n
case string:
offset = atoiD(n, 0)
}
}
delBytes := 0
if v, ok := a["del_bytes"]; ok {
switch n := v.(type) {
case float64:
delBytes = int(n)
case int:
delBytes = n
case string:
delBytes = atoiD(n, 0)
}
}
if strings.TrimSpace(path) == "" {
res, sty = "[tool error: write_file requires 'path' parameter]", 31
} else if !hasContent {
res, sty = "[tool error: write_file requires 'content' parameter]", 31
} else {
out, err := writeFile(path, offset, delBytes, content)
if err != nil {
res, sty = fmt.Sprintf("[tool error: write_file %s: %v]", path, err), 31
} else {
turnUsage.CompletionTokens += subu.CompletionTokens
turnUsage.TotalTokens += subu.TotalTokens
res, sty = filterText(last(subr)), 2
res, sty = out, 2
}
}
default:
@@ -1594,7 +1654,7 @@ func doCompact(cfg *Cfg, msgs []Message) []Message {
}
func main() {
sp := prompt(".bantamsys.txt")
sp := defaultSystemPrompt
cfg := getCfg(configPath())
cfg.ContextWindow = fetchContextWindow(&cfg)
if td := toolsDir(&cfg); td != "" {
@@ -1618,11 +1678,11 @@ func main() {
msgs = append(msgs, Message{Role: "user", Content: strp(u)})
sigCtx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
var usg Usage
msgs, usg, err = AL(sigCtx, &cfg, msgs, sp, 0)
msgs, usg, err = AL(sigCtx, &cfg, msgs)
if lastRole(msgs) == "tool" {
msgs = append(msgs, Message{Role: "user", Content: strp("continue")})
fmt.Println(c("[auto continue: last message was a tool result]", 33))
msgs, usg, err = AL(sigCtx, &cfg, msgs, sp, 0)
msgs, usg, err = AL(sigCtx, &cfg, msgs)
}
interrupted := sigCtx.Err() != nil
cancel()
@@ -1739,11 +1799,11 @@ func main() {
turnMsgs := append([]Message{}, msgs...)
turnMsgs = append(turnMsgs, Message{Role: "user", Content: strp(u)})
sigCtx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
resMsgs, usg, err := AL(sigCtx, &cfg, turnMsgs, sp, 0)
resMsgs, usg, err := AL(sigCtx, &cfg, turnMsgs)
if lastRole(resMsgs) == "tool" {
resMsgs = append(resMsgs, Message{Role: "user", Content: strp("continue")})
fmt.Println(c("[auto continue: last message was a tool result]", 33))
resMsgs, usg, err = AL(sigCtx, &cfg, resMsgs, sp, 0)
resMsgs, usg, err = AL(sigCtx, &cfg, resMsgs)
}
interrupted := sigCtx.Err() != nil
cancel()