Skip to content

Commit d49e616

Browse files
authored
feat: add dynamic routing hints for local discovery (#54)
* feat: add dynamic routing hint controller * feat: gate routing hints with search config * docs: document routing hint behavior * fix: share indexer across plugin runtime hooks * test: cover repeated backticked identifier routing * fix: restore watcher lifecycle for routing hints
1 parent d29373d commit d49e616

File tree

6 files changed

+594
-2
lines changed

6 files changed

+594
-2
lines changed

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,13 @@ src/api/checkout.ts:89 (Route handler for /pay)
132132
| Don't know the function name | `codebase_search` | Semantic search finds by meaning |
133133
| Exploring unfamiliar codebase | `codebase_search` | Discovers related code across files |
134134
| Just need to find locations | `codebase_peek` | Returns metadata only, saves ~90% tokens |
135+
| Need the authoritative definition site | `implementation_lookup` | Prioritizes real implementation definitions over docs/tests |
135136
| Understand code flow | `call_graph` | Find callers/callees of any function |
136137
| Know exact identifier | `grep` | Faster, finds all occurrences |
137138
| Need ALL matches | `grep` | Semantic returns top N only |
138139
| Mixed discovery + precision | `/find` (hybrid) | Best of both worlds |
139140

140-
**Rule of thumb**: `codebase_peek` to find locations → `Read` to examine → `grep` for precision.
141+
**Rule of thumb**: `codebase_peek` to find locations → `Read` to examine → `grep` for precision. For symbol-definition questions, use `implementation_lookup` first.
141142

142143
## 📊 Token Usage
143144

@@ -296,6 +297,12 @@ The plugin exposes these tools to the OpenCode agent:
296297
```
297298
- **Workflow**: `codebase_peek` → find locations → `Read` specific files
298299

300+
### `implementation_lookup`
301+
**Definition-first lookup.** Jumps to the authoritative definition site for a symbol or natural-language definition query.
302+
- **Use for**: "Where is X defined?", symbol-definition requests, and cases where you want the implementation site rather than all usages.
303+
- **Behavior**: Prefers real implementation files over tests, docs, examples, and fixtures.
304+
- **Fallback**: If nothing authoritative is found, use `codebase_search` for broader discovery.
305+
299306
### `find_similar`
300307
Find code similar to a provided snippet.
301308
- **Use for**: Duplicate detection, refactor prep, pattern mining.
@@ -530,7 +537,8 @@ Zero-config by default (uses `auto` mode). Customize in `.opencode/codebase-inde
530537
"fusionStrategy": "rrf", // rrf | weighted
531538
"rrfK": 60, // RRF smoothing constant
532539
"rerankTopN": 20, // Deterministic rerank depth
533-
"contextLines": 0 // Extra lines before/after match
540+
"contextLines": 0, // Extra lines before/after match
541+
"routingHints": true // Runtime nudges for local discovery/definition queries
534542
},
535543
"reranker": {
536544
"enabled": false,
@@ -600,6 +608,7 @@ String values in `codebase-index.json` can reference environment variables with
600608
| `rrfK` | `60` | RRF smoothing constant. Higher values flatten rank impact, lower values prioritize top-ranked candidates more strongly |
601609
| `rerankTopN` | `20` | Deterministic rerank depth cap. Applies lightweight name/path/chunk-type rerank to top-N only |
602610
| `contextLines` | `0` | Extra lines to include before/after each match |
611+
| `routingHints` | `true` | Inject lightweight runtime hints for local conceptual discovery and definition lookups. Set to `false` to disable plugin-side routing nudges. |
603612
| **reranker** | | Optional second-stage model reranker for the top candidate pool |
604613
| `enabled` | `false` | Turn external reranking on/off |
605614
| `provider` | `"custom"` | Hosted shortcuts: `cohere`, `jina`, or `custom` |
@@ -621,6 +630,7 @@ String values in `codebase-index.json` can reference environment variables with
621630
### Retrieval ranking behavior
622631

623632
- `codebase_search` and `codebase_peek` use the hybrid path: semantic + keyword retrieval → fusion (`fusionStrategy`) → deterministic rerank (`rerankTopN`) → optional external reranker (`reranker`) → filtering.
633+
- When `search.routingHints` is enabled (default), the plugin adds tiny per-turn runtime hints for local conceptual discovery and definition queries. Conceptual discovery is nudged toward `codebase_peek` / `codebase_search`, while definition questions are nudged toward `implementation_lookup`. Exact identifier and unrelated operational tasks are left alone.
624634
- `find_similar` stays semantic-only: semantic retrieval + deterministic rerank only (no keyword retrieval, no RRF).
625635
- For compatibility rollbacks, set `search.fusionStrategy` to `"weighted"` to use the legacy weighted fusion path.
626636
- When enabled, the external reranker sees path metadata plus a bounded on-disk code snippet for each candidate so it can distinguish real implementations from docs/tests more reliably.

src/config/schema.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export interface SearchConfig {
4747
rrfK: number;
4848
rerankTopN: number;
4949
contextLines: number;
50+
routingHints: boolean;
5051
}
5152

5253
export type RerankerProvider = "cohere" | "jina" | "custom";
@@ -161,6 +162,7 @@ function getDefaultSearchConfig(): SearchConfig {
161162
rrfK: 60,
162163
rerankTopN: 20,
163164
contextLines: 0,
165+
routingHints: true,
164166
};
165167
}
166168

