Files
gnoma/internal/tool/registry.go
vikingowl cb2d63d06f feat: Ollama/gemma4 compat — /init flow, stream filter, safety fixes
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
2026-04-05 19:24:51 +02:00

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
}