Files
LocalAI/core/application/agent_control_wiring.go
T
Ettore Di Giacinto e3040c7e3c feat(distributed): make MCP execution and discovery a selection
mcp.tools.execute and mcp.discovery were the only NATS subjects that
combined a queue group with a reply, and no carrier in this design
provides both. They never needed one: a queue group is a way of choosing
a subscriber, and choosing is a query.

The frontend now lists the approved, non-draining agent nodes, asks the
node_connections table in one joined statement which of those tunnels a
live replica holds, prefers one this replica holds so the call skips the
relay hop, and issues an ordinary control RPC on the path task 4 already
mounted. A peer-held tunnel is reached through the relay. That is a
choice a broker's hidden balancing could not make.

The selection reads presence and nothing else. It is filtered only on
node type and on the two statuses an operator controls, never on a health
verdict written on another clock, because refusing a worker that is
connected and answering is the same defect as picking one that is gone.
An empty fleet answers ErrNoAgentWorker, which is deliberately neither
ErrWorkerUnroutable nor anything cluster.IsWorkerAnswer accepts: nothing
was asked of any worker, so no reap guard may act on it.

A reply carrying an Error is the worker's own answer and is returned
unchanged; it is never offered to a second worker, which would turn "this
MCP server rejected your arguments" into "the fleet is broken" and could
run a tool twice. A call that never reached a worker is retried against a
different pick, at most three times, and whatever error is finally
returned is returned unwrapped so its identity survives the loop.

MCP prompts and resources now answer 501 in distributed mode instead of
an empty 200. They are served only from sessions the frontend holds, and
in distributed mode it holds none. That gap predates the removal of the
bus and is not closed by it; this only stops it being silent.

Agent workers keep every other subject, including nodes.<id>.backend.stop.
Their minted JWT loses the two MCP subjects and keeps a non-empty allow
list, because NATS reads an empty one as no restriction at all.

Assisted-by: Claude Opus 5 [claude-code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-27 03:05:12 +00:00

42 lines
2.0 KiB
Go

// SPDX-License-Identifier: MIT
package application
import (
"fmt"
"github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/services/nodes"
)
// newAgentControl builds the frontend's agent control client: the SELECTION
// that decides which agent worker answers a verb, and the control client that
// carries the verb to it.
//
// A named function rather than two more lines in initDistributed, for the
// reason distributedSchedulerOptions is one: initDistributed opens a database
// and a bus, so no unit spec reaches it, and the argument that matters here has
// no symptom when it is wrong. An empty instance id makes every connection read
// report nothing as held by this replica, so every MCP call takes a relay hop
// through a peer even when this replica holds the worker's tunnel itself, and
// nothing anywhere says so: the calls all succeed, just through one more
// process than they need. It is refused here instead of shipped as latency.
//
// The other two refusals are the ordinary kind. A selector with no registry has
// nothing to select from and a client with no transport reaches nobody, and
// both would present as MCP being quietly unavailable in a deployment that
// looks healthy.
func newAgentControl(cfg config.DistributedConfig, registry *nodes.NodeRegistry,
conns nodes.AgentConnectionReader, control *nodes.ControlClient) (*nodes.AgentControlClient, error) {
if cfg.InstanceID == "" {
return nil, fmt.Errorf("the agent control client was built with no instance id: every MCP call would relay through a peer even for a worker whose tunnel this replica holds")
}
if registry == nil || conns == nil {
return nil, fmt.Errorf("the agent control client was built with no way to find a connected agent worker")
}
if control == nil {
return nil, fmt.Errorf("the agent control client was built with no control transport to reach an agent worker over")
}
return nodes.NewAgentControlClient(nodes.NewAgentSelector(registry, conns, cfg.InstanceID), control), nil
}