Files
pulse/internal/ai/tools/names.go
T
rcourtman e36945741e Sanitise plain-JSON tool-call leaks from weak local models
Small Ollama models (qwen2.5:11b, qwen2.5:14b, similar) frequently emit
Pulse tool invocations as plain JSON inside content instead of routing
through the structured tool_calls channel. Users saw raw payloads like
`{"name": "pulse_query", "parameters": {...}}` as the assistant's final
response.

Extend cleanToolCallArtifacts and containsToolCallMarker with a new pass
that detects this leak shape, gated on a closed allowlist of canonical
tool names sourced from the runtime registry. The allowlist auto-syncs
when registerTools() gains a tool, so no separate hand-maintained list.

Anchored on (?:^|\n) and a leading `"name"` key so prose containing JSON
fragments, unrelated objects (`{"foo":"bar"}`), or named resources
(`{"name":"my-vm","cpu":50}`) are left untouched.
2026-05-11 17:02:07 +01:00

47 lines
1.3 KiB
Go

package tools
import "sync"
// KnownToolNames returns the canonical list of registered Pulse tool names,
// keyed off the registry built by registerTools(). The list is built lazily
// on first call via a throwaway executor so adding a new tool to
// registerTools() automatically extends the allowlist — no separate
// hand-maintained list to drift out of sync.
func KnownToolNames() []string {
initKnownToolNames()
out := make([]string, len(knownToolNamesList))
copy(out, knownToolNamesList)
return out
}
// IsKnownToolName reports whether name is one of the canonical Pulse tool
// names. Used by chat-content sanitisers to gate stripping on a closed
// allowlist (e.g. JSON tool-call shapes leaked by weak local models that
// route the call into content instead of through the tool_calls channel).
func IsKnownToolName(name string) bool {
if name == "" {
return false
}
initKnownToolNames()
_, ok := knownToolNamesSet[name]
return ok
}
var (
knownToolNamesOnce sync.Once
knownToolNamesList []string
knownToolNamesSet map[string]struct{}
)
func initKnownToolNames() {
knownToolNamesOnce.Do(func() {
e := NewPulseToolExecutor(ExecutorConfig{})
names := e.registry.allNames()
knownToolNamesList = names
knownToolNamesSet = make(map[string]struct{}, len(names))
for _, n := range names {
knownToolNamesSet[n] = struct{}{}
}
})
}