@@ -277,6 +279,7 @@ export function parseConfig(raw: unknown): ParsedCodebaseIndexConfig {
277279
rrfK: typeof rawSearch.rrfK === "number" ? Math.max(1, Math.floor(rawSearch.rrfK)) : defaultSearch.rrfK,
278280
rerankTopN: typeof rawSearch.rerankTopN === "number" ? Math.min(200, Math.max(0, Math.floor(rawSearch.rerankTopN))) : defaultSearch.rerankTopN,
279281
contextLines: typeof rawSearch.contextLines === "number" ? Math.min(50, Math.max(0, rawSearch.contextLines)) : defaultSearch.contextLines,
282+
routingHints: typeof rawSearch.routingHints === "boolean" ? rawSearch.routingHints : defaultSearch.routingHints,
280283
};
281284

282285
const rawDebug = (input.debug && typeof input.debug === "object" ? input.debug : {}) as Record<string, unknown>;

src/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
initializeTools,
2424
} from "./tools/index.js";
2525
import { loadCommandsFromDirectory } from "./commands/loader.js";
26+
import { RoutingHintController } from "./routing-hints.js";
2627
import { hasProjectMarker } from "./utils/files.js";
2728
import type { CombinedWatcher } from "./watcher/index.js";
2829

@@ -52,6 +53,9 @@ const plugin: Plugin = async ({ directory }) => {
5253
initializeTools(projectRoot, config);
5354

5455
const indexer = getSharedIndexer();
56+
const routingHints = config.search.routingHints
57+
? new RoutingHintController(() => indexer.getStatus())
58+
: null;
5559

5660
const isValidProject = !config.indexing.requireProjectMarker || hasProjectMarker(projectRoot);
5761

@@ -91,6 +95,19 @@ const plugin: Plugin = async ({ directory }) => {
9195
remove_knowledge_base,
9296
},
9397

98+
async "chat.message"(input, output) {
99+
routingHints?.observeUserMessage(input.sessionID, output.parts);
100+
},
101+
102+
async "experimental.chat.system.transform"(input, output) {
103+
const hints = await routingHints?.getSystemHints(input.sessionID) ?? [];
104+
output.system.push(...hints);
105+
},
106+
107+
async "tool.execute.after"(input) {
108+
routingHints?.markToolUsed(input.sessionID, input.tool);
109+
},
110+
94111
async config(cfg) {
95112
cfg.command = cfg.command ?? {};
96113

0 commit comments

Comments
 (0)