toolset.go

  1package claudetool
  2
  3import (
  4	"context"
  5	"strings"
  6	"sync"
  7
  8	"shelley.exe.dev/claudetool/browse"
  9	"shelley.exe.dev/llm"
 10)
 11
 12// WorkingDir is a thread-safe mutable working directory.
 13type MutableWorkingDir struct {
 14	mu  sync.RWMutex
 15	dir string
 16}
 17
 18// NewMutableWorkingDir creates a new MutableWorkingDir with the given initial directory.
 19func NewMutableWorkingDir(dir string) *MutableWorkingDir {
 20	return &MutableWorkingDir{dir: dir}
 21}
 22
 23// Get returns the current working directory.
 24func (w *MutableWorkingDir) Get() string {
 25	w.mu.RLock()
 26	defer w.mu.RUnlock()
 27	return w.dir
 28}
 29
 30// Set updates the working directory.
 31func (w *MutableWorkingDir) Set(dir string) {
 32	w.mu.Lock()
 33	defer w.mu.Unlock()
 34	w.dir = dir
 35}
 36
 37// ToolSetConfig contains configuration for creating a ToolSet.
 38type ToolSetConfig struct {
 39	// WorkingDir is the initial working directory for tools.
 40	WorkingDir string
 41	// LLMProvider provides access to LLM services for tool validation.
 42	LLMProvider LLMServiceProvider
 43	// EnableJITInstall enables just-in-time tool installation.
 44	EnableJITInstall bool
 45	// EnableBrowser enables browser tools.
 46	EnableBrowser bool
 47	// ModelID is the model being used for this conversation.
 48	// Used to determine tool configuration (e.g., simplified patch schema for weaker models).
 49	ModelID string
 50	// OnWorkingDirChange is called when the working directory changes.
 51	// This can be used to persist the change to a database.
 52	OnWorkingDirChange func(newDir string)
 53}
 54
 55// ToolSet holds a set of tools for a single conversation.
 56// Each conversation should have its own ToolSet.
 57type ToolSet struct {
 58	tools   []*llm.Tool
 59	cleanup func()
 60	wd      *MutableWorkingDir
 61}
 62
 63// Tools returns the tools in this set.
 64func (ts *ToolSet) Tools() []*llm.Tool {
 65	return ts.tools
 66}
 67
 68// Cleanup releases resources held by the tools (e.g., browser).
 69func (ts *ToolSet) Cleanup() {
 70	if ts.cleanup != nil {
 71		ts.cleanup()
 72	}
 73}
 74
 75// WorkingDir returns the shared working directory.
 76func (ts *ToolSet) WorkingDir() *MutableWorkingDir {
 77	return ts.wd
 78}
 79
 80// NewToolSet creates a new set of tools for a conversation.
 81// isStrongModel returns true for models that can handle complex tool schemas.
 82func isStrongModel(modelID string) bool {
 83	lower := strings.ToLower(modelID)
 84	return strings.Contains(lower, "sonnet") || strings.Contains(lower, "opus")
 85}
 86
 87func NewToolSet(ctx context.Context, cfg ToolSetConfig) *ToolSet {
 88	workingDir := cfg.WorkingDir
 89	if workingDir == "" {
 90		workingDir = "/"
 91	}
 92	wd := NewMutableWorkingDir(workingDir)
 93
 94	bashTool := &BashTool{
 95		WorkingDir:       wd,
 96		LLMProvider:      cfg.LLMProvider,
 97		EnableJITInstall: cfg.EnableJITInstall,
 98	}
 99
100	// Use simplified patch schema for weaker models, full schema for sonnet/opus
101	simplified := !isStrongModel(cfg.ModelID)
102	patchTool := &PatchTool{
103		Simplified:       simplified,
104		WorkingDir:       wd,
105		ClipboardEnabled: true,
106	}
107
108	keywordTool := NewKeywordToolWithWorkingDir(cfg.LLMProvider, wd)
109
110	changeDirTool := &ChangeDirTool{
111		WorkingDir: wd,
112		OnChange:   cfg.OnWorkingDirChange,
113	}
114
115	tools := []*llm.Tool{
116		Think,
117		bashTool.Tool(),
118		patchTool.Tool(),
119		keywordTool.Tool(),
120		changeDirTool.Tool(),
121	}
122
123	var cleanup func()
124	if cfg.EnableBrowser {
125		// Get max image dimension from the LLM service
126		maxImageDimension := 0
127		if cfg.LLMProvider != nil && cfg.ModelID != "" {
128			if svc, err := cfg.LLMProvider.GetService(cfg.ModelID); err == nil {
129				maxImageDimension = svc.MaxImageDimension()
130			}
131		}
132		browserTools, browserCleanup := browse.RegisterBrowserTools(ctx, true, maxImageDimension)
133		if len(browserTools) > 0 {
134			tools = append(tools, browserTools...)
135		}
136		cleanup = browserCleanup
137	}
138
139	return &ToolSet{
140		tools:   tools,
141		cleanup: cleanup,
142		wd:      wd,
143	}
144}