vectr 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. agent/__init__.py +0 -0
  2. agent/cartographer.py +428 -0
  3. agent/chunk_quality.py +1119 -0
  4. agent/config.py +648 -0
  5. agent/config.yaml +1153 -0
  6. agent/eviction_advisor.py +447 -0
  7. agent/identifier_hint.py +68 -0
  8. agent/indexer/__init__.py +112 -0
  9. agent/indexer/_chunking.py +458 -0
  10. agent/indexer/_constants.py +105 -0
  11. agent/indexer/_core.py +878 -0
  12. agent/indexer/_types.py +157 -0
  13. agent/instance_registry.py +127 -0
  14. agent/llm_client.py +8 -0
  15. agent/model_cache.py +101 -0
  16. agent/prompt_templates.py +31 -0
  17. agent/searcher.py +848 -0
  18. agent/strategy_selector.py +299 -0
  19. agent/symbol_graph/__init__.py +131 -0
  20. agent/symbol_graph/_constants.py +366 -0
  21. agent/symbol_graph/_extraction.py +528 -0
  22. agent/symbol_graph/_graph.py +1878 -0
  23. agent/symbol_graph/_types.py +35 -0
  24. agent/templates/claude_md.md +65 -0
  25. agent/templates/claude_md_search_only.md +27 -0
  26. agent/templates/cursor_mcp.json.template +7 -0
  27. agent/templates/cursor_rules_header.txt +5 -0
  28. agent/templates/hook_no_double_recall.txt +1 -0
  29. agent/templates/mcp.json.template +8 -0
  30. agent/templates/session_start_guidance_default.txt +3 -0
  31. agent/templates/session_start_guidance_hooks_aware.txt +1 -0
  32. agent/templates/tool_loading_guidance_claude.txt +1 -0
  33. agent/templates/tool_loading_guidance_claude_search_only.txt +1 -0
  34. agent/templates/vscode_mcp.json.template +8 -0
  35. agent/tool_necessity_probe.py +177 -0
  36. agent/version_stamp.py +58 -0
  37. agent/watcher.py +515 -0
  38. agent/working_context_store/__init__.py +76 -0
  39. agent/working_context_store/_audit.py +46 -0
  40. agent/working_context_store/_encryption.py +75 -0
  41. agent/working_context_store/_store.py +1130 -0
  42. agent/working_context_store/_types.py +50 -0
  43. api.py +101 -0
  44. app/__init__.py +0 -0
  45. app/models.py +368 -0
  46. app/routes.py +451 -0
  47. app/service.py +1055 -0
  48. integrations/__init__.py +0 -0
  49. integrations/mcp_server/__init__.py +73 -0
  50. integrations/mcp_server/_dispatch.py +782 -0
  51. integrations/mcp_server/_schemas.py +484 -0
  52. integrations/mcp_server/_session.py +86 -0
  53. integrations/vscode_bridge.py +71 -0
  54. integrations/workspace_detect.py +259 -0
  55. main.py +2037 -0
  56. vectr-1.0.0.dist-info/METADATA +37 -0
  57. vectr-1.0.0.dist-info/RECORD +61 -0
  58. vectr-1.0.0.dist-info/WHEEL +5 -0
  59. vectr-1.0.0.dist-info/entry_points.txt +2 -0
  60. vectr-1.0.0.dist-info/licenses/LICENSE +21 -0
  61. vectr-1.0.0.dist-info/top_level.txt +5 -0
