package tools import ( "context" "encoding/json" "fmt" "sort" "agentu/internal/llm" ) const MaxToolOutputBytes = 64 * 1024 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) registry := NewRegistry(fileRead, fileList, fileSearch) registry.RegisterAlias("file.read", fileRead) registry.RegisterAlias("file.list", fileList) registry.RegisterAlias("file.search", fileSearch) 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 { 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 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. - Tool paths should normally be relative to the project working directory. - Available tools: file_list, file_read, file_search, file_write, shell_run.`, workingDir) } 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. - 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. - Tool paths should normally be relative to the project working directory. - Available tools: file_list, file_read, file_search.`, workingDir) } func JSONSchema(schema string) json.RawMessage { return json.RawMessage(schema) } func FormatResult(output string) string { if len(output) <= MaxToolOutputBytes { return output } return output[:MaxToolOutputBytes] + fmt.Sprintf("\n\n[truncated after %d bytes]", MaxToolOutputBytes) } func FormatError(err error) string { return "error: " + err.Error() }