16 KiB
PR4 Recon — Slice 5: Agent-Search / Agent-Pick
Screen 09 Marketplace · Variation A "agent-suggestion box" "Describe what you want to do…" → agent recommends a connector + skill + tool, each with a "why" reason and an install button.
Branch: feature/warm-hive-pr4 · READ-ONLY recon · cited file:line as of 2026-06-16.
TL;DR
The agent-pick engine already exists and is good: searchCapabilities() in
packages/agent/src/capability-acquisition.ts:179 takes a natural-language need and returns a
ranked, deduped AcquisitionProposal of native tools / installed skills / starter-pack skills /
marketplace packages — each candidate already carries a matchReason ("why") and an
installAction plus a trust assessment. It fuses keyword scoring (no embeddings) across local
sources with pre-fetched marketplace FTS5 candidates.
The gap is entirely the surface, not the brain. This engine is reachable only as an agent tool
(acquire_capability, packages/agent/src/skill-tools.ts:406) invoked inside the chat loop — there is
no HTTP route that the Screen-09 agent-search bar could POST a need to and render the
three-up suggestion box. PR4 must add a thin REST endpoint over the existing searchCapabilities()
(plus its deps assembly that today lives inline in local/index.ts:620) and a new
MarketplaceApp agent-search UI. The matching scoring is single-recommendation today
(proposal.recommendation is ONE candidate); the design wants one-of-each-kind (connector+skill+tool),
which is a selection/grouping change on top of the existing ranked candidates[], not new matching.
What exists today (verified)
1. The matching brain — searchCapabilities() (the agent-pick core)
packages/agent/src/capability-acquisition.ts
- Input (
SearchCapabilitiesInput, :170):{ need, installedSkills[], starterSkillsDir, nativeToolNames[], marketplaceCandidates[] }. Marketplace candidates are pre-fetched and passed in —searchCapabilitiesitself does no IO except reading starter-skill.mdfiles from disk (loadStarterSkillsMeta, :142). - Algorithm (no embeddings — pure keyword):
extractKeywords()(:64, stop-word filtered) →scoreMatch()(:74, name-hit ×2 / content-hit ×1, normalized 0–1) across four source lanes:- native tools (scored vs
NATIVE_TOOL_HINTSmap, :113); - installed/active skills (:218);
- starter-pack skills not yet installed (:241);
- marketplace candidates, merging the FTS score when present via
Math.max(keywordScore, mkt.score)(:270).
- native tools (scored vs
- Output (
AcquisitionProposal, :39):{ need, gapDetected, summary, candidates[]≤8, recommendation, alreadyHandled }. EachCapabilityCandidate(:27) already has the exact fields Screen-09 needs:name,type('native'|'skill'|'plugin'|'mcp'|'connector'|'marketplace'),availability,description,matchReason(the "why", built bybuildMatchReason:99),installAction(string|null), andtrust. - Recommendation is SINGLE (:307–314): picks the best installable (or best active if
already-handled). It does not group into connector+skill+tool. The full ranked
candidates[]is there to do that, but the grouping logic does not exist yet. summaryis a markdown string built for the chat surface (buildProposalSummary, :330): it even emits the inline-install marker (<!--waggle:capability_request {...}-->, :385) verbatim for the chat card. This is debug/chat-grade prose — a UI agent-suggestion box would consume the structuredcandidates/recommendation, notsummary.
2. How it's invoked today — the agent tool acquire_capability
packages/agent/src/skill-tools.ts:404–478
- Tool
acquire_capability(param:need) gathers deps:getInstalledSkills(),starterSkillsDir,nativeToolNames, and callsdeps.searchMarketplace(need)(graceful try/catch) to pre-fetch marketplace candidates, then callssearchCapabilities(...)and returnsproposal.summary(the markdown string) to the model. Audit event recorded on gap (:461). - Companion tool
install_capability(:482) installs starter-pack skills only (validated byvalidateInstallCandidate, capability-acquisition.ts:424 — rejects any source ≠starter-pack). - The deps are wired in
packages/server/src/local/index.ts:620(createSkillTools({...})):nativeToolNames= union of mind/system/plan/git/document tool names (:624);getInstalledSkills= liveserver.agentState.skills(:631, hot-reloadable);searchMarketplace=marketplaceDb.search({ query, limit: 10 })mapped toMarketplaceCandidate[](:641–656) — notescoreis hardcodedundefined(FTS rank not surfaced through the API).
3. The marketplace search it sits on
packages/server/src/local/routes/marketplace.ts:57 — GET /api/marketplace/search
→ MarketplaceDB.search() (packages/marketplace/src/db.ts:86).
- FTS5 over the
packagestable. Critically,db.searchalready tolerates a verbose NLneed:toFtsMatchQuery()(db.ts:91–99) relaxes the raw string into an OR-of-prefixes and falls back to an unfiltered listing rather than throwing — so the agent's natural-language need works as-is. SearchResult(types.ts:233):{ packages[], total, facets{types,categories,sources}, installedCount }. EachMarketplacePackagecarrieswaggle_install_type('skill'|'connector'|'mcp') → the kind badge,package_type,description,downloads(install count), and (route-annotated at marketplace.ts:105)installed,scanStatus.installedCountis the natural source for the "N in this workspace" count bar (currently catalog-wide, not workspace-scoped — see Gaps).
4. The parallel, narrower "recommend" path (skills-only) — DO NOT confuse with agent-pick
packages/agent/src/skill-recommender.ts (SkillRecommender.recommend(context, topN), class at :118).
- Multi-signal keyword + bigram + synonym-cluster matcher over installed skills only (no
marketplace, no connectors, no MCP). Returns
SkillRecommendation[] = {skillName, reason, relevanceScore}— also a "why" (reason), but skills-only. - Exposed over HTTP at
GET /api/skills/suggestions?context=&topN=(packages/server/src/local/routes/skills.ts:393–406). This is the only existing HTTP surface that returns "what should I use" with a reason — but it's the wrong shelf (skills only, already installed) for Screen-09's connector+skill+tool suggestion box. Useful as a prior-art pattern for shaping the new route.
5. The router (a third matcher) — for completeness
packages/agent/src/capability-router.ts (CapabilityRouter.resolve(query), :58). Maps a query to
ranked routes across native/connector/skill/plugin/mcp/subagent with confidences. Resolution, not
recommendation — returns "where could this be handled" not "install this". Connector lane (:85)
knows connected status and emits a suggestion when not connected. Not currently HTTP-exposed; a
secondary input if PR4 wants live-connector awareness in the suggestion box.
6. The inline-card render path (the "sync" downstream, shared with the grid)
- Parser:
apps/web/src/components/os/apps/chat-blocks/capability-request-parser.ts—segmentText()splits agent text on the<!--waggle:capability_request {name,source,reason}-->marker (and a legacy phrasing) into install-card segments, deduped bysource::name. - Card:
apps/web/src/components/os/apps/chat-blocks/CapabilityRequestCard.tsx— renders the pending→installing→installed/failed micro-states (:17,:107–149) with a "why" line fromrequest.reason(:103). Install routing already branches by source: marketplace →adapter.searchMarketplacethenadapter.installMarketplacePackage(pkg.id)(:48–54, tier-gated, 403 → UpgradeModal); starter-pack →adapter.installPack(:69). This is exactly the type-aware one-click flow §09 asks for, already built for the inline-chat variation (Variation B).
7. Adapter methods already present (the install actions)
apps/web/src/lib/adapter.ts: searchMarketplace(query,limit) (:1334), installMarketplacePackage(id)
(:1338), installPack(skillId) (:1292), connectConnector(id,creds) (:1971), installMcp(mcpId)
(:2017). All three install kinds in the §09 sync spec (skill Add / connector Connect / MCP Enable)
have adapter coverage.
Gaps vs the Screen-09 contract
-
No HTTP route for agent-pick.
searchCapabilities()is reachable ONLY inside the chat agent loop via theacquire_capabilitytool. The Screen-09 centered agent-search bar ("Ask the agent") needs aPOST /api/marketplace/agent-search(or similar) that runssearchCapabilitiesand returns structured candidates (not the chatsummarymarkdown). Must be built on top of the existing engine. -
Returns chat-grade
summary, not a structured suggestion box. The tool returnsproposal.summary(markdown for the model). The UI needs the rawcandidates[]/recommendationJSON. The data is computed (proposal object) but discarded at skill-tools.ts:476 — a route would return the object directly. -
Single recommendation, not one-of-each-kind.
proposal.recommendationis ONE candidate (capability-acquisition.ts:307). §09 wants connector + skill + tool (three, one per kind) each with its own why + install. Needs a small grouping pass over the rankedcandidates[](top pertype/availability) — new logic, but trivial given candidates already carrytype. -
Marketplace deps assembly lives inline in
local/index.ts, not reusable. ThegetInstalledSkills/nativeToolNames/searchMarketplaceclosure that feeds the tool is constructed once at server boot (index.ts:620). A new route needs the same deps — either lift this into a shared helper or have the route reconstruct it. (Native tool names, in particular, are only assembled in that closure.) -
"N in this workspace" count bar is catalog-wide, not workspace-scoped.
installedCount(db.search → types.ts:242) counts all installed packages globally; the marketplace DB is not workspace-partitioned. §09's per-workspace chip count has no backing field today. -
No "example chips" / suggested-need seeding. Pure UI; no backend. Can be static or derived from persona
suggestedCommands— out of agent-pick scope. -
No shared install-state store on the frontend. §09's CRITICAL "sync" (one store powers grid + agent-pick + inline card, installing in any view reflects in all) — the current
MarketplaceApp.tsx(no agent-search at all, install state local to each card) andCapabilityRequestCard(localuseStatephase) have independent state. This is the headline PR4 frontend build; agent-pick is one of the three consumers of that store. (Owned by the "shared install store" slice — flagged here as the integration boundary.) -
MarketplaceApp.tsxhas no agent-search UI. Verified: the 417-line component (apps/web/src/components/os/apps/MarketplaceApp.tsx) contains no "Ask the agent" / suggestion /acquire-style references — only grid install/uninstall viainstallMarketplacePackage(:228). Variation A's centered bar + suggestion box must be built net-new. -
Marketplace FTS rank not surfaced.
searchMarketplacehardcodesscore: undefined(index.ts:651), so marketplace candidates rank purely by keyword re-scoring insidesearchCapabilities. Acceptable, but means FTS relevance is currently dropped on the floor for the agent-pick path.
Exact integration points a PR4 build would touch
- Reuse (engine):
searchCapabilities(input): AcquisitionProposal—packages/agent/src/capability-acquisition.ts:179. Exported from@waggle/agent(packages/agent/src/index.ts:326). TypesCapabilityCandidate/AcquisitionProposal/MarketplaceCandidate/SearchCapabilitiesInputare all exported. - Build (route): new
POST /api/marketplace/agent-search(body{ need }) in the marketplace route filepackages/server/src/local/routes/marketplace.ts— runssearchCapabilitieswith deps assembled likelocal/index.ts:620–657, returns structured{ candidates, recommendation, groupedByKind }. Pattern to mirror for shape/contract:GET /api/skills/suggestions(packages/server/src/local/routes/skills.ts:393). - Reuse (deps): marketplace search
MarketplaceDB.search({query,limit})(packages/marketplace/src/db.ts:86); installed-skills sourceserver.agentState.skills; native tool-name union (currently only assembled atlocal/index.ts:624— lift if reused). - Add (grouping): a
pickOnePerKind(candidates)helper (new) to satisfy connector+skill+tool — trivial reduce overcandidate.type. - Install actions (already present, reuse):
adapter.installMarketplacePackage/adapter.installPack/adapter.connectConnector/adapter.installMcp(apps/web/src/lib/adapter.ts:1338/1292/1971/2017). Marker/card render reuse:segmentText+CapabilityRequestCard(chat-blocks/). - Count bar:
SearchResult.installedCount(packages/marketplace/src/types.ts:242) via/api/marketplace/search; needs workspace-scoping if §09's per-workspace count is taken literally. - Audit (existing, ride along):
fastify.auditStore?.record(...)already called on gap in skill-tools.ts:461 — a route should record proposals the same way.
Risks / watch-outs
- Two parallel matchers + a router (
searchCapabilitiesvsSkillRecommendervsCapabilityRouter) — building a new route on the wrong one (e.g.SkillRecommender, which is skills-only and HTTP-exposed already) would silently drop connectors/MCP. UsesearchCapabilities. summaryvs structured-candidates confusion — the tool's return value is markdown; do not parse it for the UI. Return the proposal object from the new route.- Keyword-only matching — no embeddings; verbose/synonym-heavy needs may under-match. Acceptable
for v1 but the suggestion box may look thin on phrasing mismatch. (
SkillRecommenderhas synonym expansion;searchCapabilitiesdoes not.) - Tier gating asymmetry — marketplace/MCP install is PRO-gated (marketplace.ts:181
requireTier), starter-pack/skill is free. The suggestion box must reflect this (the inline card already 403→Upgrade, CapabilityRequestCard.tsx:55). - Workspace scoping of installs — marketplace DB is global; the "N in this workspace" framing may over-promise isolation that the substrate doesn't provide.
- Native tool-name list is closure-local (index.ts:624) — a route reconstructing deps must not drift from the real registered tool set, or agent-pick "already have a tool" answers go stale.
Open questions for the founder/lead
- Route shape: dedicated
POST /api/marketplace/agent-search, or extendGET /api/marketplace/searchwith anagentPick=truemode? (The former is cleaner given the verbose NL body + structured proposal response.) - One-of-each-kind vs top-N: §09 shows exactly connector+skill+tool (3). When a kind has no match (e.g. no relevant connector), show 2? Show an empty-kind hint? Define the grouping contract.
- Count bar semantics: is "N in this workspace" literally per-workspace (needs new
workspace-scoped install tracking) or is catalog-wide
installedCountacceptable for v1? - Suggestion-box "why": use the engine's
matchReason(keyword-hit-grade, e.g. "name matches: risk") as-is, or have the route pass candidates to the LLM for a one-line natural "why"? The former is free + deterministic; the latter is prettier but adds a model call. - Shared install store ownership: confirm the frontend "sync" store is a separate PR4 slice that agent-pick plugs into (this recon treats it as the integration boundary, not part of Slice 5).