gemcode 0.3.64__tar.gz → 0.3.66__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 (126) hide show
  1. {gemcode-0.3.64/src/gemcode.egg-info → gemcode-0.3.66}/PKG-INFO +1 -1
  2. {gemcode-0.3.64 → gemcode-0.3.66}/pyproject.toml +1 -1
  3. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/agent.py +20 -0
  4. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/callbacks.py +27 -3
  5. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/config.py +9 -2
  6. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/memory/embedding_memory_service.py +28 -2
  7. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/memory/file_memory_service.py +36 -1
  8. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/repl_commands.py +1 -1
  9. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/repl_slash.py +2 -2
  10. gemcode-0.3.66/src/gemcode/tool_result_store.py +162 -0
  11. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/__init__.py +26 -0
  12. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/bash.py +2 -2
  13. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/filesystem.py +1 -1
  14. gemcode-0.3.66/src/gemcode/tools/repo_map.py +132 -0
  15. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/search.py +1 -1
  16. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/shell.py +2 -2
  17. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/subtask.py +40 -3
  18. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/web.py +2 -2
  19. {gemcode-0.3.64 → gemcode-0.3.66/src/gemcode.egg-info}/PKG-INFO +1 -1
  20. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode.egg-info/SOURCES.txt +2 -0
  21. {gemcode-0.3.64 → gemcode-0.3.66}/LICENSE +0 -0
  22. {gemcode-0.3.64 → gemcode-0.3.66}/MANIFEST.in +0 -0
  23. {gemcode-0.3.64 → gemcode-0.3.66}/README.md +0 -0
  24. {gemcode-0.3.64 → gemcode-0.3.66}/setup.cfg +0 -0
  25. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/__init__.py +0 -0
  26. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/__main__.py +0 -0
  27. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/audit.py +0 -0
  28. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/autocompact.py +0 -0
  29. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/capability_routing.py +0 -0
  30. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/cli.py +0 -0
  31. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/compaction.py +0 -0
  32. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/computer_use/__init__.py +0 -0
  33. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/computer_use/browser_computer.py +0 -0
  34. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/context_budget.py +0 -0
  35. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/context_warning.py +0 -0
  36. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/credentials.py +0 -0
  37. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/hitl_session.py +0 -0
  38. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/hooks.py +0 -0
  39. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/intent_classifier.py +0 -0
  40. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/interactions.py +0 -0
  41. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/invoke.py +0 -0
  42. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/kairos_daemon.py +0 -0
  43. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/limits.py +0 -0
  44. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/live_audio_engine.py +0 -0
  45. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/logging_config.py +0 -0
  46. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/mcp_loader.py +0 -0
  47. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/memory/__init__.py +0 -0
  48. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/modality_tools.py +0 -0
  49. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/model_errors.py +0 -0
  50. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/model_routing.py +0 -0
  51. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/openapi_loader.py +0 -0
  52. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/paths.py +0 -0
  53. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/permissions.py +0 -0
  54. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/plugins/__init__.py +0 -0
  55. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  56. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  57. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/pricing.py +0 -0
  58. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/prompt_suggestions.py +0 -0
  59. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/query/__init__.py +0 -0
  60. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/query/config.py +0 -0
  61. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/query/deps.py +0 -0
  62. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/query/engine.py +0 -0
  63. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/query/stop_hooks.py +0 -0
  64. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/query/token_budget.py +0 -0
  65. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/query/transitions.py +0 -0
  66. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/refine.py +0 -0
  67. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/review_agent.py +0 -0
  68. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/session_runtime.py +0 -0
  69. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/session_store.py +0 -0
  70. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/slash_commands.py +0 -0
  71. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/thinking.py +0 -0
  72. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tool_prompt_manifest.py +0 -0
  73. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tool_registry.py +0 -0
  74. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/browser.py +0 -0
  75. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/edit.py +0 -0
  76. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/notebook.py +0 -0
  77. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/notes.py +0 -0
  78. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/shell_gate.py +0 -0
  79. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/tasks.py +0 -0
  80. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/think.py +0 -0
  81. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/todo.py +0 -0
  82. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/web_search.py +0 -0
  83. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools_inspector.py +0 -0
  84. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/trust.py +0 -0
  85. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tui/input_handler.py +0 -0
  86. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tui/scrollback.py +0 -0
  87. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tui/spinner.py +0 -0
  88. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tui/welcome_banner.py +0 -0
  89. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tui/welcome_rich.py +0 -0
  90. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/version.py +0 -0
  91. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/vertex.py +0 -0
  92. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/web/__init__.py +0 -0
  93. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/web/claude_sse_adapter.py +0 -0
  94. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/web/terminal_repl.py +0 -0
  95. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/workspace_hints.py +0 -0
  96. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode.egg-info/dependency_links.txt +0 -0
  97. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode.egg-info/entry_points.txt +0 -0
  98. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode.egg-info/requires.txt +0 -0
  99. {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode.egg-info/top_level.txt +0 -0
  100. {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_agent_instruction.py +0 -0
  101. {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_autocompact.py +0 -0
  102. {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_capability_routing.py +0 -0
  103. {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_claude_web_adapter_sse.py +0 -0
  104. {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_cli_init.py +0 -0
  105. {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_computer_use_permissions.py +0 -0
  106. {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_context_budget.py +0 -0
  107. {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_context_warning.py +0 -0
  108. {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_credentials.py +0 -0
  109. {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_interactive_permission_ask.py +0 -0
  110. {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_kairos_scheduler.py +0 -0
  111. {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_modality_tools.py +0 -0
  112. {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_model_error_retry.py +0 -0
  113. {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_model_errors.py +0 -0
  114. {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_model_routing.py +0 -0
  115. {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_paths.py +0 -0
  116. {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_permissions.py +0 -0
  117. {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_prompt_suggestions.py +0 -0
  118. {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_repl_commands.py +0 -0
  119. {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_repl_slash.py +0 -0
  120. {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_slash_commands.py +0 -0
  121. {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_thinking_config.py +0 -0
  122. {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_token_budget.py +0 -0
  123. {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_tool_context_circulation.py +0 -0
  124. {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_tools.py +0 -0
  125. {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_tools_inspector.py +0 -0
  126. {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_workspace_hints.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.64
3
+ Version: 0.3.66
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gemcode"
7
- version = "0.3.64"
7
+ version = "0.3.66"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -462,6 +462,14 @@ key_combination(["control+c"]) # copy
462
462
 
463
463
 
464
464
  def build_instruction(cfg: GemCodeConfig) -> str:
465
+ import os as _os
466
+ verbose_tools_guide = _os.environ.get("GEMCODE_VERBOSE_INSTRUCTIONS", "").lower() in (
467
+ "1",
468
+ "true",
469
+ "yes",
470
+ "on",
471
+ )
472
+
465
473
  base = f"""You are GemCode, an expert software engineering agent powered by Google Gemini.
466
474
  You run locally via the GemCode CLI. You are the same agent the user launched — not a hosted portal.
467
475
 
@@ -509,6 +517,17 @@ You have native deep thinking capability — use it actively:
509
517
  - When something fails, diagnose (re-read the error, check assumptions) before switching strategy. Do not repeat the same failed call.
510
518
  - When asked to analyse or explain something: read the actual files, produce concrete findings, not hypotheses.
511
519
 
520
+ ## Tool selection guide (only when needed)
521
+
522
+ Keep tool usage minimal. Prefer short, targeted calls and keep tool outputs small.
523
+ If you need more tool usage examples, set `GEMCODE_VERBOSE_INSTRUCTIONS=1`.
524
+
525
+ """
526
+
527
+ if not verbose_tools_guide:
528
+ return base.strip() + "\n"
529
+
530
+ tool_guide = r"""
512
531
  ## Tool selection guide
513
532
 
514
533
  ### Shell execution (critical — use these for real work)
@@ -864,6 +883,7 @@ def build_root_agent(
864
883
  pre-built list that excludes run_subtask itself, preventing recursion).
865
884
  When set, build_function_tools() is NOT called.
866
885
  """
886
+ return (base + tool_guide).strip() + "\n"
867
887
  if _tools is not None:
868
888
  tools = list(_tools)
869
889
  else:
@@ -311,6 +311,29 @@ def make_after_tool_callback(cfg: GemCodeConfig):
311
311
  tool_response: dict,
312
312
  ) -> dict | None:
313
313
  truncated = False
314
+ offloaded = False
315
+ name = getattr(tool, "name", None) or ""
316
+
317
+ # Offload oversized tool outputs to disk (stable refs) before truncation.
318
+ if (
319
+ isinstance(tool_response, dict)
320
+ and getattr(cfg, "tool_result_offload_enabled", False)
321
+ and getattr(cfg, "tool_result_max_chars", 0) > 0
322
+ ):
323
+ try:
324
+ from gemcode.tool_result_store import maybe_offload_tool_result
325
+ new_payload, did = maybe_offload_tool_result(
326
+ project_root=cfg.project_root,
327
+ tool_name=name,
328
+ payload=tool_response,
329
+ max_inline_chars=int(cfg.tool_result_max_chars),
330
+ )
331
+ if did and isinstance(new_payload, dict):
332
+ tool_response = new_payload
333
+ offloaded = True
334
+ except Exception:
335
+ pass
336
+
314
337
  if isinstance(tool_response, dict) and getattr(cfg, "tool_result_max_chars", 0) > 0:
315
338
  new_d, did = truncate_tool_result_dict(
316
339
  tool_response, int(cfg.tool_result_max_chars)
@@ -318,13 +341,12 @@ def make_after_tool_callback(cfg: GemCodeConfig):
318
341
  if did:
319
342
  tool_response = new_d
320
343
  truncated = True
321
- name = getattr(tool, "name", None) or ""
322
344
  if tool_context is None:
323
- return tool_response if truncated else None
345
+ return tool_response if (truncated or offloaded) else None
324
346
  try:
325
347
  st = tool_context.state
326
348
  except Exception:
327
- return tool_response if truncated else None
349
+ return tool_response if (truncated or offloaded) else None
328
350
  err = isinstance(tool_response, dict) and tool_response.get("error")
329
351
  err_kind = (
330
352
  isinstance(tool_response, dict) and tool_response.get("error_kind")
@@ -409,6 +431,8 @@ def make_after_tool_callback(cfg: GemCodeConfig):
409
431
  pass
410
432
  if truncated:
411
433
  return tool_response
434
+ if offloaded:
435
+ return tool_response
412
436
  return None
413
437
 
414
438
  return after_tool
@@ -130,6 +130,13 @@ class GemCodeConfig:
130
130
  int(os.environ.get("GEMCODE_TOOL_RESULT_MAX_CHARS", "12000")),
131
131
  )
132
132
  )
133
+
134
+ # When enabled, oversized tool outputs are offloaded to disk under
135
+ # .gemcode/tool-results/ and replaced in history with stable refs + previews.
136
+ # This reduces context bloat and improves prompt-cache stability.
137
+ tool_result_offload_enabled: bool = field(
138
+ default_factory=lambda: _truthy_env("GEMCODE_TOOL_RESULT_OFFLOAD", default=True)
139
+ )
133
140
  # Trim oldest text in llm_request.contents when over budget (see context_budget.py).
134
141
  context_shrink_enabled: bool = field(
135
142
  default_factory=lambda: _truthy_env("GEMCODE_CONTEXT_SHRINK", default=True)
@@ -245,10 +252,10 @@ class GemCodeConfig:
245
252
  )
246
253
 
247
254
  # Controls how the TUI renders model thinking: True = full Rich Markdown,
248
- # False = collapsed one-line excerpt (default, like OpenClaude).
255
+ # False = collapsed one-line excerpt.
249
256
  # Toggled at runtime via /thinking verbose|brief.
250
257
  show_full_thinking: bool = field(
251
- default_factory=lambda: _truthy_env("GEMCODE_SHOW_FULL_THINKING", default=False)
258
+ default_factory=lambda: _truthy_env("GEMCODE_SHOW_FULL_THINKING", default=True)
252
259
  )
253
260
 
254
261
  # Enable ADK BuiltInCodeExecutor for safe sandboxed Python execution via
@@ -54,6 +54,28 @@ def _concat_text(content: Any) -> str:
54
54
  return "\n".join(pieces)
55
55
 
56
56
 
57
+ def _distill_memory_text(text: str, *, max_chars: int = 1200) -> str:
58
+ t = (text or "").strip()
59
+ if not t:
60
+ return ""
61
+ t = t[:50_000]
62
+ lines = [ln.strip() for ln in t.splitlines() if ln.strip()]
63
+ keep: list[str] = []
64
+ for ln in lines:
65
+ if any(x in ln for x in (".py", ".ts", ".tsx", ".js", ".json", ".yml", ".yaml", "src/", "gemcode/")):
66
+ keep.append(ln)
67
+ elif ln.startswith(("-", "*")) and len(ln) <= 200:
68
+ keep.append(ln)
69
+ elif any(k in ln.lower() for k in ("fix", "bug", "root cause", "decision", "constraint", "todo", "note")):
70
+ keep.append(ln)
71
+ if sum(len(x) + 1 for x in keep) >= max_chars:
72
+ break
73
+ if not keep:
74
+ return t[:max_chars]
75
+ out = "\n".join(keep)
76
+ return out[:max_chars]
77
+
78
+
57
79
  def _cosine_similarity(a: list[float], b: list[float]) -> float:
58
80
  if not a or not b or len(a) != len(b):
59
81
  return -1.0
@@ -166,6 +188,9 @@ class EmbeddingFileMemoryService(BaseMemoryService):
166
188
  text = _concat_text(content)
167
189
  if not text.strip():
168
190
  continue
191
+ distilled = _distill_memory_text(text)
192
+ if not distilled.strip():
193
+ continue
169
194
 
170
195
  ev_id = getattr(ev, "id", None)
171
196
  if not isinstance(ev_id, str) or not ev_id:
@@ -176,7 +201,7 @@ class EmbeddingFileMemoryService(BaseMemoryService):
176
201
  ts = getattr(ev, "timestamp", None)
177
202
  ts_out = ts if isinstance(ts, str) else None
178
203
 
179
- truncated = text[: self.embedding_max_chars]
204
+ truncated = distilled[: self.embedding_max_chars]
180
205
  rec: dict[str, Any] = {
181
206
  "id": ev_id,
182
207
  "app_name": app_name,
@@ -184,7 +209,8 @@ class EmbeddingFileMemoryService(BaseMemoryService):
184
209
  "session_id": session_id,
185
210
  "author": author if isinstance(author, str) else None,
186
211
  "timestamp": ts_out,
187
- "text": text,
212
+ "text": distilled,
213
+ "raw_truncated": text[:4000],
188
214
  "embedding_text": truncated,
189
215
  "embedding": None,
190
216
  }
@@ -49,6 +49,37 @@ def _concat_text(content: Any) -> str:
49
49
  return "\n".join(pieces)
50
50
 
51
51
 
52
+ def _distill_memory_text(text: str, *, max_chars: int = 1200) -> str:
53
+ """
54
+ Distill verbose conversational text into a compact memory payload.
55
+
56
+ This is deliberately non-LLM (fast, deterministic) and focuses on:
57
+ - explicit decisions / constraints
58
+ - paths / symbols / commands
59
+ - short summaries
60
+ """
61
+ t = (text or "").strip()
62
+ if not t:
63
+ return ""
64
+ # Keep only the first N chars; then try to keep high-signal lines.
65
+ t = t[:50_000]
66
+ lines = [ln.strip() for ln in t.splitlines() if ln.strip()]
67
+ keep: list[str] = []
68
+ for ln in lines:
69
+ if any(x in ln for x in (".py", ".ts", ".tsx", ".js", ".json", ".yml", ".yaml", "src/", "gemcode/")):
70
+ keep.append(ln)
71
+ elif ln.startswith(("-", "*")) and len(ln) <= 200:
72
+ keep.append(ln)
73
+ elif any(k in ln.lower() for k in ("fix", "bug", "root cause", "decision", "constraint", "todo", "note")):
74
+ keep.append(ln)
75
+ if sum(len(x) + 1 for x in keep) >= max_chars:
76
+ break
77
+ if not keep:
78
+ return t[:max_chars]
79
+ out = "\n".join(keep)
80
+ return out[:max_chars]
81
+
82
+
52
83
  class FileMemoryService(BaseMemoryService):
53
84
  """JSONL-backed memory service with naive keyword matching."""
54
85
 
@@ -108,6 +139,9 @@ class FileMemoryService(BaseMemoryService):
108
139
  text = _concat_text(content)
109
140
  if not text.strip():
110
141
  continue
142
+ distilled = _distill_memory_text(text)
143
+ if not distilled.strip():
144
+ continue
111
145
 
112
146
  ev_id = getattr(ev, "id", None)
113
147
  if not isinstance(ev_id, str) or not ev_id:
@@ -127,7 +161,8 @@ class FileMemoryService(BaseMemoryService):
127
161
  "session_id": session_id,
128
162
  "author": author,
129
163
  "timestamp": ts_out,
130
- "text": text,
164
+ "text": distilled,
165
+ "raw_truncated": text[:4000],
131
166
  }
132
167
  )
133
168
  existing_ids.add(ev_id)
@@ -253,7 +253,7 @@ def slash_help_lines() -> list[str]:
253
253
  " Thinking:",
254
254
  " /thinking Show current thinking config",
255
255
  " /thinking verbose Show full thinking text each turn",
256
- " /thinking brief Show collapsed one-line excerpt (default)",
256
+ " /thinking brief Show collapsed one-line excerpt",
257
257
  " /thinking off Disable model thinking",
258
258
  " /thinking on Re-enable thinking (auto budget/level)",
259
259
  " /thinking budget <N> Set thinking token budget (Gemini 2.5, 0=off, -1=dynamic)",
@@ -1001,7 +1001,7 @@ async def process_repl_slash(
1001
1001
  out(" /thinking level <minimal|low|medium|high>")
1002
1002
  out("Display commands (all models):")
1003
1003
  out(" /thinking verbose — show full thinking text each turn")
1004
- out(" /thinking brief — show collapsed one-line excerpt (default)")
1004
+ out(" /thinking brief — show collapsed one-line excerpt")
1005
1005
  out()
1006
1006
  return ReplSlashResult(skip_model_turn=True)
1007
1007
 
@@ -1016,7 +1016,7 @@ async def process_repl_slash(
1016
1016
 
1017
1017
  if sub in ("brief", "short", "collapsed"):
1018
1018
  cfg.show_full_thinking = False
1019
- out("thinking display: brief — collapsed one-line excerpt (default)")
1019
+ out("thinking display: brief — collapsed one-line excerpt")
1020
1020
  out()
1021
1021
  return ReplSlashResult(skip_model_turn=True)
1022
1022
 
@@ -0,0 +1,162 @@
1
+ """
2
+ Disk-backed storage for oversized tool outputs.
3
+
4
+ Why:
5
+ - Large tool outputs (stdout, file contents, web pages) are the biggest driver of
6
+ context bloat and cache misses in long agent sessions.
7
+ - Instead of truncating blobs inline (which still mutates history repeatedly),
8
+ we store the full payload on disk and replace it with a stable reference +
9
+ short preview. This matches OpenClaude's "tool result storage" pattern.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import hashlib
15
+ import json
16
+ import time
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ _REF_PREFIX = "tool_result:"
21
+
22
+
23
+ def _store_dir(project_root: Path) -> Path:
24
+ d = project_root / ".gemcode" / "tool-results"
25
+ d.mkdir(parents=True, exist_ok=True)
26
+ return d
27
+
28
+
29
+ def _sha256_bytes(b: bytes) -> str:
30
+ h = hashlib.sha256()
31
+ h.update(b)
32
+ return h.hexdigest()
33
+
34
+
35
+ def _preview(text: str, max_chars: int) -> str:
36
+ if max_chars <= 0:
37
+ return ""
38
+ if len(text) <= max_chars:
39
+ return text
40
+ if max_chars <= 40:
41
+ return text[:max_chars]
42
+ return text[: max_chars - 20] + "\n… [offloaded; preview truncated]\n"
43
+
44
+
45
+ def offload_text(
46
+ *,
47
+ project_root: Path,
48
+ tool_name: str,
49
+ field: str,
50
+ text: str,
51
+ preview_max_chars: int,
52
+ ) -> dict[str, Any]:
53
+ """
54
+ Persist `text` to disk and return a compact reference dict.
55
+
56
+ The ref is content-addressed (sha256 of bytes) so repeated identical outputs
57
+ map to the same ref, improving cache stability.
58
+ """
59
+ b = text.encode("utf-8", errors="replace")
60
+ sha = _sha256_bytes(b)
61
+ ref = f"{_REF_PREFIX}{sha}"
62
+ p = _store_dir(project_root) / f"{sha}.txt"
63
+ if not p.exists():
64
+ # Write once; keep deterministic content for stable cache behavior.
65
+ p.write_bytes(b)
66
+ meta = {
67
+ "ref": ref,
68
+ "sha256": sha,
69
+ "tool": tool_name,
70
+ "field": field,
71
+ "bytes": len(b),
72
+ "chars": len(text),
73
+ "created_at": int(time.time()),
74
+ }
75
+ ( _store_dir(project_root) / f"{sha}.json" ).write_text(
76
+ json.dumps(meta, ensure_ascii=False, indent=2),
77
+ encoding="utf-8",
78
+ errors="replace",
79
+ )
80
+ return {
81
+ "offloaded": True,
82
+ "ref": ref,
83
+ "preview": _preview(text, preview_max_chars),
84
+ "chars": len(text),
85
+ "hint": "Use load_tool_result(ref) to view the full content.",
86
+ }
87
+
88
+
89
+ def maybe_offload_tool_result(
90
+ *,
91
+ project_root: Path,
92
+ tool_name: str,
93
+ payload: Any,
94
+ max_inline_chars: int,
95
+ ) -> tuple[Any, bool]:
96
+ """
97
+ Walk a tool-result payload and offload large text fields.
98
+
99
+ Returns (new_payload, changed).
100
+ """
101
+ if max_inline_chars <= 0:
102
+ return payload, False
103
+
104
+ changed = False
105
+
106
+ def _walk(obj: Any, *, field: str) -> Any:
107
+ nonlocal changed
108
+ if isinstance(obj, str) and len(obj) > max_inline_chars:
109
+ changed = True
110
+ return offload_text(
111
+ project_root=project_root,
112
+ tool_name=tool_name,
113
+ field=field,
114
+ text=obj,
115
+ preview_max_chars=max_inline_chars,
116
+ )
117
+
118
+ if isinstance(obj, list):
119
+ out_list: list[Any] = []
120
+ for i, item in enumerate(obj):
121
+ out_list.append(_walk(item, field=f"{field}[{i}]"))
122
+ if out_list != obj:
123
+ changed = True
124
+ return out_list
125
+
126
+ if isinstance(obj, dict):
127
+ out_dict: dict[str, Any] = {}
128
+ for k, v in obj.items():
129
+ out_dict[k] = _walk(v, field=str(k))
130
+ if out_dict != obj:
131
+ changed = True
132
+ return out_dict
133
+
134
+ return obj
135
+
136
+ # Only dict payloads are expected from our tools, but handle any.
137
+ return _walk(payload, field="payload"), changed
138
+
139
+
140
+ def load_tool_result_text(
141
+ *,
142
+ project_root: Path,
143
+ ref: str,
144
+ max_chars: int = 40_000,
145
+ tail: bool = True,
146
+ ) -> dict[str, Any]:
147
+ if not isinstance(ref, str) or not ref.startswith(_REF_PREFIX):
148
+ return {"error": "Invalid ref. Expected 'tool_result:<sha256>'."}
149
+ sha = ref[len(_REF_PREFIX) :].strip()
150
+ if not sha or any(c not in "0123456789abcdef" for c in sha) or len(sha) < 32:
151
+ return {"error": "Invalid ref sha."}
152
+ p = _store_dir(project_root) / f"{sha}.txt"
153
+ if not p.exists():
154
+ return {"error": f"Not found: {ref}"}
155
+ text = p.read_text(encoding="utf-8", errors="replace")
156
+ truncated = False
157
+ if max_chars is not None and isinstance(max_chars, int) and max_chars > 0:
158
+ if len(text) > max_chars:
159
+ truncated = True
160
+ text = ("… [truncated; showing tail]\n" + text[-max_chars:]) if tail else (text[:max_chars] + "\n… [truncated]")
161
+ return {"ref": ref, "text": text, "truncated": truncated, "chars": len(text)}
162
+
@@ -7,6 +7,7 @@ from gemcode.tools.bash import make_bash_tool
7
7
  from gemcode.tools.edit import make_edit_tools
8
8
  from gemcode.tools.filesystem import make_filesystem_tools
9
9
  from gemcode.tools.notebook import make_notebook_tools
10
+ from gemcode.tools.repo_map import make_repo_map_tool
10
11
  from gemcode.tools.search import make_grep_tool
11
12
  from gemcode.tools.shell import make_run_command
12
13
  from gemcode.tools.subtask import make_run_subtask_tool
@@ -31,6 +32,26 @@ def _get_load_memory_tool():
31
32
  return None
32
33
 
33
34
 
35
+ def _make_load_tool_result_tool(cfg: GemCodeConfig):
36
+ def load_tool_result(ref: str, max_chars: int = 40_000, tail: bool = True) -> dict:
37
+ """
38
+ Load a previously offloaded tool output by reference.
39
+
40
+ Offloaded outputs are created automatically when GEMCODE_TOOL_RESULT_OFFLOAD=1.
41
+ References look like: tool_result:<sha256>.
42
+ """
43
+ from gemcode.tool_result_store import load_tool_result_text
44
+
45
+ return load_tool_result_text(
46
+ project_root=cfg.project_root,
47
+ ref=ref,
48
+ max_chars=max_chars,
49
+ tail=tail,
50
+ )
51
+
52
+ return load_tool_result
53
+
54
+
34
55
  def _wrap_long_running(fn):
35
56
  """
36
57
  Wrap a function tool with ADK's LongRunningFunctionTool so that long-running
@@ -60,6 +81,8 @@ def build_function_tools(cfg: GemCodeConfig, *, include_subtask: bool = True) ->
60
81
  web_search = make_web_search_tool()
61
82
  notebook_read, notebook_edit = make_notebook_tools(cfg)
62
83
  list_tasks, kill_task, task_output = make_task_tools(cfg)
84
+ load_tool_result = _make_load_tool_result_tool(cfg)
85
+ repo_map = make_repo_map_tool(cfg)
63
86
 
64
87
  # bash and run_command are the most common long-running tools (builds, tests,
65
88
  # installs). Wrap them with LongRunningFunctionTool so ADK can handle slow
@@ -77,6 +100,7 @@ def build_function_tools(cfg: GemCodeConfig, *, include_subtask: bool = True) ->
77
100
  list_directory,
78
101
  glob_files,
79
102
  grep_content,
103
+ repo_map,
80
104
  # Notebooks
81
105
  notebook_read,
82
106
  notebook_edit,
@@ -95,6 +119,8 @@ def build_function_tools(cfg: GemCodeConfig, *, include_subtask: bool = True) ->
95
119
  # Web / research
96
120
  web_search,
97
121
  web_fetch,
122
+ # Tool output offload loader
123
+ load_tool_result,
98
124
  ]
99
125
 
100
126
  # ADK load_memory: explicit on-demand memory search (complements preload_memory).
@@ -182,8 +182,8 @@ def make_bash_tool(cfg: GemCodeConfig):
182
182
  env=env,
183
183
  check=False,
184
184
  )
185
- stdout = proc.stdout[:80_000]
186
- stderr = proc.stderr[:20_000]
185
+ stdout = proc.stdout[:20_000]
186
+ stderr = proc.stderr[:10_000]
187
187
  result: dict = {
188
188
  "command": command,
189
189
  "cwd": str(exec_cwd.relative_to(root)) if exec_cwd != root else ".",
@@ -16,7 +16,7 @@ def make_filesystem_tools(cfg: GemCodeConfig):
16
16
 
17
17
  def read_file(
18
18
  path: str,
19
- max_bytes: int = 200_000,
19
+ max_bytes: int = 80_000,
20
20
  start_line: int = 1,
21
21
  end_line: int | None = None,
22
22
  ) -> dict:
@@ -0,0 +1,132 @@
1
+ """
2
+ Repo map tool: lightweight symbol-first context for large repos.
3
+
4
+ Inspired by Aider's "repo map" approach: provide a compact overview (files +
5
+ top-level symbols) under a strict token/char budget, then read specific files
6
+ on demand.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from gemcode.config import GemCodeConfig
16
+ from gemcode.paths import PathEscapeError, resolve_under_root
17
+ from gemcode.trust import is_trusted_root
18
+
19
+
20
+ _PY_DEF = re.compile(r"^\s*(def|class)\s+([A-Za-z_][A-Za-z0-9_]*)")
21
+ _TS_DEF = re.compile(
22
+ r"^\s*(export\s+)?(async\s+)?(function|class|interface|type)\s+([A-Za-z_][A-Za-z0-9_]*)"
23
+ )
24
+
25
+
26
+ def _iter_files(root: Path, include_glob: str) -> list[Path]:
27
+ out: list[Path] = []
28
+ for p in root.glob(include_glob):
29
+ if not p.is_file():
30
+ continue
31
+ if "/.git/" in str(p):
32
+ continue
33
+ # Skip huge files; repo_map is meant to be cheap.
34
+ try:
35
+ if p.stat().st_size > 300_000:
36
+ continue
37
+ except OSError:
38
+ continue
39
+ out.append(p)
40
+ if len(out) >= 800:
41
+ break
42
+ return out
43
+
44
+
45
+ def _symbols_for_file(p: Path, *, max_lines: int = 400) -> list[str]:
46
+ try:
47
+ text = p.read_text(encoding="utf-8", errors="ignore")
48
+ except OSError:
49
+ return []
50
+ lines = text.splitlines()[:max_lines]
51
+ syms: list[str] = []
52
+ for ln in lines:
53
+ m = _PY_DEF.match(ln)
54
+ if m:
55
+ syms.append(f"{m.group(1)} {m.group(2)}")
56
+ continue
57
+ m2 = _TS_DEF.match(ln)
58
+ if m2:
59
+ syms.append(f"{m2.group(3)} {m2.group(4)}")
60
+ # Deduplicate while preserving order
61
+ seen: set[str] = set()
62
+ out: list[str] = []
63
+ for s in syms:
64
+ if s in seen:
65
+ continue
66
+ seen.add(s)
67
+ out.append(s)
68
+ if len(out) >= 40:
69
+ break
70
+ return out
71
+
72
+
73
+ def make_repo_map_tool(cfg: GemCodeConfig):
74
+ root = cfg.project_root
75
+ trusted = is_trusted_root(root)
76
+
77
+ def repo_map(
78
+ path: str = ".",
79
+ include_glob: str = "**/*.{py,ts,tsx,js,jsx,md,txt,json,yml,yaml}",
80
+ max_chars: int = 18_000,
81
+ max_files: int = 200,
82
+ include_symbols: bool = True,
83
+ ) -> dict[str, Any]:
84
+ """
85
+ Return a compact overview of a repo subtree under a strict char budget.
86
+
87
+ Best for: large codebases where sending many full files is expensive.
88
+ Use this, then `read_file` for specific files.
89
+ """
90
+ if not trusted:
91
+ return {"error": "Project folder is not trusted. Re-run GemCode and approve folder trust."}
92
+ try:
93
+ base = resolve_under_root(root, path)
94
+ except PathEscapeError as e:
95
+ return {"error": str(e)}
96
+ if not base.is_dir():
97
+ return {"error": f"Not a directory: {path}"}
98
+
99
+ files = _iter_files(base, include_glob)
100
+ # Make paths relative to project root for stable references
101
+ rels: list[str] = []
102
+ for p in files:
103
+ try:
104
+ rels.append(str(p.resolve().relative_to(root)))
105
+ except ValueError:
106
+ continue
107
+ if len(rels) >= max_files:
108
+ break
109
+
110
+ # Build a char-budgeted map string.
111
+ lines: list[str] = []
112
+ lines.append(f"Repo map for: {path} (files={len(rels)})")
113
+ lines.append("")
114
+ for rel in rels:
115
+ if sum(len(x) + 1 for x in lines) >= max_chars:
116
+ break
117
+ lines.append(rel)
118
+ if include_symbols and rel.endswith((".py", ".ts", ".tsx", ".js", ".jsx")):
119
+ sym = _symbols_for_file(root / rel)
120
+ for s in sym:
121
+ if sum(len(x) + 1 for x in lines) >= max_chars:
122
+ break
123
+ lines.append(f" - {s}")
124
+
125
+ out = "\n".join(lines)
126
+ truncated = len(out) > max_chars
127
+ if truncated:
128
+ out = out[:max_chars] + "\n… [truncated]"
129
+ return {"path": path, "map": out, "truncated": truncated}
130
+
131
+ return repo_map
132
+
@@ -33,7 +33,7 @@ def make_grep_tool(cfg: GemCodeConfig):
33
33
  def grep_content(
34
34
  pattern: str,
35
35
  path_glob: str = "**/*",
36
- max_matches: int = 80,
36
+ max_matches: int = 40,
37
37
  context_lines: int = 0,
38
38
  case_sensitive: bool = True,
39
39
  ) -> dict:
@@ -161,8 +161,8 @@ def make_run_command(cfg: GemCodeConfig):
161
161
  "command": [exe, *args],
162
162
  "cwd": str(exec_cwd.relative_to(root)) if exec_cwd != root else ".",
163
163
  "exit_code": proc.returncode,
164
- "stdout": proc.stdout[:50_000],
165
- "stderr": proc.stderr[:50_000],
164
+ "stdout": proc.stdout[:20_000],
165
+ "stderr": proc.stderr[:20_000],
166
166
  }
167
167
  except subprocess.TimeoutExpired:
168
168
  return {"error": f"Timeout after {timeout_seconds}s"}
@@ -153,9 +153,24 @@ def make_run_subtask_tool(cfg: GemCodeConfig):
153
153
  sub_session_id = str(uuid.uuid4())
154
154
 
155
155
  # Compose the sub-agent prompt.
156
- prompt = task.strip()
157
- if context and context.strip():
158
- prompt = f"{task.strip()}\n\nAdditional context:\n{context.strip()}"
156
+ task_clean = task.strip()
157
+ ctx_clean = (context or "").strip()
158
+ prompt = task_clean
159
+ if ctx_clean:
160
+ prompt = f"{task_clean}\n\nAdditional context:\n{ctx_clean}"
161
+
162
+ # Enforce a compact response contract to protect the parent context.
163
+ prompt = (
164
+ "Return a concise result using this exact structure:\n"
165
+ "## Summary\n"
166
+ "- <3-7 bullets>\n\n"
167
+ "## Findings\n"
168
+ "- <key technical findings>\n\n"
169
+ "## Evidence (paths / commands)\n"
170
+ "- <file paths, symbols, or commands you used>\n\n"
171
+ "Do NOT include long code blocks or raw logs. If something is long, summarize it.\n\n"
172
+ + prompt
173
+ )
159
174
 
160
175
  # Sub-agents get a higher cap than before (64 vs 48) since they now
161
176
  # carry a richer tool surface (research, notes, etc.)
@@ -197,6 +212,28 @@ def make_run_subtask_tool(cfg: GemCodeConfig):
197
212
  "that asks the sub-agent to summarise its findings.)"
198
213
  )
199
214
 
215
+ # Hard-cap sub-agent output; offload the full text if it exceeds the cap.
216
+ max_chars = 8_000
217
+ if len(result_text) > max_chars:
218
+ try:
219
+ from gemcode.tool_result_store import offload_text
220
+ ref_obj = offload_text(
221
+ project_root=cfg.project_root,
222
+ tool_name="run_subtask",
223
+ field="result",
224
+ text=result_text,
225
+ preview_max_chars=max_chars,
226
+ )
227
+ return {
228
+ "result": ref_obj.get("preview", "") or "",
229
+ "offloaded": True,
230
+ "ref": ref_obj.get("ref"),
231
+ "note": "Subtask output was long; full text offloaded. Use load_tool_result(ref).",
232
+ }
233
+ except Exception:
234
+ result_text = result_text[:max_chars] + "\n… [truncated]"
235
+ return {"result": result_text, "truncated": True}
236
+
200
237
  return {"result": result_text}
201
238
 
202
239
  return run_subtask
@@ -53,7 +53,7 @@ def _html_to_text(html: str) -> str:
53
53
 
54
54
 
55
55
  def make_web_fetch_tool():
56
- def web_fetch(url: str, max_chars: int = 40_000, raw: bool = False) -> dict:
56
+ def web_fetch(url: str, max_chars: int = 20_000, raw: bool = False) -> dict:
57
57
  """
58
58
  Fetch content from a URL and return it as text.
59
59
 
@@ -64,7 +64,7 @@ def make_web_fetch_tool():
64
64
  - Reading READMEs, changelogs, or issue trackers online
65
65
 
66
66
  Set raw=True to get the raw HTML/JSON instead of extracted text.
67
- max_chars caps the returned content (default 40 000 chars).
67
+ max_chars caps the returned content (default 20 000 chars).
68
68
  """
69
69
  if not url or not url.strip():
70
70
  return {"error": "url must not be empty"}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.64
3
+ Version: 0.3.66
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -43,6 +43,7 @@ src/gemcode/slash_commands.py
43
43
  src/gemcode/thinking.py
44
44
  src/gemcode/tool_prompt_manifest.py
45
45
  src/gemcode/tool_registry.py
46
+ src/gemcode/tool_result_store.py
46
47
  src/gemcode/tools_inspector.py
47
48
  src/gemcode/trust.py
48
49
  src/gemcode/version.py
@@ -76,6 +77,7 @@ src/gemcode/tools/edit.py
76
77
  src/gemcode/tools/filesystem.py
77
78
  src/gemcode/tools/notebook.py
78
79
  src/gemcode/tools/notes.py
80
+ src/gemcode/tools/repo_map.py
79
81
  src/gemcode/tools/search.py
80
82
  src/gemcode/tools/shell.py
81
83
  src/gemcode/tools/shell_gate.py
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