f235b8ce90
- Move cmd/agentu/main.go → main.go (project root) - Move internal/llm/ → pkg/llm/ (leaf package, no internal deps) - Move internal/config/ → pkg/config/ (leaf package, no internal deps) - Update all import paths: agentu/internal/llm → agentu/pkg/llm (14 files) - Update all import paths: agentu/internal/config → agentu/pkg/config (3 files) - Update README: build/run commands, CLI flags section - agent/session/tools/tui stay in internal/ (app-specific business logic) Build: go build . | Test: go test ./... | Vet: go vet ./...
169 lines
6.4 KiB
Go
169 lines
6.4 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"agentu/pkg/llm"
|
|
)
|
|
|
|
const MaxToolOutputBytes = 64 * 1024
|
|
|
|
const truncationNoticeBudget = 160
|
|
|
|
type Tool interface {
|
|
Definition() llm.Tool
|
|
Execute(ctx context.Context, args json.RawMessage) (string, error)
|
|
}
|
|
|
|
type Registry struct {
|
|
tools map[string]Tool
|
|
primary []string
|
|
}
|
|
|
|
func NewRegistry(items ...Tool) *Registry {
|
|
r := &Registry{tools: make(map[string]Tool, len(items))}
|
|
for _, item := range items {
|
|
r.Register(item)
|
|
}
|
|
return r
|
|
}
|
|
|
|
func (r *Registry) Register(tool Tool) {
|
|
name := tool.Definition().Function.Name
|
|
if _, exists := r.tools[name]; !exists {
|
|
r.primary = append(r.primary, name)
|
|
}
|
|
r.tools[name] = tool
|
|
}
|
|
|
|
func (r *Registry) RegisterAlias(alias string, tool Tool) {
|
|
r.tools[alias] = tool
|
|
}
|
|
|
|
func (r *Registry) Get(name string) (Tool, bool) {
|
|
tool, ok := r.tools[name]
|
|
return tool, ok
|
|
}
|
|
|
|
func (r *Registry) Definitions() []llm.Tool {
|
|
defs := make([]llm.Tool, 0, len(r.primary))
|
|
for _, name := range r.primary {
|
|
tool := r.tools[name]
|
|
defs = append(defs, tool.Definition())
|
|
}
|
|
sort.Slice(defs, func(i, j int) bool {
|
|
return defs[i].Function.Name < defs[j].Function.Name
|
|
})
|
|
return defs
|
|
}
|
|
|
|
func ReadOnlyBuiltins(workingDir string) *Registry {
|
|
fileRead := NewFileReadTool(workingDir)
|
|
fileList := NewFileListTool(workingDir)
|
|
fileSearch := NewFileSearchTool(workingDir)
|
|
codeSymbols := NewCodeSymbolsTool(workingDir)
|
|
|
|
registry := NewRegistry(fileRead, fileList, fileSearch, codeSymbols)
|
|
registry.RegisterAlias("file.read", fileRead)
|
|
registry.RegisterAlias("file.list", fileList)
|
|
registry.RegisterAlias("file.search", fileSearch)
|
|
registry.RegisterAlias("code.symbols", codeSymbols)
|
|
return registry
|
|
}
|
|
|
|
func Builtins(workingDir string) *Registry {
|
|
registry := ReadOnlyBuiltins(workingDir)
|
|
fileWrite := NewFileWriteTool(workingDir)
|
|
shellRun := NewShellRunTool(workingDir)
|
|
registry.Register(fileWrite)
|
|
registry.Register(shellRun)
|
|
registry.RegisterAlias("file.write", fileWrite)
|
|
registry.RegisterAlias("shell.run", shellRun)
|
|
return registry
|
|
}
|
|
|
|
func AgentInstructions(workingDir string, yolo bool) string {
|
|
searchInstructions := webSearchInstructions()
|
|
readOnlyTools := "file_list, file_read, file_search, code_symbols, web_search, web_fetch"
|
|
yoloTools := "file_list, file_read, file_search, code_symbols, file_write, shell_run, web_search, web_fetch"
|
|
if yolo {
|
|
return fmt.Sprintf(`Local project context:
|
|
- The project working directory is %q.
|
|
- Tools are available, but use them only when needed to satisfy the user's request.
|
|
- Do not call tools for greetings, small talk, simple Q&A, or general questions that do not require local project or command context.
|
|
- Do not explore the project preemptively. Do not call file_list just to discover context unless the user asks about project files or the answer truly depends on local files.
|
|
- When the user asks to inspect, list, search, or read project files, use file_list, file_search, and file_read instead of saying you cannot access local files.
|
|
- When the user asks about code structure, symbols, functions, methods, types, or definitions in Go files, use code_symbols before reading whole files.
|
|
- When the user asks to run, execute, test, check, debug, or inspect a shell command, use shell_run. This includes local commands written in backticks, such as go, make, git, pwd, ls, ps, and similar terminal commands.
|
|
- Do not use file tools as a substitute for a command execution request.
|
|
- Do not start interactive or long-running sessions. Prefer bounded, non-interactive local commands with clear completion conditions.
|
|
- %s
|
|
- Tool paths should normally be relative to the project working directory.
|
|
- Available tools: %s.`, workingDir, searchInstructions, yoloTools)
|
|
}
|
|
return fmt.Sprintf(`Local project context:
|
|
- The project working directory is %q.
|
|
- Read-only project file tools are available, but use them only when needed to satisfy the user's request.
|
|
- Do not call tools for greetings, small talk, simple Q&A, or general questions that do not require local project context.
|
|
- Do not explore the project preemptively. Do not call file_list just to discover context unless the user asks about project files or the answer truly depends on local files.
|
|
- When the user asks to inspect, list, search, or read project files, use file_list, file_search, and file_read instead of saying you cannot access local files.
|
|
- When the user asks about code structure, symbols, functions, methods, types, or definitions in Go files, use code_symbols before reading whole files.
|
|
- Shell command execution is disabled in this mode. When the user asks to run, execute, test, check, debug, or inspect a shell command, do not use file tools as a substitute; explain that they need to restart agentu with --yolo to enable shell_run.
|
|
- %s
|
|
- Tool paths should normally be relative to the project working directory.
|
|
- Available tools: %s.`, workingDir, searchInstructions, readOnlyTools)
|
|
}
|
|
|
|
func webSearchInstructions() string {
|
|
return "When the user's request depends on current or external web information, use web_search. This includes news, prices, laws, schedules, software versions, product recommendations, or requests for the latest information. Use web_fetch to read a source URL when search snippets are not enough, when the user provides a URL, or when the user names a specific website to inspect. Base answers on returned sources and include source URLs when useful."
|
|
}
|
|
|
|
func JSONSchema(schema string) json.RawMessage {
|
|
return json.RawMessage(schema)
|
|
}
|
|
|
|
func FormatResult(output string) string {
|
|
return FormatResultLimit(output, MaxToolOutputBytes)
|
|
}
|
|
|
|
func FormatResultLimit(output string, limit int) string {
|
|
if limit <= 0 || len(output) <= limit {
|
|
return output
|
|
}
|
|
if limit <= truncationNoticeBudget {
|
|
return output[:limit]
|
|
}
|
|
notice := fmt.Sprintf("\n\n[truncated: showing head and tail; omitted %d bytes]\n\n", len(output)-limit)
|
|
keep := limit - len(notice)
|
|
if keep <= 0 {
|
|
return output[:limit]
|
|
}
|
|
headLen := keep / 2
|
|
tailLen := keep - headLen
|
|
head := trimHeadAtLine(output[:headLen])
|
|
tail := trimTailAtLine(output[len(output)-tailLen:])
|
|
return head + notice + tail
|
|
}
|
|
|
|
func trimHeadAtLine(value string) string {
|
|
if idx := strings.LastIndexByte(value, '\n'); idx > 0 {
|
|
return value[:idx+1]
|
|
}
|
|
return value
|
|
}
|
|
|
|
func trimTailAtLine(value string) string {
|
|
if idx := strings.IndexByte(value, '\n'); idx >= 0 && idx+1 < len(value) {
|
|
return value[idx+1:]
|
|
}
|
|
return value
|
|
}
|
|
|
|
func FormatError(err error) string {
|
|
return "error: " + err.Error()
|
|
}
|