flash-context 0.3.0__tar.gz → 0.3.1__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.1}/CHANGELOG.md +22 -0
- {flash_context-0.3.0 → flash_context-0.3.1}/PKG-INFO +7 -3
- {flash_context-0.3.0 → flash_context-0.3.1}/README.md +6 -2
- {flash_context-0.3.0 → flash_context-0.3.1}/pyproject.toml +1 -1
- {flash_context-0.3.0 → flash_context-0.3.1}/src/flash_context/agent.py +93 -24
- {flash_context-0.3.0 → flash_context-0.3.1}/src/flash_context/mcp_server.py +3 -0
- {flash_context-0.3.0 → flash_context-0.3.1}/.env.example +0 -0
- {flash_context-0.3.0 → flash_context-0.3.1}/.gitignore +0 -0
- {flash_context-0.3.0 → flash_context-0.3.1}/LICENSE +0 -0
- {flash_context-0.3.0 → flash_context-0.3.1}/config.yaml.example +0 -0
- {flash_context-0.3.0 → flash_context-0.3.1}/src/flash_context/__init__.py +0 -0
- {flash_context-0.3.0 → flash_context-0.3.1}/src/flash_context/cli.py +0 -0
- {flash_context-0.3.0 → flash_context-0.3.1}/src/flash_context/config.py +0 -0
- {flash_context-0.3.0 → flash_context-0.3.1}/src/flash_context/llm.py +0 -0
- {flash_context-0.3.0 → flash_context-0.3.1}/src/flash_context/logging.py +0 -0
- {flash_context-0.3.0 → flash_context-0.3.1}/src/flash_context/tools/__init__.py +0 -0
- {flash_context-0.3.0 → flash_context-0.3.1}/src/flash_context/tools/glob.py +0 -0
- {flash_context-0.3.0 → flash_context-0.3.1}/src/flash_context/tools/grep.py +0 -0
- {flash_context-0.3.0 → flash_context-0.3.1}/src/flash_context/tools/read.py +0 -0
- {flash_context-0.3.0 → flash_context-0.3.1}/src/flash_context/tools/safety.py +0 -0
- {flash_context-0.3.0 → flash_context-0.3.1}/src/flash_context/types.py +0 -0
|
@@ -4,6 +4,28 @@ User-visible changes and migration instructions for each release.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.3.1] - 2026-11-01
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Report the flash-context package version during MCP initialization instead of
|
|
12
|
+
the MCP SDK version.
|
|
13
|
+
- Reuse repeated tool observations without discarding fresh searches in the same
|
|
14
|
+
batch; execute same-turn duplicates only once and stop all-repeat stalls.
|
|
15
|
+
- Continue implementation searches after documentation-only citations within the
|
|
16
|
+
existing turn budget. Documentation-only fallbacks retain citations and snippets
|
|
17
|
+
with explicit incomplete-result metadata and warnings.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Describe deterministic lookup and delegated model search together in the README.
|
|
22
|
+
|
|
23
|
+
### Migration
|
|
24
|
+
|
|
25
|
+
- Implementation questions may use additional turns before returning documentation
|
|
26
|
+
evidence. These fallbacks set `truncated: true`; partial cited CLI results still
|
|
27
|
+
exit 0. Budgets, configuration keys, and result schemas are unchanged.
|
|
28
|
+
|
|
7
29
|
## [0.3.0] - 2026-11-01
|
|
8
30
|
|
|
9
31
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: flash-context
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
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,8 +28,9 @@ 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 that combines deterministic
|
|
32
|
+
lookup with delegated LLM search to deliver compact, validated source evidence.
|
|
33
|
+
Available as a Python CLI and Model Context Protocol (MCP) server.
|
|
33
34
|
|
|
34
35
|
Delegate recursive, repetitive file search and reading to a utility model
|
|
35
36
|
behind an OpenAI-compatible endpoint. The calling agent receives validated
|
|
@@ -41,6 +42,9 @@ exploration core with read-only tools, path safety, and resource limits.
|
|
|
41
42
|
Scans and regex execution are bounded; partial results report truncation.
|
|
42
43
|
Citation validation checks safe paths and existing line coverage, not semantic
|
|
43
44
|
relevance or minimal ranges. Citation precision depends on the explorer model.
|
|
45
|
+
Implementation queries continue past documentation-only citations within the turn budget; documentation-only
|
|
46
|
+
fallbacks carry incomplete-result metadata and warnings. Build outputs such as
|
|
47
|
+
`dist/` remain excluded; inspect a source checkout for implementation evidence.
|
|
44
48
|
Optional metadata-only trajectory logs must stay outside the inspected repository.
|
|
45
49
|
|
|
46
50
|
Inspired by [FastContext](https://arxiv.org/abs/2606.14066), a now-withdrawn paper
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
[](https://pypi.org/project/flash-context/)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
A lightweight, model-agnostic repository explorer that combines deterministic
|
|
9
|
+
lookup with delegated LLM search to deliver compact, validated source evidence.
|
|
10
|
+
Available as a Python CLI and Model Context Protocol (MCP) server.
|
|
10
11
|
|
|
11
12
|
Delegate recursive, repetitive file search and reading to a utility model
|
|
12
13
|
behind an OpenAI-compatible endpoint. The calling agent receives validated
|
|
@@ -18,6 +19,9 @@ exploration core with read-only tools, path safety, and resource limits.
|
|
|
18
19
|
Scans and regex execution are bounded; partial results report truncation.
|
|
19
20
|
Citation validation checks safe paths and existing line coverage, not semantic
|
|
20
21
|
relevance or minimal ranges. Citation precision depends on the explorer model.
|
|
22
|
+
Implementation queries continue past documentation-only citations within the turn budget; documentation-only
|
|
23
|
+
fallbacks carry incomplete-result metadata and warnings. Build outputs such as
|
|
24
|
+
`dist/` remain excluded; inspect a source checkout for implementation evidence.
|
|
21
25
|
Optional metadata-only trajectory logs must stay outside the inspected repository.
|
|
22
26
|
|
|
23
27
|
Inspired by [FastContext](https://arxiv.org/abs/2606.14066), a now-withdrawn paper
|
|
@@ -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
|
|
|
@@ -66,7 +66,8 @@ TOKEN_RE = re.compile(r"\b(?P<token>[A-Za-z_][A-Za-z0-9_]*)\b")
|
|
|
66
66
|
class EvidenceState:
|
|
67
67
|
reads: list[ToolObservation] = field(default_factory=list)
|
|
68
68
|
citations: list[Citation] = field(default_factory=list)
|
|
69
|
-
|
|
69
|
+
observations: dict[str, ToolObservation] = field(default_factory=dict)
|
|
70
|
+
prefer_source: bool = False
|
|
70
71
|
|
|
71
72
|
|
|
72
73
|
def explore(
|
|
@@ -103,7 +104,7 @@ def explore(
|
|
|
103
104
|
warnings = list(file_sample.warnings)
|
|
104
105
|
truncated = bool(warnings)
|
|
105
106
|
last_content = ""
|
|
106
|
-
evidence = EvidenceState()
|
|
107
|
+
evidence = EvidenceState(prefer_source=_implementation_query(request.query))
|
|
107
108
|
|
|
108
109
|
try:
|
|
109
110
|
for turn in range(1, effective_max_turns + 1):
|
|
@@ -145,6 +146,7 @@ def explore(
|
|
|
145
146
|
recorder.finish(result=result)
|
|
146
147
|
return result
|
|
147
148
|
recorder.record_model_turn(turn, message)
|
|
149
|
+
needs_source = False
|
|
148
150
|
content_text = str(message.get("content") or "").strip()
|
|
149
151
|
if content_text:
|
|
150
152
|
last_content = content_text
|
|
@@ -157,6 +159,12 @@ def explore(
|
|
|
157
159
|
)
|
|
158
160
|
if citations:
|
|
159
161
|
evidence.citations = citations
|
|
162
|
+
needs_source = evidence.prefer_source and _documentation_only(
|
|
163
|
+
citations
|
|
164
|
+
)
|
|
165
|
+
if needs_source:
|
|
166
|
+
last_content = ""
|
|
167
|
+
if citations and not needs_source:
|
|
160
168
|
if _message_has_tool_calls(message):
|
|
161
169
|
_append_warning_once(
|
|
162
170
|
warnings,
|
|
@@ -178,6 +186,11 @@ def explore(
|
|
|
178
186
|
tool_calls = _extract_tool_calls(message)
|
|
179
187
|
if not tool_calls:
|
|
180
188
|
citations = _best_current_citations(evidence)
|
|
189
|
+
if evidence.prefer_source and _documentation_only(citations):
|
|
190
|
+
last_content = ""
|
|
191
|
+
messages.append({"role": "assistant", "content": content_text})
|
|
192
|
+
messages.append(_source_feedback())
|
|
193
|
+
continue
|
|
181
194
|
if request.citation and content_text != NO_CITATIONS_FOUND:
|
|
182
195
|
_append_warning_once(
|
|
183
196
|
warnings,
|
|
@@ -187,7 +200,7 @@ def explore(
|
|
|
187
200
|
request=request,
|
|
188
201
|
repo_root=repo_root,
|
|
189
202
|
settings=settings,
|
|
190
|
-
answer=content_text,
|
|
203
|
+
answer="" if needs_source else content_text,
|
|
191
204
|
citations=citations,
|
|
192
205
|
turns_used=turn,
|
|
193
206
|
truncated=truncated,
|
|
@@ -219,8 +232,12 @@ def explore(
|
|
|
219
232
|
recorder.finish(result=result)
|
|
220
233
|
return result
|
|
221
234
|
|
|
222
|
-
|
|
223
|
-
|
|
235
|
+
fresh_calls: dict[str, ToolCall] = {}
|
|
236
|
+
for tool_call in tool_calls:
|
|
237
|
+
fingerprint = _tool_call_fingerprint(tool_call)
|
|
238
|
+
if fingerprint not in evidence.observations:
|
|
239
|
+
fresh_calls.setdefault(fingerprint, tool_call)
|
|
240
|
+
if not fresh_calls:
|
|
224
241
|
_append_warning_once(
|
|
225
242
|
warnings,
|
|
226
243
|
"repeated tool call stopped exploration",
|
|
@@ -238,17 +255,18 @@ def explore(
|
|
|
238
255
|
)
|
|
239
256
|
recorder.finish(result=result)
|
|
240
257
|
return result
|
|
241
|
-
for tool_call in tool_calls:
|
|
242
|
-
evidence.seen_tool_calls.add(_tool_call_fingerprint(tool_call))
|
|
243
|
-
|
|
244
258
|
messages.append(_assistant_tool_message(message))
|
|
245
259
|
observations = _execute_tool_calls_parallel(
|
|
246
|
-
|
|
260
|
+
list(fresh_calls.values()),
|
|
247
261
|
repo_root=repo_root,
|
|
248
262
|
settings=settings,
|
|
249
263
|
)
|
|
250
|
-
|
|
251
|
-
|
|
264
|
+
evidence.observations.update(zip(fresh_calls, observations, strict=True))
|
|
265
|
+
for tool_call in tool_calls:
|
|
266
|
+
observation = replace(
|
|
267
|
+
evidence.observations[_tool_call_fingerprint(tool_call)],
|
|
268
|
+
tool_call_id=tool_call.id,
|
|
269
|
+
)
|
|
252
270
|
payload = _model_observation_payload(
|
|
253
271
|
observation,
|
|
254
272
|
max_chars=settings.max_observation_chars,
|
|
@@ -262,7 +280,9 @@ def explore(
|
|
|
262
280
|
and observation.path
|
|
263
281
|
and observation.line_range
|
|
264
282
|
):
|
|
265
|
-
|
|
283
|
+
read = replace(observation, tool_call_id=None)
|
|
284
|
+
if read not in evidence.reads:
|
|
285
|
+
evidence.reads.append(read)
|
|
266
286
|
recorder.record_observation(turn, tool_call.name, observation)
|
|
267
287
|
messages.append(
|
|
268
288
|
{
|
|
@@ -275,6 +295,8 @@ def explore(
|
|
|
275
295
|
),
|
|
276
296
|
}
|
|
277
297
|
)
|
|
298
|
+
if needs_source:
|
|
299
|
+
messages.append(_source_feedback())
|
|
278
300
|
if last_content:
|
|
279
301
|
citations = _validated_citations_from_content(
|
|
280
302
|
last_content,
|
|
@@ -418,6 +440,13 @@ def _finalize_result(
|
|
|
418
440
|
warnings: list[str],
|
|
419
441
|
) -> ExploreResult:
|
|
420
442
|
citations = _merge_adjacent_citations(citations)[:MAX_FINAL_CITATIONS]
|
|
443
|
+
if _implementation_query(request.query) and _documentation_only(citations):
|
|
444
|
+
truncated = True
|
|
445
|
+
answer = _citation_text(citations)
|
|
446
|
+
_append_warning_once(
|
|
447
|
+
warnings,
|
|
448
|
+
"implementation source not verified; returning documentation evidence",
|
|
449
|
+
)
|
|
421
450
|
if request.citation or (not answer and citations):
|
|
422
451
|
answer = _citation_text(citations)
|
|
423
452
|
raw_locations = _extract_raw_locations(
|
|
@@ -1016,6 +1045,19 @@ def _dedupe_citations(citations: list[Citation]) -> list[Citation]:
|
|
|
1016
1045
|
|
|
1017
1046
|
|
|
1018
1047
|
def _best_current_citations(evidence: EvidenceState) -> list[Citation]:
|
|
1048
|
+
if evidence.prefer_source:
|
|
1049
|
+
source_reads = [
|
|
1050
|
+
read for read in evidence.reads if not _documentation_path(read.path or "")
|
|
1051
|
+
]
|
|
1052
|
+
source_citations = [
|
|
1053
|
+
citation
|
|
1054
|
+
for citation in evidence.citations
|
|
1055
|
+
if not _documentation_path(citation.path)
|
|
1056
|
+
]
|
|
1057
|
+
if source_citations or source_reads:
|
|
1058
|
+
return source_citations[:MAX_FINAL_CITATIONS] or _citations_from_reads(
|
|
1059
|
+
source_reads
|
|
1060
|
+
)
|
|
1019
1061
|
if evidence.citations:
|
|
1020
1062
|
return evidence.citations[:MAX_FINAL_CITATIONS]
|
|
1021
1063
|
return _citations_from_reads(evidence.reads)
|
|
@@ -1120,17 +1162,44 @@ def _tool_call_fingerprint(tool_call: ToolCall) -> str:
|
|
|
1120
1162
|
)
|
|
1121
1163
|
|
|
1122
1164
|
|
|
1123
|
-
def
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
)
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1165
|
+
def _implementation_query(query: str) -> bool:
|
|
1166
|
+
if _is_exact_lookup(query) or re.search(
|
|
1167
|
+
r"\b(?:docs?|documentation|markdown|readme)\b", query, re.IGNORECASE
|
|
1168
|
+
):
|
|
1169
|
+
return False
|
|
1170
|
+
return bool(
|
|
1171
|
+
re.search(
|
|
1172
|
+
r"\b(?:implement\w*|handl\w*|code|functions?|methods?|classes|class)\b",
|
|
1173
|
+
query,
|
|
1174
|
+
re.IGNORECASE,
|
|
1175
|
+
)
|
|
1176
|
+
)
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
def _documentation_path(path: str) -> bool:
|
|
1180
|
+
name = Path(path).name.lower()
|
|
1181
|
+
return Path(name).suffix in {".md", ".mdx", ".rst", ".txt", ".adoc"} or name in {
|
|
1182
|
+
"readme",
|
|
1183
|
+
"changelog",
|
|
1184
|
+
"contributing",
|
|
1185
|
+
"license",
|
|
1186
|
+
"authors",
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
def _documentation_only(citations: list[Citation]) -> bool:
|
|
1191
|
+
return bool(citations) and all(_documentation_path(c.path) for c in citations)
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
def _source_feedback() -> dict[str, str]:
|
|
1195
|
+
return {
|
|
1196
|
+
"role": "user",
|
|
1197
|
+
"content": (
|
|
1198
|
+
"These citations only establish documentation evidence. Continue searching "
|
|
1199
|
+
"for implementation source using fresh, focused read-only tool calls. "
|
|
1200
|
+
"Do not repeat previous calls or bypass denied paths."
|
|
1201
|
+
),
|
|
1202
|
+
}
|
|
1134
1203
|
|
|
1135
1204
|
|
|
1136
1205
|
def _message_has_tool_calls(message: dict[str, Any]) -> bool:
|
|
@@ -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
|