flash-context 0.3.0__tar.gz → 0.3.2__tar.gz
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.
- {flash_context-0.3.0 → flash_context-0.3.2}/CHANGELOG.md +44 -0
- {flash_context-0.3.0 → flash_context-0.3.2}/PKG-INFO +10 -14
- {flash_context-0.3.0 → flash_context-0.3.2}/README.md +10 -14
- {flash_context-0.3.0 → flash_context-0.3.2}/pyproject.toml +1 -1
- {flash_context-0.3.0 → flash_context-0.3.2}/src/flash_context/agent.py +229 -37
- {flash_context-0.3.0 → flash_context-0.3.2}/src/flash_context/mcp_server.py +3 -0
- {flash_context-0.3.0 → flash_context-0.3.2}/.env.example +0 -0
- {flash_context-0.3.0 → flash_context-0.3.2}/.gitignore +0 -0
- {flash_context-0.3.0 → flash_context-0.3.2}/LICENSE +0 -0
- {flash_context-0.3.0 → flash_context-0.3.2}/config.yaml.example +0 -0
- {flash_context-0.3.0 → flash_context-0.3.2}/src/flash_context/__init__.py +0 -0
- {flash_context-0.3.0 → flash_context-0.3.2}/src/flash_context/cli.py +0 -0
- {flash_context-0.3.0 → flash_context-0.3.2}/src/flash_context/config.py +0 -0
- {flash_context-0.3.0 → flash_context-0.3.2}/src/flash_context/llm.py +0 -0
- {flash_context-0.3.0 → flash_context-0.3.2}/src/flash_context/logging.py +0 -0
- {flash_context-0.3.0 → flash_context-0.3.2}/src/flash_context/tools/__init__.py +0 -0
- {flash_context-0.3.0 → flash_context-0.3.2}/src/flash_context/tools/glob.py +0 -0
- {flash_context-0.3.0 → flash_context-0.3.2}/src/flash_context/tools/grep.py +0 -0
- {flash_context-0.3.0 → flash_context-0.3.2}/src/flash_context/tools/read.py +0 -0
- {flash_context-0.3.0 → flash_context-0.3.2}/src/flash_context/tools/safety.py +0 -0
- {flash_context-0.3.0 → flash_context-0.3.2}/src/flash_context/types.py +0 -0
|
@@ -4,6 +4,50 @@ User-visible changes and migration instructions for each release.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.3.2] - 2026-11-01
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Require whole, case-sensitive query tokens inside exact-symbol citations,
|
|
12
|
+
including read-derived fallback evidence. Reject plausible-home file citations
|
|
13
|
+
without matching source text and retain scoped Python preflight diagnostics.
|
|
14
|
+
- Report `query symbol not found in verified evidence` when symbol lookups finish
|
|
15
|
+
without accepted evidence, rather than relying on generic termination warnings.
|
|
16
|
+
|
|
17
|
+
### Migration
|
|
18
|
+
|
|
19
|
+
- Exact-symbol queries may now return `NO_CITATIONS_FOUND` with empty citations
|
|
20
|
+
and snippets where previous versions returned unrelated files. Token presence
|
|
21
|
+
is lexical evidence, not proof of a definition or semantic relevance. Public
|
|
22
|
+
schemas, configuration, and CLI exit semantics are unchanged.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Condense the README overview and move safety and evidence-validation details
|
|
27
|
+
to the architecture documentation.
|
|
28
|
+
|
|
29
|
+
## [0.3.1] - 2026-11-01
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- Report the flash-context package version during MCP initialization instead of
|
|
34
|
+
the MCP SDK version.
|
|
35
|
+
- Reuse repeated tool observations without discarding fresh searches in the same
|
|
36
|
+
batch; execute same-turn duplicates only once and stop all-repeat stalls.
|
|
37
|
+
- Continue implementation searches after documentation-only citations within the
|
|
38
|
+
existing turn budget. Documentation-only fallbacks retain citations and snippets
|
|
39
|
+
with explicit incomplete-result metadata and warnings.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- Describe deterministic lookup and delegated model search together in the README.
|
|
44
|
+
|
|
45
|
+
### Migration
|
|
46
|
+
|
|
47
|
+
- Implementation questions may use additional turns before returning documentation
|
|
48
|
+
evidence. These fallbacks set `truncated: true`; partial cited CLI results still
|
|
49
|
+
exit 0. Budgets, configuration keys, and result schemas are unchanged.
|
|
50
|
+
|
|
7
51
|
## [0.3.0] - 2026-11-01
|
|
8
52
|
|
|
9
53
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: flash-context
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Read-only context exploration and retrieval for AI agents, starting with codebases and repositories.
|
|
5
5
|
Project-URL: Repository, https://github.com/zyf0717/flash-context
|
|
6
6
|
Project-URL: Issues, https://github.com/zyf0717/flash-context/issues
|
|
@@ -28,20 +28,16 @@ Description-Content-Type: text/markdown
|
|
|
28
28
|
[](https://pypi.org/project/flash-context/)
|
|
29
29
|
[](LICENSE)
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
A lightweight, model-agnostic repository explorer for AI agents, available as a
|
|
32
|
+
Python CLI and Model Context Protocol (MCP) server. It combines deterministic
|
|
33
|
+
lookup with search and reading delegated to a utility model through an
|
|
34
|
+
OpenAI-compatible endpoint, returning compact, validated file-line citations and
|
|
35
|
+
source snippets.
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
Codebases and repositories are the current focus; the same tools explore
|
|
39
|
-
documentation, configuration, and other local text files. CLI and MCP share one
|
|
40
|
-
exploration core with read-only tools, path safety, and resource limits.
|
|
41
|
-
Scans and regex execution are bounded; partial results report truncation.
|
|
42
|
-
Citation validation checks safe paths and existing line coverage, not semantic
|
|
43
|
-
relevance or minimal ranges. Citation precision depends on the explorer model.
|
|
44
|
-
Optional metadata-only trajectory logs must stay outside the inspected repository.
|
|
37
|
+
CLI and MCP share a read-only core for exploring code, documentation,
|
|
38
|
+
configuration, and other local text files with path safety and resource limits.
|
|
39
|
+
See [safety and evidence validation](docs/architecture.md#safety-and-evidence-validation)
|
|
40
|
+
for details and limitations.
|
|
45
41
|
|
|
46
42
|
Inspired by [FastContext](https://arxiv.org/abs/2606.14066), a now-withdrawn paper
|
|
47
43
|
on separating repository exploration from task solving, using specialized models
|
|
@@ -5,20 +5,16 @@
|
|
|
5
5
|
[](https://pypi.org/project/flash-context/)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
Scans and regex execution are bounded; partial results report truncation.
|
|
19
|
-
Citation validation checks safe paths and existing line coverage, not semantic
|
|
20
|
-
relevance or minimal ranges. Citation precision depends on the explorer model.
|
|
21
|
-
Optional metadata-only trajectory logs must stay outside the inspected repository.
|
|
8
|
+
A lightweight, model-agnostic repository explorer for AI agents, available as a
|
|
9
|
+
Python CLI and Model Context Protocol (MCP) server. It combines deterministic
|
|
10
|
+
lookup with search and reading delegated to a utility model through an
|
|
11
|
+
OpenAI-compatible endpoint, returning compact, validated file-line citations and
|
|
12
|
+
source snippets.
|
|
13
|
+
|
|
14
|
+
CLI and MCP share a read-only core for exploring code, documentation,
|
|
15
|
+
configuration, and other local text files with path safety and resource limits.
|
|
16
|
+
See [safety and evidence validation](docs/architecture.md#safety-and-evidence-validation)
|
|
17
|
+
for details and limitations.
|
|
22
18
|
|
|
23
19
|
Inspired by [FastContext](https://arxiv.org/abs/2606.14066), a now-withdrawn paper
|
|
24
20
|
on separating repository exploration from task solving, using specialized models
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
import re
|
|
5
5
|
from concurrent.futures import ThreadPoolExecutor
|
|
6
|
-
from dataclasses import dataclass, field
|
|
6
|
+
from dataclasses import dataclass, field, replace
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Any, Literal, cast
|
|
9
9
|
|
|
@@ -62,11 +62,21 @@ EXACT_TEXT_RE = re.compile(r"[`'\"](?P<target>[^`'\"]{1,120})[`'\"]")
|
|
|
62
62
|
TOKEN_RE = re.compile(r"\b(?P<token>[A-Za-z_][A-Za-z0-9_]*)\b")
|
|
63
63
|
|
|
64
64
|
|
|
65
|
+
@dataclass(frozen=True, slots=True)
|
|
66
|
+
class _DefinitionScan:
|
|
67
|
+
citations: tuple[Citation, ...]
|
|
68
|
+
status: Literal["unique", "missing", "ambiguous", "incomplete"]
|
|
69
|
+
warnings: tuple[str, ...]
|
|
70
|
+
|
|
71
|
+
|
|
65
72
|
@dataclass(slots=True)
|
|
66
73
|
class EvidenceState:
|
|
67
74
|
reads: list[ToolObservation] = field(default_factory=list)
|
|
68
75
|
citations: list[Citation] = field(default_factory=list)
|
|
69
|
-
|
|
76
|
+
observations: dict[str, ToolObservation] = field(default_factory=dict)
|
|
77
|
+
prefer_source: bool = False
|
|
78
|
+
symbol: str | None = None
|
|
79
|
+
definition_scan: _DefinitionScan | None = None
|
|
70
80
|
|
|
71
81
|
|
|
72
82
|
def explore(
|
|
@@ -78,10 +88,15 @@ def explore(
|
|
|
78
88
|
request.validate()
|
|
79
89
|
repo_root = request.repo_root.resolve()
|
|
80
90
|
validate_trajectory_dir(settings.traj_dir, repo_root)
|
|
91
|
+
evidence = EvidenceState(
|
|
92
|
+
prefer_source=_implementation_query(request.query),
|
|
93
|
+
symbol=_query_symbol(request.query),
|
|
94
|
+
)
|
|
81
95
|
fast_result = _try_exact_fast_path(
|
|
82
96
|
request,
|
|
83
97
|
repo_root=repo_root,
|
|
84
98
|
settings=settings,
|
|
99
|
+
evidence=evidence,
|
|
85
100
|
)
|
|
86
101
|
if fast_result is not None:
|
|
87
102
|
return fast_result
|
|
@@ -98,12 +113,28 @@ def explore(
|
|
|
98
113
|
limits=settings.limits,
|
|
99
114
|
)
|
|
100
115
|
messages = _initial_messages(request, file_sample, settings)
|
|
116
|
+
if evidence.definition_scan is not None:
|
|
117
|
+
scan = evidence.definition_scan
|
|
118
|
+
messages.append(
|
|
119
|
+
{
|
|
120
|
+
"role": "system",
|
|
121
|
+
"content": (
|
|
122
|
+
f"Local Python .py/.pyi definition scan for {evidence.symbol}: "
|
|
123
|
+
f"{scan.status}. "
|
|
124
|
+
"A complete missing result means zero Python definition matches; "
|
|
125
|
+
"assignments, imports, docs, and other languages may still "
|
|
126
|
+
"contain the symbol. Never infer a plausible home file. "
|
|
127
|
+
"Cited lines must contain the exact case-sensitive whole symbol."
|
|
128
|
+
),
|
|
129
|
+
}
|
|
130
|
+
)
|
|
101
131
|
tools = tool_schemas()
|
|
102
132
|
# A capped file sample is expected; actual scan failures must reach callers.
|
|
103
133
|
warnings = list(file_sample.warnings)
|
|
134
|
+
if evidence.definition_scan is not None:
|
|
135
|
+
warnings.extend(evidence.definition_scan.warnings)
|
|
104
136
|
truncated = bool(warnings)
|
|
105
137
|
last_content = ""
|
|
106
|
-
evidence = EvidenceState()
|
|
107
138
|
|
|
108
139
|
try:
|
|
109
140
|
for turn in range(1, effective_max_turns + 1):
|
|
@@ -145,6 +176,7 @@ def explore(
|
|
|
145
176
|
recorder.finish(result=result)
|
|
146
177
|
return result
|
|
147
178
|
recorder.record_model_turn(turn, message)
|
|
179
|
+
needs_source = False
|
|
148
180
|
content_text = str(message.get("content") or "").strip()
|
|
149
181
|
if content_text:
|
|
150
182
|
last_content = content_text
|
|
@@ -157,6 +189,12 @@ def explore(
|
|
|
157
189
|
)
|
|
158
190
|
if citations:
|
|
159
191
|
evidence.citations = citations
|
|
192
|
+
needs_source = evidence.prefer_source and _documentation_only(
|
|
193
|
+
citations
|
|
194
|
+
)
|
|
195
|
+
if needs_source:
|
|
196
|
+
last_content = ""
|
|
197
|
+
if citations and not needs_source:
|
|
160
198
|
if _message_has_tool_calls(message):
|
|
161
199
|
_append_warning_once(
|
|
162
200
|
warnings,
|
|
@@ -178,6 +216,27 @@ def explore(
|
|
|
178
216
|
tool_calls = _extract_tool_calls(message)
|
|
179
217
|
if not tool_calls:
|
|
180
218
|
citations = _best_current_citations(evidence)
|
|
219
|
+
if evidence.symbol and content_text == NO_CITATIONS_FOUND:
|
|
220
|
+
citations = []
|
|
221
|
+
elif evidence.symbol and not citations and turn < effective_max_turns:
|
|
222
|
+
messages.append({"role": "assistant", "content": content_text})
|
|
223
|
+
messages.append(
|
|
224
|
+
{
|
|
225
|
+
"role": "user",
|
|
226
|
+
"content": (
|
|
227
|
+
"No verified citation contains the exact query symbol. "
|
|
228
|
+
"Search with fresh read-only calls, or return "
|
|
229
|
+
"NO_CITATIONS_FOUND."
|
|
230
|
+
),
|
|
231
|
+
}
|
|
232
|
+
)
|
|
233
|
+
last_content = ""
|
|
234
|
+
continue
|
|
235
|
+
if evidence.prefer_source and _documentation_only(citations):
|
|
236
|
+
last_content = ""
|
|
237
|
+
messages.append({"role": "assistant", "content": content_text})
|
|
238
|
+
messages.append(_source_feedback())
|
|
239
|
+
continue
|
|
181
240
|
if request.citation and content_text != NO_CITATIONS_FOUND:
|
|
182
241
|
_append_warning_once(
|
|
183
242
|
warnings,
|
|
@@ -187,7 +246,7 @@ def explore(
|
|
|
187
246
|
request=request,
|
|
188
247
|
repo_root=repo_root,
|
|
189
248
|
settings=settings,
|
|
190
|
-
answer=content_text,
|
|
249
|
+
answer="" if needs_source else content_text,
|
|
191
250
|
citations=citations,
|
|
192
251
|
turns_used=turn,
|
|
193
252
|
truncated=truncated,
|
|
@@ -219,8 +278,12 @@ def explore(
|
|
|
219
278
|
recorder.finish(result=result)
|
|
220
279
|
return result
|
|
221
280
|
|
|
222
|
-
|
|
223
|
-
|
|
281
|
+
fresh_calls: dict[str, ToolCall] = {}
|
|
282
|
+
for tool_call in tool_calls:
|
|
283
|
+
fingerprint = _tool_call_fingerprint(tool_call)
|
|
284
|
+
if fingerprint not in evidence.observations:
|
|
285
|
+
fresh_calls.setdefault(fingerprint, tool_call)
|
|
286
|
+
if not fresh_calls:
|
|
224
287
|
_append_warning_once(
|
|
225
288
|
warnings,
|
|
226
289
|
"repeated tool call stopped exploration",
|
|
@@ -238,17 +301,18 @@ def explore(
|
|
|
238
301
|
)
|
|
239
302
|
recorder.finish(result=result)
|
|
240
303
|
return result
|
|
241
|
-
for tool_call in tool_calls:
|
|
242
|
-
evidence.seen_tool_calls.add(_tool_call_fingerprint(tool_call))
|
|
243
|
-
|
|
244
304
|
messages.append(_assistant_tool_message(message))
|
|
245
305
|
observations = _execute_tool_calls_parallel(
|
|
246
|
-
|
|
306
|
+
list(fresh_calls.values()),
|
|
247
307
|
repo_root=repo_root,
|
|
248
308
|
settings=settings,
|
|
249
309
|
)
|
|
250
|
-
|
|
251
|
-
|
|
310
|
+
evidence.observations.update(zip(fresh_calls, observations, strict=True))
|
|
311
|
+
for tool_call in tool_calls:
|
|
312
|
+
observation = replace(
|
|
313
|
+
evidence.observations[_tool_call_fingerprint(tool_call)],
|
|
314
|
+
tool_call_id=tool_call.id,
|
|
315
|
+
)
|
|
252
316
|
payload = _model_observation_payload(
|
|
253
317
|
observation,
|
|
254
318
|
max_chars=settings.max_observation_chars,
|
|
@@ -262,7 +326,9 @@ def explore(
|
|
|
262
326
|
and observation.path
|
|
263
327
|
and observation.line_range
|
|
264
328
|
):
|
|
265
|
-
|
|
329
|
+
read = replace(observation, tool_call_id=None)
|
|
330
|
+
if read not in evidence.reads:
|
|
331
|
+
evidence.reads.append(read)
|
|
266
332
|
recorder.record_observation(turn, tool_call.name, observation)
|
|
267
333
|
messages.append(
|
|
268
334
|
{
|
|
@@ -275,6 +341,8 @@ def explore(
|
|
|
275
341
|
),
|
|
276
342
|
}
|
|
277
343
|
)
|
|
344
|
+
if needs_source:
|
|
345
|
+
messages.append(_source_feedback())
|
|
278
346
|
if last_content:
|
|
279
347
|
citations = _validated_citations_from_content(
|
|
280
348
|
last_content,
|
|
@@ -286,6 +354,8 @@ def explore(
|
|
|
286
354
|
last_content,
|
|
287
355
|
evidence.reads,
|
|
288
356
|
)
|
|
357
|
+
if evidence.symbol:
|
|
358
|
+
citations = _best_current_citations(evidence)
|
|
289
359
|
_append_warning_once(warnings, "max turns exceeded after partial answer")
|
|
290
360
|
if request.citation and not citations:
|
|
291
361
|
_append_warning_once(
|
|
@@ -417,7 +487,17 @@ def _finalize_result(
|
|
|
417
487
|
truncated: bool,
|
|
418
488
|
warnings: list[str],
|
|
419
489
|
) -> ExploreResult:
|
|
490
|
+
if _query_symbol(request.query) and not citations:
|
|
491
|
+
answer = NO_CITATIONS_FOUND
|
|
492
|
+
_append_warning_once(warnings, "query symbol not found in verified evidence")
|
|
420
493
|
citations = _merge_adjacent_citations(citations)[:MAX_FINAL_CITATIONS]
|
|
494
|
+
if _implementation_query(request.query) and _documentation_only(citations):
|
|
495
|
+
truncated = True
|
|
496
|
+
answer = _citation_text(citations)
|
|
497
|
+
_append_warning_once(
|
|
498
|
+
warnings,
|
|
499
|
+
"implementation source not verified; returning documentation evidence",
|
|
500
|
+
)
|
|
421
501
|
if request.citation or (not answer and citations):
|
|
422
502
|
answer = _citation_text(citations)
|
|
423
503
|
raw_locations = _extract_raw_locations(
|
|
@@ -551,6 +631,7 @@ def _try_exact_fast_path(
|
|
|
551
631
|
*,
|
|
552
632
|
repo_root: Path,
|
|
553
633
|
settings: Settings,
|
|
634
|
+
evidence: EvidenceState,
|
|
554
635
|
) -> ExploreResult | None:
|
|
555
636
|
bare_path = request.query.strip()
|
|
556
637
|
if len(bare_path) >= 2 and bare_path[0] in "`'\"" and bare_path[-1] == bare_path[0]:
|
|
@@ -581,7 +662,6 @@ def _try_exact_fast_path(
|
|
|
581
662
|
)
|
|
582
663
|
if not _is_exact_lookup(request.query):
|
|
583
664
|
return None
|
|
584
|
-
evidence = EvidenceState()
|
|
585
665
|
warnings: list[str] = []
|
|
586
666
|
citations = _validated_citations_from_content(
|
|
587
667
|
request.query,
|
|
@@ -614,6 +694,13 @@ def _try_exact_fast_path(
|
|
|
614
694
|
target=target,
|
|
615
695
|
allow_assignment=True,
|
|
616
696
|
)
|
|
697
|
+
citations = _validate_citations(
|
|
698
|
+
citations,
|
|
699
|
+
repo_root=repo_root,
|
|
700
|
+
settings=settings,
|
|
701
|
+
evidence=evidence,
|
|
702
|
+
warnings=warnings,
|
|
703
|
+
)
|
|
617
704
|
if citations:
|
|
618
705
|
return _finalize_result(
|
|
619
706
|
request=request,
|
|
@@ -630,25 +717,77 @@ def _try_exact_fast_path(
|
|
|
630
717
|
symbol = _extract_pathless_symbol(request.query)
|
|
631
718
|
if symbol is None:
|
|
632
719
|
return None
|
|
633
|
-
|
|
720
|
+
scan = _scan_definitions(
|
|
634
721
|
repo_root=repo_root,
|
|
635
722
|
settings=settings,
|
|
636
723
|
symbol=symbol,
|
|
637
724
|
)
|
|
638
|
-
|
|
725
|
+
evidence.definition_scan = scan
|
|
726
|
+
if scan.status != "unique":
|
|
639
727
|
return None
|
|
640
728
|
return _finalize_result(
|
|
641
729
|
request=request,
|
|
642
730
|
repo_root=repo_root,
|
|
643
731
|
settings=settings,
|
|
644
732
|
answer="",
|
|
645
|
-
citations=
|
|
733
|
+
citations=list(scan.citations),
|
|
646
734
|
turns_used=0,
|
|
647
735
|
truncated=False,
|
|
648
736
|
warnings=[],
|
|
649
737
|
)
|
|
650
738
|
|
|
651
739
|
|
|
740
|
+
def _query_symbol(query: str) -> str | None:
|
|
741
|
+
if not _is_exact_lookup(query) or extract_citations(query):
|
|
742
|
+
return None
|
|
743
|
+
path = _extract_explicit_path(query)
|
|
744
|
+
target = (
|
|
745
|
+
_extract_exact_target(query, path) if path else _extract_pathless_symbol(query)
|
|
746
|
+
)
|
|
747
|
+
if target and re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", target):
|
|
748
|
+
return target
|
|
749
|
+
return None
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def _symbol_in_range(
|
|
753
|
+
citation: Citation,
|
|
754
|
+
reads: list[ToolObservation],
|
|
755
|
+
symbol: str | None,
|
|
756
|
+
) -> bool:
|
|
757
|
+
if symbol is None:
|
|
758
|
+
return True
|
|
759
|
+
pattern = re.compile(rf"\b{re.escape(symbol)}\b")
|
|
760
|
+
for read in reads:
|
|
761
|
+
if not _citation_has_read_coverage(citation, [read]) or read.content is None:
|
|
762
|
+
continue
|
|
763
|
+
start, _ = _parse_line_range(read.line_range or "")
|
|
764
|
+
assert start is not None and citation.start_line is not None
|
|
765
|
+
assert citation.end_line is not None
|
|
766
|
+
lines = read.content.split("\n")
|
|
767
|
+
lines = [line + "\n" for line in lines[:-1]] + lines[-1:]
|
|
768
|
+
text = "".join(
|
|
769
|
+
lines[citation.start_line - start : citation.end_line - start + 1]
|
|
770
|
+
)
|
|
771
|
+
for match in pattern.finditer(text):
|
|
772
|
+
# A byte-capped trailing token has no verified right boundary.
|
|
773
|
+
if match.end() < len(text) or not read.truncated or text.endswith("\n"):
|
|
774
|
+
return True
|
|
775
|
+
return False
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def _eligible_reads(evidence: EvidenceState) -> list[ToolObservation]:
|
|
779
|
+
if evidence.symbol is None:
|
|
780
|
+
return evidence.reads
|
|
781
|
+
return [
|
|
782
|
+
read
|
|
783
|
+
for read in evidence.reads
|
|
784
|
+
if any(
|
|
785
|
+
_symbol_in_range(citation, [read], evidence.symbol)
|
|
786
|
+
for citation in _citations_from_reads([read])
|
|
787
|
+
)
|
|
788
|
+
]
|
|
789
|
+
|
|
790
|
+
|
|
652
791
|
def _is_exact_lookup(query: str) -> bool:
|
|
653
792
|
# Admit a small lookup vocabulary; any unexplained intent goes to the model.
|
|
654
793
|
paths = list(EXPLICIT_PATH_RE.finditer(query))
|
|
@@ -769,12 +908,12 @@ def _exact_matches_in_file(
|
|
|
769
908
|
return citations if 0 < len(citations) <= MAX_FINAL_CITATIONS else []
|
|
770
909
|
|
|
771
910
|
|
|
772
|
-
def
|
|
911
|
+
def _scan_definitions(
|
|
773
912
|
*,
|
|
774
913
|
repo_root: Path,
|
|
775
914
|
settings: Settings,
|
|
776
915
|
symbol: str,
|
|
777
|
-
) ->
|
|
916
|
+
) -> _DefinitionScan:
|
|
778
917
|
definition_pattern = rf"^\s*(?:async\s+def|def|class)\s+{re.escape(symbol)}\b"
|
|
779
918
|
grep_result = repo_grep(
|
|
780
919
|
repo_root=repo_root,
|
|
@@ -797,9 +936,16 @@ def _unique_definition_citation(
|
|
|
797
936
|
for result in (grep_result, pyi_result)
|
|
798
937
|
for hit in result.hits
|
|
799
938
|
]
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
939
|
+
status: Literal["unique", "missing", "ambiguous", "incomplete"]
|
|
940
|
+
if grep_result.truncated or pyi_result.truncated:
|
|
941
|
+
status = "incomplete"
|
|
942
|
+
elif len(citations) > 1:
|
|
943
|
+
status = "ambiguous"
|
|
944
|
+
else:
|
|
945
|
+
status = "unique" if citations else "missing"
|
|
946
|
+
return _DefinitionScan(
|
|
947
|
+
tuple(citations), status, tuple(grep_result.warnings + pyi_result.warnings)
|
|
948
|
+
)
|
|
803
949
|
|
|
804
950
|
|
|
805
951
|
def _grep_exact_target(
|
|
@@ -890,7 +1036,8 @@ def _validate_citations(
|
|
|
890
1036
|
if normalized is None:
|
|
891
1037
|
continue
|
|
892
1038
|
if _citation_has_read_coverage(normalized, evidence.reads):
|
|
893
|
-
|
|
1039
|
+
if _symbol_in_range(normalized, evidence.reads, evidence.symbol):
|
|
1040
|
+
validated.append(normalized)
|
|
894
1041
|
continue
|
|
895
1042
|
verified = _verify_citation_by_read(
|
|
896
1043
|
normalized,
|
|
@@ -899,7 +1046,9 @@ def _validate_citations(
|
|
|
899
1046
|
evidence=evidence,
|
|
900
1047
|
warnings=warnings,
|
|
901
1048
|
)
|
|
902
|
-
if verified is not None
|
|
1049
|
+
if verified is not None and _symbol_in_range(
|
|
1050
|
+
verified, evidence.reads, evidence.symbol
|
|
1051
|
+
):
|
|
903
1052
|
validated.append(verified)
|
|
904
1053
|
return _merge_adjacent_citations(_dedupe_citations(validated))[:MAX_FINAL_CITATIONS]
|
|
905
1054
|
|
|
@@ -1016,9 +1165,23 @@ def _dedupe_citations(citations: list[Citation]) -> list[Citation]:
|
|
|
1016
1165
|
|
|
1017
1166
|
|
|
1018
1167
|
def _best_current_citations(evidence: EvidenceState) -> list[Citation]:
|
|
1168
|
+
reads = _eligible_reads(evidence)
|
|
1169
|
+
if evidence.prefer_source:
|
|
1170
|
+
source_reads = [
|
|
1171
|
+
read for read in reads if not _documentation_path(read.path or "")
|
|
1172
|
+
]
|
|
1173
|
+
source_citations = [
|
|
1174
|
+
citation
|
|
1175
|
+
for citation in evidence.citations
|
|
1176
|
+
if not _documentation_path(citation.path)
|
|
1177
|
+
]
|
|
1178
|
+
if source_citations or source_reads:
|
|
1179
|
+
return source_citations[:MAX_FINAL_CITATIONS] or _citations_from_reads(
|
|
1180
|
+
source_reads
|
|
1181
|
+
)
|
|
1019
1182
|
if evidence.citations:
|
|
1020
1183
|
return evidence.citations[:MAX_FINAL_CITATIONS]
|
|
1021
|
-
return _citations_from_reads(
|
|
1184
|
+
return _citations_from_reads(reads)
|
|
1022
1185
|
|
|
1023
1186
|
|
|
1024
1187
|
def _citations_from_reads(observations: list[ToolObservation]) -> list[Citation]:
|
|
@@ -1070,7 +1233,7 @@ def _best_narrow_read_citation(
|
|
|
1070
1233
|
evidence: EvidenceState,
|
|
1071
1234
|
settings: Settings,
|
|
1072
1235
|
) -> Citation | None:
|
|
1073
|
-
for citation in _citations_from_reads(evidence
|
|
1236
|
+
for citation in _citations_from_reads(_eligible_reads(evidence)):
|
|
1074
1237
|
for observation in evidence.reads:
|
|
1075
1238
|
start, end = _parse_line_range(observation.line_range or "")
|
|
1076
1239
|
if (
|
|
@@ -1120,17 +1283,44 @@ def _tool_call_fingerprint(tool_call: ToolCall) -> str:
|
|
|
1120
1283
|
)
|
|
1121
1284
|
|
|
1122
1285
|
|
|
1123
|
-
def
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
)
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1286
|
+
def _implementation_query(query: str) -> bool:
|
|
1287
|
+
if _is_exact_lookup(query) or re.search(
|
|
1288
|
+
r"\b(?:docs?|documentation|markdown|readme)\b", query, re.IGNORECASE
|
|
1289
|
+
):
|
|
1290
|
+
return False
|
|
1291
|
+
return bool(
|
|
1292
|
+
re.search(
|
|
1293
|
+
r"\b(?:implement\w*|handl\w*|code|functions?|methods?|classes|class)\b",
|
|
1294
|
+
query,
|
|
1295
|
+
re.IGNORECASE,
|
|
1296
|
+
)
|
|
1297
|
+
)
|
|
1298
|
+
|
|
1299
|
+
|
|
1300
|
+
def _documentation_path(path: str) -> bool:
|
|
1301
|
+
name = Path(path).name.lower()
|
|
1302
|
+
return Path(name).suffix in {".md", ".mdx", ".rst", ".txt", ".adoc"} or name in {
|
|
1303
|
+
"readme",
|
|
1304
|
+
"changelog",
|
|
1305
|
+
"contributing",
|
|
1306
|
+
"license",
|
|
1307
|
+
"authors",
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
def _documentation_only(citations: list[Citation]) -> bool:
|
|
1312
|
+
return bool(citations) and all(_documentation_path(c.path) for c in citations)
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
def _source_feedback() -> dict[str, str]:
|
|
1316
|
+
return {
|
|
1317
|
+
"role": "user",
|
|
1318
|
+
"content": (
|
|
1319
|
+
"These citations only establish documentation evidence. Continue searching "
|
|
1320
|
+
"for implementation source using fresh, focused read-only tool calls. "
|
|
1321
|
+
"Do not repeat previous calls or bypass denied paths."
|
|
1322
|
+
),
|
|
1323
|
+
}
|
|
1134
1324
|
|
|
1135
1325
|
|
|
1136
1326
|
def _message_has_tool_calls(message: dict[str, Any]) -> bool:
|
|
@@ -1245,6 +1435,8 @@ def _initial_messages(
|
|
|
1245
1435
|
"tool calls in the same turn. "
|
|
1246
1436
|
"For exact symbol, function, or class names, use repo_grep "
|
|
1247
1437
|
"before reading broad file ranges. "
|
|
1438
|
+
"Exact-symbol citations must contain the whole case-sensitive "
|
|
1439
|
+
"query token inside the cited lines. "
|
|
1248
1440
|
"For implementation questions, prefer implementation source over "
|
|
1249
1441
|
"documentation or Markdown; use documentation when requested or "
|
|
1250
1442
|
"needed to support the answer. Cite the smallest line ranges "
|
|
@@ -6,6 +6,7 @@ from importlib import import_module
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
+
from flash_context import __version__
|
|
9
10
|
from flash_context.agent import explore
|
|
10
11
|
from flash_context.config import Settings, SettingsOverrides, load_settings
|
|
11
12
|
from flash_context.types import ExploreRequest, ExplorerError, ExploreResult
|
|
@@ -52,6 +53,8 @@ def create_server() -> Any:
|
|
|
52
53
|
) from exc
|
|
53
54
|
fast_mcp = fastmcp_module.FastMCP
|
|
54
55
|
server = fast_mcp("flash-context")
|
|
56
|
+
# FastMCP exposes no version argument; the low-level default is the SDK version.
|
|
57
|
+
server._mcp_server.version = __version__
|
|
55
58
|
|
|
56
59
|
@server.tool() # type: ignore[untyped-decorator]
|
|
57
60
|
def explore_repository(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|