Files
agentu/internal/tools/tools.go
T
loveuer 8dbcb1147f v0.0.3: add built-in web tools
Add no-key web_search and web_fetch tools.
Fetch pages with net/http and parse HTML content.
Document built-in web capabilities.
2026-06-23 09:59:24 +08:00

132 lines
5.2 KiB
Go

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 {
searchInstructions := webSearchInstructions()
readOnlyTools := "file_list, file_read, file_search, web_search, web_fetch"
yoloTools := "file_list, file_read, file_search, 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 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.
- 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 {
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()
}