@@ -0,0 +1,447 @@
1
+ """
2
+ EvictionAdvisor — tells the LLM which chunks vectr can re-retrieve in <50ms.
3
+
4
+ The guarantee: anything listed here is fully indexed and re-retrievable in <50ms.
5
+ This is the reverse signal in the bidirectional protocol. The LLM calls vectr_remember
6
+ to save notes; the EvictionAdvisor proactively signals when retrieved content
7
+ is fully indexed and can be re-retrieved without re-reading the file.
8
+
9
+ The advisor tracks which chunks have been retrieved in the current session and
10
+ estimates their token cost. When the session hits a threshold, it fires an
11
+ eviction hint.
12
+
13
+ Limitation — Read/Bash blind spot (E2):
14
+ The advisor only tracks content delivered through vectr's own tools (vectr_search,
15
+ vectr_locate, vectr_trace, vectr_recall). Code the agent reads via the IDE's native
16
+ Read or Bash tools is invisible here. Token estimates are therefore a lower bound;
17
+ eviction may fire later than ideal on sessions that mix vectr retrieval with direct
18
+ file reading. The gap shrinks as agents adopt vectr_search as their primary navigation
19
+ path (Problem 3). Track `vectr_evict_hint_triggered` in benchmarks (E5) to monitor
20
+ real-world eviction uptake.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import time
25
+ from dataclasses import dataclass, field
26
+
27
+ from agent.config import (
28
+ EVICTION_HINT_MAX_IDS,
29
+ EVICTION_REMEMBER_ESCALATION_CHUNKS,
30
+ EVICTION_REMEMBER_ESCALATION_TOKENS,
31
+ EVICTION_RETRIEVED_TOKEN_GATE,
32
+ )
33
+
34
+
35
+ @dataclass
36
+ class RetrievedChunk:
37
+ file_path: str
38
+ lines: str
39
+ symbol_name: str
40
+ content: str
41
+ # UPG-EVICT-SESSION-SCOPE: the exact vectr_fetch re-fetch key for this chunk
42
+ # (`file_path:start_line-end_line`), when the caller had one. Empty for
43
+ # chunks recorded from a surface that doesn't guarantee an exact indexed
44
+ # chunk id (e.g. a symbol-graph snippet whose line range may not match a
45
+ # stored chunk boundary) — never guessed, so eviction_hint() only ever
46
+ # advertises a re-fetch key that is known to work.
47
+ chunk_id: str = ""
48
+ retrieved_at: float = field(default_factory=time.time)
49
+
50
+ @property
51
+ def estimated_tokens(self) -> int:
52
+ return max(1, len(self.content) // 4)
53
+
54
+
55
+ class EvictionAdvisor:
56
+ """
57
+ Tracks what the LLM has retrieved this session and advises on eviction.
58
+
59
+ Usage:
60
+ advisor = EvictionAdvisor(eviction_threshold_tokens=4000)
61
+ advisor.record(chunk) # call after every vectr_search result
62
+ hint = advisor.eviction_hint() # returns text the LLM can act on
63
+ """
64
+
65
+ def __init__(
66
+ self,
67
+ eviction_threshold_tokens: int = 40_000,
68
+ tool_call_threshold: int = 10,
69
+ retrieval_call_threshold: int = 1,
70
+ time_threshold_seconds: float = 180.0,
71
+ rearm_retrieval_calls: int = 4,
72
+ retrieved_token_gate: int | None = None,
73
+ remember_escalation_chunks: int | None = None,
74
+ remember_escalation_tokens: int | None = None,
75
+ ) -> None:
76
+ # Fire when ANY of these conditions is met:
77
+ # cumulative injected chars ÷ 4 >= 40K (vectr-tracked content)
78
+ # tool_call_count > 10 (all MCP calls this session)
79
+ # retrieval_call_count > 1 (search/locate/trace calls only)
80
+ # elapsed seconds >= 180 (wall-clock since session start)
81
+ #
82
+ # Why retrieval_call_threshold: the tool_call_count trigger counts all vectr MCP
83
+ # calls, but the LLM typically makes only 3-4 per task (status + 1 locate + 2 search)
84
+ # so threshold=10 never fires. Counting only retrieval calls (search/locate/trace)
85
+ # and firing on the 2nd one gives a natural mid-task trigger: the LLM has found
86
+ # something worth keeping before it switches into implementation mode.
87
+ #
88
+ # Why time_threshold: the eviction protocol requires vectr to see every retrieval
89
+ # to track real context pressure (spec line 78), but the LLM uses native Read/Bash
90
+ # for most reading — those calls are invisible here. A time-based trigger fires
91
+ # regardless of tool mix, catching sessions where the LLM barely uses vectr at all.
92
+ # arXiv:2310.08560, arXiv:2510.24699.
93
+ self._threshold = eviction_threshold_tokens
94
+ self._tool_call_threshold = tool_call_threshold
95
+ self._retrieval_call_threshold = retrieval_call_threshold
96
+ self._time_threshold_seconds = time_threshold_seconds
97
+ # UPG-7.1: re-arm the auto-footer only after this many further retrieval
98
+ # calls (or a fresh token/time crossing) since the last time it fired —
99
+ # so the footer can't repeat on every response.
100
+ self._rearm_retrieval_calls = rearm_retrieval_calls
101
+ # UPG-11.15: suppress auto_eviction_hint() when accumulated retrieved
102
+ # tokens since the last hint (or session start) are below this gate.
103
+ # Prevents a burst of small-result searches from triggering the full
104
+ # ACTION REQUIRED block before real context pressure has accumulated.
105
+ # None → use the config value (EVICTION_RETRIEVED_TOKEN_GATE).
106
+ self._retrieved_token_gate: int = (
107
+ retrieved_token_gate if retrieved_token_gate is not None
108
+ else EVICTION_RETRIEVED_TOKEN_GATE
109
+ )
110
+ # UPG-REMEMBER-BANNER-FATIGUE: gates auto_eviction_hint()'s escalated
111
+ # directive on chunks retrieved since the last vectr_remember, not
112
+ # just on raw context pressure — see note_remembered().
113
+ self._remember_escalation_chunks: int = (
114
+ remember_escalation_chunks if remember_escalation_chunks is not None
115
+ else EVICTION_REMEMBER_ESCALATION_CHUNKS
116
+ )
117
+ # UPG-EVICT-ESCALATION-GATE-TOO-LOW: companion gate required IN ADDITION
118
+ # to _remember_escalation_chunks — see note_remembered() and
119
+ # _tokens_since_remember(). A single large search can satisfy the chunk
120
+ # gate alone in one burst; requiring both makes that burst insufficient
121
+ # on its own to re-trip the escalated directive.
122
+ self._remember_escalation_tokens: int = (
123
+ remember_escalation_tokens if remember_escalation_tokens is not None
124
+ else EVICTION_REMEMBER_ESCALATION_TOKENS
125
+ )
126
+ self._chunks: list[RetrievedChunk] = []
127
+ self._tool_call_count: int = 0
128
+ self._retrieval_call_count: int = 0
129
+ self._session_started_at: float = time.time()
130
+ # (tokens, retrieval_count, wall_time) recorded the last time the auto
131
+ # footer emitted; None until the first emit. Gates auto_eviction_hint().
132
+ self._last_emit: tuple[int, int, float] | None = None
133
+ # New chunks recorded since the caller's last vectr_remember (or
134
+ # session start). Reset by note_remembered(). See
135
+ # EVICTION_REMEMBER_ESCALATION_CHUNKS.
136
+ self._chunks_since_remember: int = 0
137
+ # Tracked total-token count as of the caller's last vectr_remember (or
138
+ # 0 at session start). _tokens_since_remember() subtracts this baseline
139
+ # from the running total (UPG-EVICT-ESCALATION-GATE-TOO-LOW).
140
+ self._tokens_at_last_remember: int = 0
141
+ # True once note_remembered() has fired and no escalated (ACTION
142
+ # REQUIRED) directive has re-emitted since. While True, the NEXT
143
+ # eligible auto-hint renders the softer, non-imperative form instead
144
+ # of escalating immediately — repeating harsh wording on the very
145
+ # next search after compliance trains the caller to ignore it.
146
+ # Starts False: before the caller has ever called vectr_remember,
147
+ # auto_eviction_hint() escalates immediately as before.
148
+ self._soft_fire_pending: bool = False
149
+
150
+ def record(
151
+ self, file_path: str, lines: str, symbol_name: str, content: str,
152
+ chunk_id: str = "",
153
+ ) -> None:
154
+ """Record a chunk that was delivered to the LLM this session."""
155
+ # avoid duplicate tracking for the same file:lines
156
+ key = f"{file_path}:{lines}"
157
+ for i, c in enumerate(self._chunks):
158
+ if f"{c.file_path}:{c.lines}" == key:
159
+ # UPG-EVICT-RECENCY-DEDUP-BLIND: a repeat retrieval of the exact
160
+ # same chunk (the LLM re-running the same query and getting the
161
+ # same top hit) is the common case, and eviction_hint()'s
162
+ # most-recently-retrieved-first ordering keys recency off a
163
+ # file's position in self._chunks. Without this move, re-touching
164
+ # a file never refreshes that position, so the file the caller
165
+ # just retrieved again silently renders LAST instead of first.
166
+ # Move the existing entry to the end (fresh recency) without
167
+ # storing a duplicate or advancing any counter below — a dedup
168
+ # hit must be a no-op for token/chunk totals, only recency moves.
169
+ self._chunks.append(self._chunks.pop(i))
170
+ return
171
+ self._chunks.append(RetrievedChunk(
172
+ file_path=file_path,
173
+ lines=lines,
174
+ symbol_name=symbol_name,
175
+ content=content,
176
+ chunk_id=chunk_id,
177
+ ))
178
+ self._chunks_since_remember += 1
179
+
180
+ def record_results(self, results: list) -> None:
181
+ """Record a batch of SearchResult objects (from searcher.py)."""
182
+ for r in results:
183
+ self.record(
184
+ file_path=r.file_path,
185
+ lines=str(r.lines),
186
+ symbol_name=r.symbol_name or "",
187
+ content=r.content,
188
+ chunk_id=getattr(r, "chunk_id", "") or "",
189
+ )
190
+
191
+ def note_remembered(self) -> None:
192
+ """Call when the caller's vectr_remember succeeds this session.
193
+
194
+ Resets the chunks-since-last-remember counter and the tokens-since-
195
+ last-remember baseline so auto_eviction_hint()'s escalated directive
196
+ doesn't immediately re-fire on the very next retrieval
197
+ (UPG-REMEMBER-BANNER-FATIGUE, UPG-EVICT-ESCALATION-GATE-TOO-LOW).
198
+ Also arms the softer first-refire wording (see _soft_fire_pending).
199
+ Does not touch should_evict()'s own token/call/time state — a
200
+ vectr_remember doesn't reduce the actual context already retrieved.
201
+ """
202
+ self._chunks_since_remember = 0
203
+ self._tokens_at_last_remember = self.total_tokens_in_session()
204
+ self._soft_fire_pending = True
205
+
206
+ def _tokens_since_remember(self) -> int:
207
+ """Tokens tracked since the caller's last vectr_remember (or session
208
+ start, if never called). Companion to _chunks_since_remember —
209
+ see EVICTION_REMEMBER_ESCALATION_TOKENS."""
210
+ return self.total_tokens_in_session() - self._tokens_at_last_remember
211
+
212
+ def increment_tool_call(self) -> None:
213
+ """Increment the total MCP tool call counter for this session."""
214
+ self._tool_call_count += 1
215
+
216
+ def increment_retrieval_call(self) -> None:
217
+ """Increment the retrieval-specific call counter (search/locate/trace only)."""
218
+ self._retrieval_call_count += 1
219
+
220
+ def total_tokens_in_session(self) -> int:
221
+ return sum(c.estimated_tokens for c in self._chunks)
222
+
223
+ def should_evict(self) -> bool:
224
+ elapsed = time.time() - self._session_started_at
225
+ return (
226
+ self.total_tokens_in_session() >= self._threshold
227
+ or self._tool_call_count > self._tool_call_threshold
228
+ or self._retrieval_call_count > self._retrieval_call_threshold
229
+ or elapsed >= self._time_threshold_seconds
230
+ )
231
+
232
+ def _fresh_escalation(self) -> bool:
233
+ """True on the first auto-emit, then only after a MATERIAL increase since
234
+ the last one: another full token-threshold retrieved, `_rearm_retrieval_calls`
235
+ more retrieval calls, or another time-threshold window elapsed. Keeps the
236
+ per-response footer from repeating once pressure has already been flagged."""
237
+ if self._last_emit is None:
238
+ return True
239
+ tokens0, retr0, t0 = self._last_emit
240
+ return (
241
+ self.total_tokens_in_session() - tokens0 >= self._threshold
242
+ or self._retrieval_call_count - retr0 >= self._rearm_retrieval_calls
243
+ or (time.time() - t0) >= self._time_threshold_seconds
244
+ )
245
+
246
+ def _tokens_since_last_hint(self) -> int:
247
+ """Tokens accumulated since the last auto-hint emit (or session start)."""
248
+ tokens_at_last = self._last_emit[0] if self._last_emit is not None else 0
249
+ return self.total_tokens_in_session() - tokens_at_last
250
+
251
+ def auto_eviction_hint(self) -> str:
252
+ """Gated variant for the per-response footer (UPG-7.1 / UPG-11.15).
253
+
254
+ Emits the hint only when BOTH conditions hold:
255
+ 1. Context pressure freshly escalates (UPG-7.1 — never on every response).
256
+ 2. Accumulated retrieved tokens since the last hint (or session start)
257
+ have crossed _retrieved_token_gate (UPG-11.15 — suppresses bursts of
258
+ small-result searches that add negligible context pressure).
259
+
260
+ The explicit ``vectr_evict_hint`` tool and the ``/v1/evict`` endpoint
261
+ still use ``eviction_hint()`` (ungated): an explicit ask always answers.
262
+
263
+ UPG-REMEMBER-BANNER-FATIGUE / UPG-EVICT-ESCALATION-GATE-TOO-LOW: also
264
+ suppressed unless BOTH enough new chunks AND enough new tokens have
265
+ been retrieved since the caller's last vectr_remember (or session
266
+ start) — a single large search can trip a chunk-only gate in one
267
+ burst, so both must independently clear before the gate is satisfied
268
+ at all. Below that, MCP dispatch's own turn-count soft nudge (or
269
+ nothing) takes over instead of this method's wording.
270
+
271
+ Once the gate is satisfied, the FIRST eligible fire after a
272
+ vectr_remember renders the softer, non-imperative form (not ACTION
273
+ REQUIRED); only a second-or-later eligible fire without an
274
+ intervening vectr_remember escalates to the imperative directive
275
+ (UPG-EVICT-ESCALATION-GATE-TOO-LOW) — so compliance is never
276
+ answered with the same harsh wording on the very next response.
277
+ """
278
+ if not self.should_evict() or not self._fresh_escalation():
279
+ return ""
280
+ # UPG-11.15: even if should_evict() is true (e.g. retrieval_call_count
281
+ # tripped), don't emit if the retrieved content since the last hint is
282
+ # small. The per-call-count trigger fires on the 2nd retrieval call,
283
+ # but three 15-line methods contribute only ~100–150 tokens — far below
284
+ # real context pressure.
285
+ if self._tokens_since_last_hint() < self._retrieved_token_gate:
286
+ return ""
287
+ if (
288
+ self._chunks_since_remember < self._remember_escalation_chunks
289
+ or self._tokens_since_remember() < self._remember_escalation_tokens
290
+ ):
291
+ return ""
292
+ escalate = not self._soft_fire_pending
293
+ hint = self.eviction_hint(escalated=escalate)
294
+ if hint:
295
+ self._last_emit = (
296
+ self.total_tokens_in_session(),
297
+ self._retrieval_call_count,
298
+ time.time(),
299
+ )
300
+ if not escalate:
301
+ # Soft form consumed — the next eligible fire (without an
302
+ # intervening vectr_remember) escalates.
303
+ self._soft_fire_pending = False
304
+ return hint
305
+
306
+ def eviction_hint(self, escalated: bool = True) -> str:
307
+ """
308
+ Return a message listing chunks vectr can re-retrieve in <50ms.
309
+ Always safe to call — returns an empty hint if nothing has been retrieved
310
+ and no time-based pressure exists.
311
+
312
+ escalated=True (the default, used by the explicit vectr_evict_hint tool
313
+ and the /v1/evict endpoint — an explicit ask always gets the full
314
+ directive) renders the imperative "ACTION REQUIRED" wording plus a
315
+ trailing machine-readable ``needs_remember: true`` line a harness can
316
+ key off without parsing prose. escalated=False renders the identical
317
+ factual content (files, token count, re-fetch keys) with softer,
318
+ non-imperative wording and omits the needs_remember line — used by
319
+ auto_eviction_hint() for the first eligible re-fire after a
320
+ vectr_remember (UPG-EVICT-ESCALATION-GATE-TOO-LOW).
321
+ """
322
+ if not self._chunks:
323
+ # No vectr-tracked chunks, but time pressure still warrants a nudge
324
+ elapsed = time.time() - self._session_started_at
325
+ if elapsed >= self._time_threshold_seconds:
326
+ if escalated:
327
+ return (
328
+ "─── ACTION REQUIRED ───\n"
329
+ "Call vectr_remember(content, tags=[...]) NOW before continuing.\n"
330
+ "Save: key type names, module paths, entry points, non-obvious patterns.\n"
331
+ "Your synthesized understanding does not persist automatically.\n"
332
+ "Call vectr_remember now, then continue your task.\n"
333
+ "needs_remember: true"
334
+ )
335
+ return (
336
+ "Reminder: consider calling vectr_remember(content, tags=[...]) soon.\n"
337
+ "Save: key type names, module paths, entry points, non-obvious patterns.\n"
338
+ "Your synthesized understanding does not persist automatically."
339
+ )
340
+ return ""
341
+
342
+ total_tokens = self.total_tokens_in_session()
343
+ by_file: dict[str, list[RetrievedChunk]] = {}
344
+ for c in self._chunks:
345
+ by_file.setdefault(c.file_path, []).append(c)
346
+
347
+ # UPG-EVICT-HINT-RECENCY: order files most-recently-retrieved-first,
348
+ # not by first-ever-recorded order (a dict's insertion-order iteration
349
+ # was an accident of implementation, not a chosen policy). The
350
+ # caller's active working set — what it can most usefully drop from
351
+ # context and re-fetch on demand — is whatever it retrieved most
352
+ # recently, not whatever it happened to retrieve first this session.
353
+ # A file's recency is the position (in self._chunks) of the LAST
354
+ # chunk recorded for it; since record() appends monotonically, that
355
+ # position doubles as a recency sequence number. Sorting descending
356
+ # by this unique-per-file key is deterministic; the stable sort
357
+ # tie-breaks by insertion order in the (practically unreachable)
358
+ # case of equal keys.
359
+ last_seq: dict[str, int] = {}
360
+ for i, c in enumerate(self._chunks):
361
+ last_seq[c.file_path] = i
362
+ file_items = sorted(by_file.items(), key=lambda kv: last_seq[kv[0]], reverse=True)
363
+ shown = file_items[:5]
364
+ overflow = len(file_items) - len(shown)
365
+
366
+ if escalated:
367
+ lines = [
368
+ "─── ACTION REQUIRED ───",
369
+ "Call vectr_remember(content, tags=[...]) NOW before continuing.",
370
+ "Save: key type names, module paths, entry points, non-obvious patterns.",
371
+ "Your synthesized understanding does not persist automatically — the output",
372
+ "file captures findings, not the navigational path to reach them.",
373
+ "",
374
+ ]
375
+ else:
376
+ lines = [
377
+ "─── context recap ───",
378
+ "New content has been retrieved since your last vectr_remember. If you found",
379
+ "anything worth keeping — key type names, module paths, entry points, non-obvious",
380
+ "patterns — call vectr_remember(content, tags=[...]) now; otherwise continue.",
381
+ "",
382
+ ]
383
+ lines += [
384
+ f"Vectr has {len(self._chunks)} retrieved chunks (~{total_tokens} tokens)"
385
+ " fully indexed. Drop these chunks from context — each is re-fetchable"
386
+ " verbatim via vectr_fetch(ids=[...]) in <50ms."
387
+ " Your synthesized analysis (saved via vectr_remember) is retrievable via vectr_recall.",
388
+ "",
389
+ "Files below are listed most recently retrieved first:",
390
+ ]
391
+ for fpath, chunks in shown:
392
+ ranges = ", ".join(
393
+ f"lines {c.lines}" + (f" ({c.symbol_name})" if c.symbol_name else "")
394
+ for c in chunks
395
+ )
396
+ lines.append(f" {fpath} [{ranges}]")
397
+ if overflow:
398
+ lines.append(f" ... and {overflow} more file(s). All retrievable via vectr_search('<description>').")
399
+
400
+ # UPG-EVICT-SESSION-SCOPE: list the exact re-fetch keys additively —
401
+ # only for chunks whose id is a known-good vectr_fetch key (never a
402
+ # guessed one), capped so the hint's own token cost stays bounded.
403
+ # UPG-EVICT-REFETCH-KEYS-STALE: self._chunks is oldest-first with
404
+ # re-touched chunks moved to the end, so take the suffix and reverse —
405
+ # most recently retrieved first, matching the file list's stated
406
+ # ordering above. The old prefix slice pinned the session's OLDEST
407
+ # chunks here forever, rendering the identical id list in every
408
+ # escalated banner regardless of what was retrieved since.
409
+ fetch_ids = [c.chunk_id for c in self._chunks if c.chunk_id][-EVICTION_HINT_MAX_IDS:][::-1]
410
+ if fetch_ids:
411
+ id_list = ", ".join(f'"{i}"' for i in fetch_ids)
412
+ lines += [
413
+ "",
414
+ f"Re-fetch keys: vectr_fetch(ids=[{id_list}]) restores these verbatim.",
415
+ ]
416
+
417
+ if escalated:
418
+ lines += [
419
+ "",
420
+ "Call vectr_remember now, then continue your task.",
421
+ "needs_remember: true",
422
+ ]
423
+ return "\n".join(lines)
424
+
425
+ def clear_session(self) -> None:
426
+ """Reset for a new session."""
427
+ self._chunks.clear()
428
+ self._tool_call_count = 0
429
+ self._retrieval_call_count = 0
430
+ self._session_started_at = time.time()
431
+ self._last_emit = None
432
+ self._chunks_since_remember = 0
433
+ self._tokens_at_last_remember = 0
434
+ self._soft_fire_pending = False
435
+
436
+ def as_chunk_dicts(self) -> list[dict]:
437
+ """Serialisable form for snapshot storage."""
438
+ return [
439
+ {
440
+ "file": c.file_path,
441
+ "lines": c.lines,
442
+ "symbol": c.symbol_name,
443
+ "content": c.content,
444
+ "chunk_id": c.chunk_id,
445
+ }
446
+ for c in self._chunks
447
+ ]
@@ -0,0 +1,68 @@
1
+ """
2
+ Identifier-shape tokenizer for the vectr_search additive symbol-graph hint
3
+ (UPG-QUERYTYPE-REROUTE).
4
+
5
+ This replaces the deleted `agent/query_router.py` regex query-classification
6
+ layer. That layer classified a query's INTENT via keyword/phrase lists
7
+ (`\\boverride\\b`, `\\bsubclass(es)?\\b`, `\\b(dependenc(y|ies)|downstream|
8
+ upstream)\\b`, ...) and, on a match, silently overrode the fingerprint-derived
9
+ semantic weight and guessed a symbol to look up from the query's first
10
+ non-stopword word — an innocent NL question about "dependencies" or
11
+ "overriding a method" misrouted into a same-named-homonym symbol lookup with
12
+ no ranking tie to the query at all.
13
+
14
+ The replacement here does no intent classification whatsoever. It detects
15
+ token SHAPE, not meaning: CamelCase, snake_case, and dotted/qualified
16
+ (`Class.method`) forms are structural properties of an identifier regardless
17
+ of what the surrounding sentence is about. A plain word is never treated as
18
+ an identifier candidate even if it happens to also be a symbol name — the
19
+ `identifier_hint_symbols()` caller (see `app/service.py`) additionally
20
+ requires an EXACT symbol-graph match before anything is surfaced, so a
21
+ shape-detected token that doesn't resolve produces no output at all.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import re
26
+
27
+ # Combined, single regex — shape detection only, no word/keyword/phrase list:
28
+ # 1. dotted/qualified form e.g. "QuerySet.delete"
29
+ # 2. CamelCase/PascalCase e.g. "WorkspaceLock" (leading capital, an
30
+ # internal lower-then-upper transition — this
31
+ # excludes an ordinary capitalised sentence word
32
+ # like "Where", which has no internal transition)
33
+ # 3. lowerCamelCase e.g. "scheduleUpdateOnFiber" (leading
34
+ # lowercase, at least two lower/digit chars, then
35
+ # an internal lower-then-upper transition — the
36
+ # two-char minimum before the transition is what
37
+ # excludes a single-letter-prefix brand word like
38
+ # "iPhone"/"eBay", not shape detection of any
39
+ # particular word)
40
+ # 4. snake_case e.g. "acquire_lock" (contains an underscore)
41
+ # Order matters: the dotted alternative is tried first so "Class.method" is
42
+ # captured as one token rather than splitting at the dot. ALLCAPS tokens
43
+ # ("HTML", "API") match none of these — the CamelCase/lowerCamelCase
44
+ # alternatives both require an internal LOWERCASE run before the case
45
+ # transition, which an all-uppercase run never has.
46
+ _IDENTIFIER_TOKEN_RE = re.compile(
47
+ r"[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*"
48
+ r"|[A-Z][a-z0-9]+[A-Z][A-Za-z0-9]*"
49
+ r"|[a-z][a-z0-9]+[A-Z][A-Za-z0-9]*"
50
+ r"|[A-Za-z_][A-Za-z0-9]*_[A-Za-z0-9_]*"
51
+ )
52
+
53
+
54
+ def extract_identifier_tokens(query: str) -> list[str]:
55
+ """Extract identifier-SHAPED tokens from a raw query string.
56
+
57
+ Returns candidates in order of first appearance, deduplicated. Empty for
58
+ a query built entirely of plain words (no CamelCase/snake_case/dotted
59
+ signal) — the common case for ordinary natural-language questions.
60
+ """
61
+ seen: set[str] = set()
62
+ tokens: list[str] = []
63
+ for m in _IDENTIFIER_TOKEN_RE.finditer(query):
64
+ tok = m.group(0)
65
+ if tok not in seen:
66
+ seen.add(tok)
67
+ tokens.append(tok)
68
+ return tokens
@@ -0,0 +1,112 @@
1
+ """AST-aware code chunking and embedding pipeline.
2
+
3
+ Package layout:
4
+ _constants.py — LANG_BY_EXT, EXCLUDED_DIRS, batch-size constants
5
+ _types.py — CodeChunk dataclass, EmbedProvider protocol, embed provider classes
6
+ _chunking.py — tree-sitter parsers, chunk collection, fallback window chunker
7
+ _core.py — CodeIndexer orchestration class
8
+
9
+ All names that existed on the flat agent/indexer.py module are re-exported here
10
+ so every existing import site keeps working unchanged:
11
+ from agent.indexer import CodeIndexer (watcher, searcher, service, tests)
12
+ from agent.indexer import chunk_file (tests)
13
+ from agent.indexer import LANG_BY_EXT (watcher, cartographer, symbol_graph)
14
+ from agent.indexer import EXCLUDED_DIRS (watcher, cartographer)
15
+ from agent.indexer import get_embed_provider (tests — patched at this namespace)
16
+ from agent.indexer import c_symbol_name (symbol_graph)
17
+ from agent.indexer import _get_parser (symbol_graph)
18
+ from agent.indexer import _parser_language_for (symbol_graph — UPG-JSFLOW-SYMBOLS)
19
+ """
20
+ from __future__ import annotations
21
+
22
+ # Public constants
23
+ from agent.indexer._constants import (
24
+ LANG_BY_EXT,
25
+ EXCLUDED_DIRS,
26
+ _FILE_BATCH_SIZE,
27
+ _EMBED_BATCH_SIZE,
28
+ _UPSERT_BATCH_SIZE,
29
+ _CHUNK_WORKERS,
30
+ )
31
+
32
+ # Public types / embed providers
33
+ from agent.indexer._types import (
34
+ CodeChunk,
35
+ EmbedProvider,
36
+ LocalEmbedProvider,
37
+ VoyageEmbedProvider,
38
+ OpenAIEmbedProvider,
39
+ get_embed_provider,
40
+ )
41
+
42
+ # Config-sourced tunables re-exported so `import agent.indexer as m; m._MAX_CHUNK_LINES`
43
+ # continues to work (test_config_loader asserts these are the same objects as config.py).
44
+ from agent.config import (
45
+ INDEXING_MAX_CHUNK_LINES as _MAX_CHUNK_LINES,
46
+ INDEXING_CLASS_HEADER_LINES as _CLASS_HEADER_LINES,
47
+ )
48
+
49
+ # Public chunking symbols (tests import chunk_file; symbol_graph imports
50
+ # _get_parser and c_symbol_name directly from agent.indexer)
51
+ from agent.indexer._chunking import (
52
+ chunk_file,
53
+ c_symbol_name,
54
+ _get_parser,
55
+ _PARSER_CACHE,
56
+ _CLASS_NODE_TYPES,
57
+ _CHUNK_NODE_TYPES,
58
+ _SYMBOL_FIELD,
59
+ _C_TYPE_NAME_NODES,
60
+ _c_declarator_name,
61
+ _extract_symbol_name,
62
+ _get_leading_comments,
63
+ _collect_chunks_ast,
64
+ _fallback_window_chunks,
65
+ _chunk_markdown,
66
+ _postprocess_chunks,
67
+ is_flow_javascript,
68
+ _parser_language_for,
69
+ )
70
+
71
+ # Public indexer class
72
+ from agent.indexer._core import CodeIndexer
73
+
74
+ __all__ = [
75
+ # Config-sourced tunables (re-exported for test_config_loader)
76
+ "_MAX_CHUNK_LINES",
77
+ "_CLASS_HEADER_LINES",
78
+ # Constants
79
+ "LANG_BY_EXT",
80
+ "EXCLUDED_DIRS",
81
+ "_FILE_BATCH_SIZE",
82
+ "_EMBED_BATCH_SIZE",
83
+ "_UPSERT_BATCH_SIZE",
84
+ "_CHUNK_WORKERS",
85
+ # Types / providers
86
+ "CodeChunk",
87
+ "EmbedProvider",
88
+ "LocalEmbedProvider",
89
+ "VoyageEmbedProvider",
90
+ "OpenAIEmbedProvider",
91
+ "get_embed_provider",
92
+ # Chunking
93
+ "chunk_file",
94
+ "c_symbol_name",
95
+ "_get_parser",
96
+ "_PARSER_CACHE",
97
+ "_CLASS_NODE_TYPES",
98
+ "_CHUNK_NODE_TYPES",
99
+ "_SYMBOL_FIELD",
100
+ "_C_TYPE_NAME_NODES",
101
+ "_c_declarator_name",
102
+ "_extract_symbol_name",
103
+ "_get_leading_comments",
104
+ "_collect_chunks_ast",
105
+ "_fallback_window_chunks",
106
+ "_chunk_markdown",
107
+ "_postprocess_chunks",
108
+ "is_flow_javascript",
109
+ "_parser_language_for",
110
+ # Core
111
+ "CodeIndexer",
112
+ ]