Skip to content
This repository was archived by the owner on May 18, 2026. It is now read-only.

feat: add single-shot tool retrieval layer#1

Merged
GusCayresMindsight merged 4 commits into
masterfrom
feat/tool-retrieval-layer
May 15, 2026
Merged

feat: add single-shot tool retrieval layer#1
GusCayresMindsight merged 4 commits into
masterfrom
feat/tool-retrieval-layer

Conversation

@GusCayresMindsight

Copy link
Copy Markdown
Owner

Summary

Adds a tool-RAG layer that embeds MCP tool descriptions at index time and uses cosine similarity to expose only the top-K relevant tools to OpenCode per session, cutting context overhead from loading every server's schema upfront.

New commands

Command Description
`opencode-workspace index [--force]` Connect to every MCP server, embed tools, store in `~/.config/opencode-workspace/tools.db`. Incremental: only re-embeds when description or input schema changes.
`opencode-workspace ""` One-shot: embed prompt → top-K retrieval → temp config with deny rules → `opencode run`.
`opencode-workspace stats [--last N]` Summarise retrieval sessions from `sessions.jsonl`.

Existing TUI mode (`opencode-workspace` with no args) is unchanged.
Kill switch: `OPENCODE_WORKSPACE_RETRIEVAL=off` passes through to opencode with no filtering.

Architecture

```
src/
index/ MCP stdio+remote client, HuggingFace ONNX embedder (all-MiniLM-L6-v2),
SQLite corpus with sqlite-vec ANN + brute-force cosine fallback
retrieval/ cosine search, server-level deny-rule composer,
non-destructive temp config overlay (OPENCODE_CONFIG, never mutates user config)
telemetry/ atomic JSONL session log, stats aggregation
config.js deep-merged config, defaults: local embedder, k=10, topk strategy
OpenAI wired; Voyage/Cohere stubbed
bin/smoke.js make smoke: index then assert GitHub tool is top-1 for a GitHub query
docs/ Gherkin feature files documenting all new behaviour (37 scenarios, 5 files)
```

Non-destructive by design

  • Never modifies the user's OpenCode config in place
  • Temp config written to `/tmp/ow-session-.json`, deleted after `opencode` exits
  • Only `"deny"` rules are generated — user's existing `allow`/`deny` entries are preserved
  • Filtering is server-level: one retrieved tool keeps the whole server open

Verified

```
$ make smoke
=== Step 1: index MCP tool corpus ===
✓ notion +22
✓ gitlab +179
✓ playwright +23
✓ fetch +1
✓ semgrep +7
✓ aws-knowledge +6
✓ sequential-thinking +1
✓ github +26
⚠ brave-search failed: --brave-api-key is required (expected, no key configured)
Done. corpus: 265 tools

=== Step 2: retrieval assertion ===
PASS Corpus contains 265 tools
PASS Query returned 5 result(s)
PASS Top result: github/get_pull_request score=0.370
```

Embeds MCP tool descriptions at index time and uses cosine similarity
to expose only the top-K relevant tools to OpenCode per session.

New commands:
- opencode-workspace index [--force]  build/update the tool corpus
- opencode-workspace stats [--last N] summarise retrieval sessions
- opencode-workspace "<prompt>"       one-shot with tool filtering

Key components:
- src/index/       MCP client (stdio+remote), HuggingFace ONNX embedder,
                   SQLite corpus with sqlite-vec ANN + brute-force fallback
- src/retrieval/   cosine search, server-level deny-rule composer,
                   non-destructive temp config overlay via OPENCODE_CONFIG
- src/telemetry/   atomic JSONL sessions log, stats aggregation
- src/config.js    deep-merged config with defaults (local all-MiniLM-L6-v2,
                   k=10, topk strategy); OpenAI/Voyage/Cohere provider stubs
- bin/smoke.js     make smoke: index + assert GitHub tool is top-1 for a
                   known GitHub query

Kill switch: OPENCODE_WORKSPACE_RETRIEVAL=off passes through unchanged.
TUI mode (no prompt arg) is unaffected.

docs/: Gherkin feature files for all new behaviour (37 scenarios).
37 scenarios / 158 steps covering all 5 feature files:
  indexing, retrieval, permissions, telemetry, configuration

Infrastructure:
- unit-tests/support/   world, hooks (isolated HOME per scenario,
                        process.exit stub, console capture), fixtures
                        (fake 384-dim vector space, canned tool lists)
