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.
Files changed (21) hide show
  1. {flash_context-0.3.0 → flash_context-0.3.1}/CHANGELOG.md +22 -0
  2. {flash_context-0.3.0 → flash_context-0.3.1}/PKG-INFO +7 -3
  3. {flash_context-0.3.0 → flash_context-0.3.1}/README.md +6 -2
  4. {flash_context-0.3.0 → flash_context-0.3.1}/pyproject.toml +1 -1
  5. {flash_context-0.3.0 → flash_context-0.3.1}/src/flash_context/agent.py +93 -24
  6. {flash_context-0.3.0 → flash_context-0.3.1}/src/flash_context/mcp_server.py +3 -0
  7. {flash_context-0.3.0 → flash_context-0.3.1}/.env.example +0 -0
  8. {flash_context-0.3.0 → flash_context-0.3.1}/.gitignore +0 -0
  9. {flash_context-0.3.0 → flash_context-0.3.1}/LICENSE +0 -0
  10. {flash_context-0.3.0 → flash_context-0.3.1}/config.yaml.example +0 -0
  11. {flash_context-0.3.0 → flash_context-0.3.1}/src/flash_context/__init__.py +0 -0
  12. {flash_context-0.3.0 → flash_context-0.3.1}/src/flash_context/cli.py +0 -0
  13. {flash_context-0.3.0 → flash_context-0.3.1}/src/flash_context/config.py +0 -0
  14. {flash_context-0.3.0 → flash_context-0.3.1}/src/flash_context/llm.py +0 -0
  15. {flash_context-0.3.0 → flash_context-0.3.1}/src/flash_context/logging.py +0 -0
  16. {flash_context-0.3.0 → flash_context-0.3.1}/src/flash_context/tools/__init__.py +0 -0
  17. {flash_context-0.3.0 → flash_context-0.3.1}/src/flash_context/tools/glob.py +0 -0
  18. {flash_context-0.3.0 → flash_context-0.3.1}/src/flash_context/tools/grep.py +0 -0
  19. {flash_context-0.3.0 → flash_context-0.3.1}/src/flash_context/tools/read.py +0 -0
  20. {flash_context-0.3.0 → flash_context-0.3.1}/src/flash_context/tools/safety.py +0 -0
  21. {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.0
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
  [![Python versions](https://img.shields.io/pypi/pyversions/flash-context)](https://pypi.org/project/flash-context/)
29
29
  [![License](https://img.shields.io/pypi/l/flash-context)](LICENSE)
30
30
 
31
- Read-only context exploration and retrieval for AI agents,
32
- available as a Python CLI and Model Context Protocol (MCP) server.
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
  [![Python versions](https://img.shields.io/pypi/pyversions/flash-context)](https://pypi.org/project/flash-context/)
6
6
  [![License](https://img.shields.io/pypi/l/flash-context)](LICENSE)
7
7
 
8
- Read-only context exploration and retrieval for AI agents,
9
- available as a Python CLI and Model Context Protocol (MCP) server.
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flash-context"
3
- version = "0.3.0"
3
+ version = "0.3.1"
4
4
  description = "Read-only context exploration and retrieval for AI agents, starting with codebases and repositories."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -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
- seen_tool_calls: set[str] = field(default_factory=set)
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
- repeated_call = _first_repeated_tool_call(evidence, tool_calls)
223
- if repeated_call is not None:
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
- tool_calls,
260
+ list(fresh_calls.values()),
247
261
  repo_root=repo_root,
248
262
  settings=settings,
249
263
  )
250
- for tool_call, observation in zip(tool_calls, observations, strict=True):
251
- observation.tool_call_id = tool_call.id
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
- evidence.reads.append(observation)
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 _first_repeated_tool_call(
1124
- evidence: EvidenceState,
1125
- tool_calls: list[ToolCall],
1126
- ) -> ToolCall | None:
1127
- batch_fingerprints: set[str] = set()
1128
- for tool_call in tool_calls:
1129
- fingerprint = _tool_call_fingerprint(tool_call)
1130
- if fingerprint in evidence.seen_tool_calls or fingerprint in batch_fingerprints:
1131
- return tool_call
1132
- batch_fingerprints.add(fingerprint)
1133
- return None
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