Skip to content

fix: let bare Tools compete directly in delegate selection (not just as a last resort) - #195

Open
imaustink wants to merge 2 commits into
mainfrom
fix/ssh-skill-routing
Open

fix: let bare Tools compete directly in delegate selection (not just as a last resort)#195
imaustink wants to merge 2 commits into
mainfrom
fix/ssh-skill-routing

Conversation

@imaustink

@imaustink imaustink commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Summary

Follow-up to #192. After that PR deployed, "Can you SSH into airvinyl and see if it's running all good?" in Open WebUI got a generic LLM fallback with no tool use, and a follow-up with a specific command ("SSH into airvinyl and run uptime") got stuck showing "found 1 agent candidate" indefinitely.

Root cause, confirmed against the live cluster: claude-code-swe-agent is the only Agent in the whole catalog, and its broad description ("Performs software-engineering work on GitHub end-to-end. Runs the Claude Code CLI headless — which has bash, file-read/write, grep, and glob tools...") loosely matched via embedding similarity. selectDelegate (apps/agent-orchestrator/src/agent/graph.ts) only ever considered a bare Tool (like ssh, with no wrapping Skill) as a last resort — reachable only once skillCandidates AND agentCandidates both came up empty. Since an Agent candidate always existed, the ssh Tool never got a chance to compete at all, and dispatching to the agent hung on its Claude identity-link gate (a known, pre-existing bug class in this repo — fix/claude-auth-submit-hang et al. — unrelated to #192's code).

Fix (two parts)

1. ssh-skill (charts/community-components/templates/skill-ssh.yaml) — mirrors cluster-debug-skill's shape (toolRefs: [ssh]). Gives selectDelegate a strong, specific match for SSH-shaped requests so it wins over the vague agent overlap, and its authored markdown gives explicit guidance for turning an open-ended "is it healthy?" into a concrete sequence of read-only diagnostic calls.

2. The actual root cause (docs/adr/0036-tools-compete-directly-in-delegate-selection.md) — a bare Tool was starved out categorically, not because it lost a comparison, but because it was never in the comparison. Any future unwrapped Tool would hit the identical problem the moment a broad Agent (or Skill) exists in the catalog. Fixed by:

  • A new retrieveTools node (apps/agent-orchestrator/src/agent/graph.ts), inserted between retrieveAgents and selectDelegate: runs the same embedding query + ToolFitChecker two-stage relevance gate selectFallbackTool already used, reused rather than reinvented. Guarded on deps.delegateSelector being configured, so non-NATS deployments pay no extra cost and keep the exact old behavior.
  • DelegateSelector.select (apps/agent-orchestrator/src/agent/delegate-selector.ts) now takes a third tools parameter and makes one combined three-way choice (skill/agent/tool), with an explicit preference order in its prompt: skill (authored guidance) > bare tool (single well-defined action) > agent (open-ended/multi-step work).
  • selectDelegate's tool branch reuses a new shared helper (planFallbackToolCall, extracted from selectFallbackTool's own tail) to construct the actual tool call, falling back to the old noMatchFallback/selectFallbackTool safety net if the planner declines or the combined choice comes back empty.

ssh-skill is kept alongside the core fix rather than superseded by it — it still adds value a bare Tool alone can't (the authored multi-step diagnostic guidance).

Test plan

  • npm run typecheck --workspace=agent-orchestrator
  • npm run build --workspace=agent-orchestrator
  • npm run test --workspace=agent-orchestrator — 579/579 passing, including 4 new tests covering: tool candidates reach the delegate selector already fit-checked, a tool wins over an agent candidate when the selector says so (agent never launched), a tool that fails ToolFitChecker is excluded from what's offered to the selector, and the noMatchFallback safety net still works when the combined selector picks nothing.
  • helm lint charts/community-components
  • helm template against values.yaml defaults, values-production.yaml, and values-ci-all.yaml (verified against validate-crds.yml's literal "every template rendered" assertion).
  • Real kind cluster + kubectl apply --dry-run=server against the full rendered catalog — skill.core.controller-agent.dev/ssh-skill created (server dry run).
  • Real chat retest against the live cluster once deployed ("is airvinyl running okay", "SSH into airvinyl and run uptime").

🤖 Generated with Claude Code

https://claude.ai/code/session_01Fg8b9pPWm91nLbDnB6ECJh

imaustink and others added 2 commits August 4, 2026 16:01
"Is airvinyl running okay" matched claude-code-swe-agent -- the only
Agent in the catalog, description broad enough ("runs bash...") to
loosely overlap via embedding similarity -- instead of the ssh tool,
and hung on that agent's Claude identity-link gate (a known class of
pre-existing bug in this repo: fix/claude-auth-submit-hang et al.).
Nothing about that path is specific to ssh; any bare Tool with no
wrapping Skill is exposed to the same mis-routing risk once a broad
Agent exists in the catalog.

Adds ssh-skill (mirroring cluster-debug-skill's shape: toolRefs: [ssh],
no allowedRoles of its own per ADR 0011), so selectDelegate's combined
skill+agent choice sees a strong, specific match for SSH-shaped
requests and picks it over the vague agent match. Its markdown also
gives authored guidance for turning an open-ended "is it healthy?"
into a concrete sequence of read-only diagnostic calls (uptime, df -h,
free -m, then a named service if implied) -- separately addressing
why a vague request was declining at the ActionPlanner stage even when
the ssh tool WAS the right fallback candidate.

Verified with a real kind cluster + kubectl apply --dry-run=server.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fg8b9pPWm91nLbDnB6ECJh
…tion

Fixes the actual root cause behind ssh-skill being needed at all: a
bare Tool with no wrapping Skill was only ever considered as a last
resort (noMatchFallback's selectFallbackTool), invoked only once
skillCandidates AND agentCandidates both came up empty. Any Agent
whose description loosely overlapped a request via embedding
similarity alone -- not because it was actually the better fit, but
because a Tool never got the chance to compete at all -- would win by
existing as a candidate. That's what let claude-code-swe-agent (the
only Agent in the catalog) absorb "SSH into X" requests instead of
the ssh Tool, hanging on its identity-link gate.

Adds a `retrieveTools` graph node (embedding query + ToolFitChecker,
reusing selectFallbackTool's own two-stage relevance gate) alongside
retrieveSkills/retrieveAgents, and extends DelegateSelector to a
three-way choice among skills/agents/tools in one combined decision
(docs/adr/0036). Guarded on deps.delegateSelector being configured, so
non-NATS deployments pay no extra cost and keep the exact old
skill-only + selectFallbackTool behavior.

ssh-skill (this branch's other commit) stays -- it still adds value
a bare Tool alone can't: authored guidance for turning an open-ended
"is it healthy?" into a concrete sequence of diagnostic calls. This
fixes the starvation itself so no *future* unwrapped Tool needs a
Skill just to be reachable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fg8b9pPWm91nLbDnB6ECJh
@imaustink imaustink changed the title fix: add ssh-skill so SSH requests route to the tool, not an agent fix: let bare Tools compete directly in delegate selection (not just as a last resort) Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant