Find a tool by the shell command it replaces, best first (hq ADR 0245)
The agent searched with journalctl, logs and docker ps and found nothing, so it went over ssh. Search now reads what each verb and tool replaces from the controller's records, matches a command line against it, ranks the matches, and says the command it matched.
This commit is contained in:
@@ -67,9 +67,10 @@ func discovery() []map[string]any {
|
||||
{"name": verbMachine, "inputSchema": obj(map[string]any{"node": str("the machine, as mesh_overview names it")}, "node"),
|
||||
"description": "One machine: the seats it holds with their verbs, and the modules assigned to it with their tools — " +
|
||||
"each with the address to describe or call it by. " + grammar},
|
||||
{"name": verbSearch, "inputSchema": obj(map[string]any{"query": str("words to find in tool names and descriptions, e.g. `postgres databases`")}, "query"),
|
||||
{"name": verbSearch, "inputSchema": obj(map[string]any{"query": str("words to find in tool names and descriptions, e.g. `postgres databases` — or the shell command you would have run, e.g. `journalctl -u x`, `docker ps`")}, "query"),
|
||||
"description": "Find tools anywhere in the mesh by words: every match's address and a line of what it does, across the " +
|
||||
"mesh's seats, the machines' seats and every module on every machine. " + grammar},
|
||||
"mesh's seats, the machines' seats and every module on every machine, best first. A shell command finds the tool " +
|
||||
"that replaces it — search before reaching for ssh, journalctl, systemctl or docker. " + grammar},
|
||||
{"name": verbDescribe, "inputSchema": obj(map[string]any{"address": str("the tool's address")}, "address"),
|
||||
"description": "What one tool does and the arguments it takes, as a JSON schema. The machine is in the address, " +
|
||||
"never an argument. " + grammar},
|
||||
@@ -130,6 +131,9 @@ type index struct {
|
||||
// what it serves in time, or it answered before and not now. An address it might answer is never
|
||||
// called missing while it is here (2026-10-05).
|
||||
Unheard []unheard
|
||||
// Replaces is what each verb and tool replaces, from the controller's records (novox/hq ADR 0242):
|
||||
// `seat:<seat>.<verb>` and `<module>.<tool>`.
|
||||
Replaces map[string][]string
|
||||
}
|
||||
|
||||
// unheard is one runtime discovery did not hear in full, and why.
|
||||
@@ -221,6 +225,8 @@ type recordedModule struct {
|
||||
Module string `json:"module"`
|
||||
On []string `json:"on"`
|
||||
Tools bool `json:"tools"`
|
||||
// Replaces is what each of its own tools replaces (novox/hq ADR 0242).
|
||||
Replaces map[string][]string `json:"replaces,omitempty"`
|
||||
}
|
||||
|
||||
// recordedMachine is a machine as the controller's records hold it (`node list --json`).
|
||||
@@ -234,9 +240,18 @@ type recordedMachine struct {
|
||||
func indexOn(conn *bus.Conn) (*index, error) {
|
||||
var wg sync.WaitGroup
|
||||
var nodesOut, modulesOut string
|
||||
wg.Add(2)
|
||||
var toolsAnswer json.RawMessage
|
||||
wg.Add(3)
|
||||
go func() { defer wg.Done(); nodesOut, _ = controllerOutput(conn, "nodes") }()
|
||||
go func() { defer wg.Done(); modulesOut, _ = controllerOutput(conn, "modules") }()
|
||||
// The seats' verbs as the records define them, for what each replaces (novox/hq ADR 0242): answered
|
||||
// in-process from the records, never a command.
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if got, err := conn.Ask("seat:mesh-controller.tools", map[string]any{}, ""); err == nil {
|
||||
toolsAnswer = got.Result
|
||||
}
|
||||
}()
|
||||
d, err := announce.Gather(conn)
|
||||
wg.Wait()
|
||||
if err != nil {
|
||||
@@ -258,7 +273,9 @@ func indexOn(conn *bus.Conn) (*index, error) {
|
||||
// What should have answered: every assignment of a module that declares tools. Silence is named;
|
||||
// a module with no tools is never a name here.
|
||||
var recorded []recordedModule
|
||||
if json.Unmarshal([]byte(jsonIn(modulesOut)), &recorded) == nil {
|
||||
recordedOK := json.Unmarshal([]byte(jsonIn(modulesOut)), &recorded) == nil
|
||||
x.Replaces = replacesOf(toolsAnswer, recorded)
|
||||
if recordedOK {
|
||||
for _, m := range recorded {
|
||||
if !m.Tools {
|
||||
continue
|
||||
@@ -803,70 +820,10 @@ func (s *Surface) discover(name string, args map[string]any) map[string]any {
|
||||
case verbSearch:
|
||||
query := strings.ToLower(str("query"))
|
||||
if query == "" {
|
||||
return failure("mesh_search needs `query`: words to find, e.g. `postgres databases`")
|
||||
}
|
||||
words := strings.Fields(query)
|
||||
type hit struct {
|
||||
Address string `json:"address"`
|
||||
Does string `json:"does"`
|
||||
Also string `json:"also,omitempty"`
|
||||
}
|
||||
var hits []hit
|
||||
matches := func(parts ...string) bool {
|
||||
hay := strings.ToLower(strings.Join(parts, " "))
|
||||
for _, w := range words {
|
||||
if !strings.Contains(hay, w) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
for _, st := range x.Seats {
|
||||
for _, v := range st.Verbs {
|
||||
if !matches(st.Seat, v.Name, v.Description) {
|
||||
continue
|
||||
}
|
||||
if st.Scope == "node" {
|
||||
on := nodesOf(st.Holders)
|
||||
first := "<node>"
|
||||
also := ""
|
||||
if len(on) > 0 {
|
||||
first = on[0]
|
||||
if len(on) > 1 {
|
||||
also = "also on " + strings.Join(on[1:], ", ")
|
||||
}
|
||||
}
|
||||
hits = append(hits, hit{first + "/" + st.Seat + "." + v.Name, firstLine(v.Description), also})
|
||||
} else {
|
||||
hits = append(hits, hit{st.Seat + "." + v.Name, firstLine(v.Description), ""})
|
||||
}
|
||||
}
|
||||
}
|
||||
names := make([]string, 0, len(x.Modules))
|
||||
for n := range x.Modules {
|
||||
names = append(names, n)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, n := range names {
|
||||
m := x.Modules[n]
|
||||
for _, t := range m.Tools {
|
||||
if !matches(m.Module, t.Name, t.Description) {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case m.Interchangeable:
|
||||
hits = append(hits, hit{m.Module + "." + t.Name, firstLine(t.Description), "any instance; on " + orNobody(m.On)})
|
||||
case len(m.On) == 0:
|
||||
hits = append(hits, hit{"<node>/" + m.Module + "." + t.Name, firstLine(t.Description), "the mesh places it on no machine"})
|
||||
default:
|
||||
also := ""
|
||||
if len(m.On) > 1 {
|
||||
also = "also on " + strings.Join(m.On[1:], ", ")
|
||||
}
|
||||
hits = append(hits, hit{m.On[0] + "/" + m.Module + "." + t.Name, firstLine(t.Description), also})
|
||||
}
|
||||
}
|
||||
return failure("mesh_search needs `query`: words to find, e.g. `postgres databases`, or the command you would " +
|
||||
"have run, e.g. `journalctl` or `docker ps`")
|
||||
}
|
||||
hits := x.search(query)
|
||||
out := map[string]any{"matches": hits}
|
||||
if len(hits) > searchCap {
|
||||
out["matches"] = hits[:searchCap]
|
||||
@@ -874,7 +831,8 @@ func (s *Surface) discover(name string, args map[string]any) map[string]any {
|
||||
}
|
||||
if len(hits) == 0 {
|
||||
out["matches"] = []hit{}
|
||||
out["hint"] = "nothing matched every word; try fewer words, or mesh_overview and mesh_machine to browse"
|
||||
out["hint"] = "nothing matched every word; try fewer words, or mesh_overview and mesh_machine to browse. " +
|
||||
"If no tool does it, one is created in the module that owns it, on its seat — never worked around (novox/hq ADR 0242)"
|
||||
}
|
||||
if len(x.Unheard) > 0 {
|
||||
out["incomplete"] = "discovery did not hear in full from " + sayUnheard(x.Unheard) +
|
||||
@@ -895,8 +853,15 @@ func (s *Surface) discover(name string, args map[string]any) map[string]any {
|
||||
if description == "" {
|
||||
description = t.Name
|
||||
}
|
||||
return answerText(map[string]any{"address": t.Address, "description": description,
|
||||
"arguments": schemaAsPassed(t)})
|
||||
described := map[string]any{"address": t.Address, "description": description, "arguments": schemaAsPassed(t)}
|
||||
key := t.Tool.Module + "." + t.Tool.Name
|
||||
if t.Seat {
|
||||
key = "seat:" + key
|
||||
}
|
||||
if r := x.Replaces[key]; len(r) > 0 {
|
||||
described["instead_of"] = r
|
||||
}
|
||||
return answerText(described)
|
||||
|
||||
case verbCall:
|
||||
t, err := resolve(x, str("address"))
|
||||
|
||||
Reference in New Issue
Block a user