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) } // MutatingTool is optionally implemented by tools that modify external state. // It is used by the agent to determine whether tool calls can run in parallel. type MutatingTool interface { Mutates() bool } // IsMutating reports whether tool implements MutatingTool and returns true. func IsMutating(tool Tool) bool { mutating, ok := tool.(MutatingTool) return ok && mutating.Mutates() } 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) gitStatus := NewGitStatusTool(workingDir) gitDiff := NewGitDiffTool(workingDir) gitLog := NewGitLogTool(workingDir) registry := NewRegistry(fileRead, fileList, fileSearch, codeSymbols, gitStatus, gitDiff, gitLog) registry.RegisterAlias("file.read", fileRead) registry.RegisterAlias("file.list", fileList) registry.RegisterAlias("file.search", fileSearch) registry.RegisterAlias("code.symbols", codeSymbols) registry.RegisterAlias("git.status", gitStatus) registry.RegisterAlias("git.diff", gitDiff) registry.RegisterAlias("git.log", gitLog) return registry } func Builtins(workingDir string) *Registry { registry := ReadOnlyBuiltins(workingDir) fileWrite := NewFileWriteTool(workingDir) fileEdit := NewFileEditTool(workingDir) shellRun := NewShellRunTool(workingDir) registry.Register(fileWrite) registry.Register(fileEdit) registry.Register(shellRun) registry.RegisterAlias("file.write", fileWrite) registry.RegisterAlias("file.edit", fileEdit) 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, git_status, git_diff, git_log, web_search, web_fetch" yoloTools := "file_list, file_read, file_search, code_symbols, file_write, file_edit, shell_run, git_status, git_diff, git_log, 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. - When editing existing files, prefer file_edit for small or localized changes; use file_write only for creating files, replacing whole files intentionally, or appending. - Do not use file tools as a substitute for a command execution request. - When the user asks about changed files, diffs, or git history, use git_status, git_diff, or git_log instead of shell_run. - 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. - When the user asks about changed files, diffs, or git history, use git_status, git_diff, or git_log instead of 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() }