provider/openai: - Fix doubled tool call args (argsComplete flag): Ollama sends complete args in the first streaming chunk then repeats them as delta, causing doubled JSON and 400 errors in elfs - Handle fs: prefix (gemma4 uses fs:grep instead of fs.grep) - Add Reasoning field support for Ollama thinking output cmd/gnoma: - Early TTY detection so logger is created with correct destination before any component gets a reference to it (fixes slog WARN bleed into TUI textarea) permission: - Exempt spawn_elfs and agent tools from safety scanner: elf prompt text may legitimately mention .env/.ssh/credentials patterns and should not be blocked tui/app: - /init retry chain: no-tool-calls → spawn_elfs nudge → write nudge (ask for plain text output) → TUI fallback write from streamBuf - looksLikeAgentsMD + extractMarkdownDoc: validate and clean fallback content before writing (reject refusals, strip narrative preambles) - Collapse thinking output to 3 lines; ctrl+o to expand (live stream and committed messages) - Stream-level filter for model pseudo-tool-call blocks: suppresses <<tool_code>>...</tool_code>> and <<function_call>>...<tool_call|> from entering streamBuf across chunk boundaries - sanitizeAssistantText regex covers both block formats - Reset streamFilterClose at every turn start
81 lines
1.8 KiB
Go
81 lines
1.8 KiB
Go
package tool
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"sync"
|
|
)
|
|
|
|
// Definition is the provider-agnostic tool schema sent to the LLM.
|
|
type Definition struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Parameters json.RawMessage `json:"parameters"`
|
|
}
|
|
|
|
// Registry holds all available tools.
|
|
type Registry struct {
|
|
mu sync.RWMutex
|
|
tools map[string]Tool
|
|
}
|
|
|
|
func NewRegistry() *Registry {
|
|
return &Registry{
|
|
tools: make(map[string]Tool),
|
|
}
|
|
}
|
|
|
|
// Register adds a tool. Overwrites if name already exists.
|
|
func (r *Registry) Register(t Tool) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.tools[t.Name()] = t
|
|
}
|
|
|
|
// Get returns a tool by name.
|
|
func (r *Registry) Get(name string) (Tool, bool) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
t, ok := r.tools[name]
|
|
return t, ok
|
|
}
|
|
|
|
// All returns all registered tools sorted by name for deterministic ordering.
|
|
func (r *Registry) All() []Tool {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
all := make([]Tool, 0, len(r.tools))
|
|
for _, t := range r.tools {
|
|
all = append(all, t)
|
|
}
|
|
sort.Slice(all, func(i, j int) bool { return all[i].Name() < all[j].Name() })
|
|
return all
|
|
}
|
|
|
|
// Definitions returns tool definitions for all registered tools sorted by name,
|
|
// suitable for sending to the LLM.
|
|
func (r *Registry) Definitions() []Definition {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
defs := make([]Definition, 0, len(r.tools))
|
|
for _, t := range r.tools {
|
|
defs = append(defs, Definition{
|
|
Name: t.Name(),
|
|
Description: t.Description(),
|
|
Parameters: t.Parameters(),
|
|
})
|
|
}
|
|
sort.Slice(defs, func(i, j int) bool { return defs[i].Name < defs[j].Name })
|
|
return defs
|
|
}
|
|
|
|
// MustGet returns a tool by name or panics. For use in tests.
|
|
func (r *Registry) MustGet(name string) Tool {
|
|
t, ok := r.Get(name)
|
|
if !ok {
|
|
panic(fmt.Sprintf("tool not found: %q", name))
|
|
}
|
|
return t
|
|
}
|