- unit-tests/step-definitions/  common + one file per feature
- cucumber.js           runner config (docs/*.feature, 30s timeout)
- proxyquire used for cmd modules that destructure deps at require time

.github/workflows/test.yml:
- ubuntu-latest / Node 20 LTS
- npm ci --ignore-scripts (skip postinstall tool downloads)
- npm rebuild better-sqlite3 (restore native binary)
- npx cucumber-js

make test -> npx cucumber-js (was: help + kill-switch check)
Three new retrieval modes complement the existing one-shot flow:

1. Standalone retrieve command
   - src/cmd/retrieve.js: cmdRetrieve(query, { json, k }) embeds a query
     and prints top-K corpus matches to stdout (text or --json).
   - Dispatched as: opencode-workspace retrieve [--json] [--k N] <query>

2. TUI first-message hook (OpenCode plugin)
   - lib/tool-retrieval.plugin.js: ESM plugin installed to
     ~/.config/opencode/plugins/ow-tool-retrieval.js by 'install'.
   - Hooks message.updated; fires once per session on the first user
     message; calls 'opencode-workspace retrieve --json' as a subprocess;
     injects results via client.session.prompt({ noReply: true }) so the
     agent has tool recommendations in context before it replies.
   - Soft-fails silently on any error.

3. On-demand search_tools MCP tool
   - src/mcp/tool-retrieval-server.js: MCP stdio server launched as
     'opencode-workspace mcp-serve'.
   - Exposes search_tools({ query, k? }) — the agent calls this
     proactively whenever it needs to discover relevant MCP capabilities.
   - Added to lib/opencode.json.template as the 'tool-retrieval' server.
   - Listed in ALWAYS_ALLOWED (src/retrieval/permissions.js) so it is
     never denied by the one-shot permission generator.

Other changes:
   - bin/cli.js: retrieve and mcp-serve subcommands; plugin copy step in
     cmdInstall(); updated help text.
   - AGENTS.md: documents all three retrieval modes, new runtime paths,
     ALWAYS_ALLOWED gotcha, plugin ESM/global-scope notes.
   - docs/tui-retrieval.feature, docs/tool-retrieval-mcp.feature: BDD specs.
   - unit-tests/step-definitions/indexing.steps.js: add tool-retrieval to
     the all-servers-fail stub so the exit-code-1 scenario stays correct.
…eval-mcp features

Refactored plugin and MCP server to expose testable CommonJS helpers, then
wired up full step definitions for both new feature files. All 50 scenarios
and 223 steps pass.

Refactors (to enable unit testing without a live OpenCode or MCP process):
- src/cmd/tui-hook.js (new): extracts first-message hook logic from the ESM
  plugin into a testable CommonJS module. Exports createFirstMessageHandler()
  (stateful, closes over seenSessions Set) and handleFirstMessage() (core
  injection logic with optional _searchFn override for tests).
- src/mcp/search-tools-handler.js (new): extracts search_tools handler from
  the MCP server. Exports handleSearchTools() and formatResults(). Accepts
  optional _searchFn for test injection.
- lib/tool-retrieval.plugin.js: rewritten to import createFirstMessageHandler
  via createRequire(import.meta.url) instead of spawning a subprocess. Plugin
  body is now a 3-line thin adapter.
- src/mcp/tool-retrieval-server.js: delegates tools/call to handleSearchTools.

Test infrastructure:
- unit-tests/support/fixtures.js: add FETCH_TOOLS (fetch server, dim 4) and
  add 'fetch' to ALL_FIXTURES for 'browse/fetch URL' scenarios.
- unit-tests/support/world.js: add _capturedInjections, _hookHandler,
  _hookLastQuery, _searchToolsResult to constructor; add runHook() and
  runSearchTools() helpers.

New step definitions (all steps now defined — 0 undefined):
- unit-tests/step-definitions/tui-retrieval.steps.js: 26 steps covering
  first-message injection, session dedup, role filtering, silent-fail on
  error, and context format validation.
- unit-tests/step-definitions/tool-retrieval-mcp.steps.js: 18 steps covering
  ranked results, k limit, empty corpus message, missing query error,
  ALWAYS_ALLOWED permission behaviour, and server-specific relevance.

Feature file updates:
- docs/tui-retrieval.feature: renamed 'Plugin is silent when opencode-workspace
  is not in PATH' → 'Plugin is silent when search throws an error' to reflect
  the in-process (non-subprocess) architecture after the refactor.
@GusCayresMindsight GusCayresMindsight self-assigned this May 15, 2026
@GusCayresMindsight GusCayresMindsight merged commit 3d37779 into master May 15, 2026
3 checks passed
@GusCayresMindsight GusCayresMindsight deleted the feat/tool-retrieval-layer branch May 15, 2026 19:27
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant