gemcode 0.3.65__py3-none-any.whl → 0.3.67__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
gemcode/agent.py CHANGED
@@ -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,21 @@ 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
+ ## Token efficiency without losing intelligence
521
+ - Prefer **small, targeted tool outputs** by default (saves context, improves accuracy).
522
+ - If a tool output was **offloaded** (you see a `tool_result:<sha>` reference), and you need details, call `load_tool_result(ref)` and extract only the relevant slice.
523
+
524
+ ## Tool selection guide (only when needed)
525
+
526
+ Keep tool usage minimal. Prefer short, targeted calls and keep tool outputs small.
527
+ If you need more tool usage examples, set `GEMCODE_VERBOSE_INSTRUCTIONS=1`.
528
+
529
+ """
530
+
531
+ if not verbose_tools_guide:
532
+ return base.strip() + "\n"
533
+
534
+ tool_guide = r"""
512
535
  ## Tool selection guide
513
536
 
514
537
  ### Shell execution (critical — use these for real work)
@@ -864,6 +887,7 @@ def build_root_agent(
864
887
  pre-built list that excludes run_subtask itself, preventing recursion).
865
888
  When set, build_function_tools() is NOT called.
866
889
  """
890
+ return (base + tool_guide).strip() + "\n"
867
891
  if _tools is not None:
868
892
  tools = list(_tools)
869
893
  else:
gemcode/callbacks.py CHANGED
@@ -311,20 +311,50 @@ def make_after_tool_callback(cfg: GemCodeConfig):
311
311
  tool_response: dict,
312
312
  ) -> dict | None:
313
313
  truncated = False
314
- if isinstance(tool_response, dict) and getattr(cfg, "tool_result_max_chars", 0) > 0:
314
+ offloaded = False
315
+ name = getattr(tool, "name", None) or ""
316
+
317
+ # Offload oversized tool outputs to disk (stable refs) before truncation.
318
+ # Dynamic caps for tool inline payload size.
319
+ effective_tool_chars = int(getattr(cfg, "tool_result_max_chars", 0) or 0)
320
+ try:
321
+ from gemcode.dynamic_policy import get_dynamic_caps
322
+ effective_tool_chars = get_dynamic_caps(cfg).tool_inline_chars
323
+ except Exception:
324
+ pass
325
+
326
+ if (
327
+ isinstance(tool_response, dict)
328
+ and getattr(cfg, "tool_result_offload_enabled", False)
329
+ and effective_tool_chars > 0
330
+ ):
331
+ try:
332
+ from gemcode.tool_result_store import maybe_offload_tool_result
333
+ new_payload, did = maybe_offload_tool_result(
334
+ project_root=cfg.project_root,
335
+ tool_name=name,
336
+ payload=tool_response,
337
+ max_inline_chars=int(effective_tool_chars),
338
+ )
339
+ if did and isinstance(new_payload, dict):
340
+ tool_response = new_payload
341
+ offloaded = True
342
+ except Exception:
343
+ pass
344
+
345
+ if isinstance(tool_response, dict) and effective_tool_chars > 0:
315
346
  new_d, did = truncate_tool_result_dict(
316
- tool_response, int(cfg.tool_result_max_chars)
347
+ tool_response, int(effective_tool_chars)
317
348
  )
318
349
  if did:
319
350
  tool_response = new_d
320
351
  truncated = True
321
- name = getattr(tool, "name", None) or ""
322
352
  if tool_context is None:
323
- return tool_response if truncated else None
353
+ return tool_response if (truncated or offloaded) else None
324
354
  try:
325
355
  st = tool_context.state
326
356
  except Exception:
327
- return tool_response if truncated else None
357
+ return tool_response if (truncated or offloaded) else None
328
358
  err = isinstance(tool_response, dict) and tool_response.get("error")
329
359
  err_kind = (
330
360
  isinstance(tool_response, dict) and tool_response.get("error_kind")
@@ -409,6 +439,8 @@ def make_after_tool_callback(cfg: GemCodeConfig):
409
439
  pass
410
440
  if truncated:
411
441
  return tool_response
442
+ if offloaded:
443
+ return tool_response
412
444
  return None
413
445
 
414
446
  return after_tool
@@ -508,6 +540,14 @@ def make_after_model_callback(cfg: GemCodeConfig):
508
540
  st[_LAST_PROMPT_TOKENS] = pt
509
541
  st[_LAST_CONTEXT_PCT] = cw.get("percent_left")
510
542
  st[_LAST_CONTEXT_LEVEL] = level
543
+ # Expose to tool layer (dynamic token policy).
544
+ try:
545
+ pct = cw.get("percent_left")
546
+ if isinstance(pct, int):
547
+ object.__setattr__(cfg, "_context_percent_left", pct)
548
+ object.__setattr__(cfg, "_context_alert_level", int(level))
549
+ except Exception:
550
+ pass
511
551
  append_audit(
512
552
  cfg.project_root,
513
553
  {
gemcode/config.py CHANGED
@@ -130,6 +130,19 @@ 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
+ )
140
+
141
+ # Dynamic token policy: adapt tool output caps to context pressure so we stay
142
+ # cheap when context is tight, but remain evidence-rich when there's room.
143
+ dynamic_token_policy: bool = field(
144
+ default_factory=lambda: _truthy_env("GEMCODE_DYNAMIC_TOKEN_POLICY", default=True)
145
+ )
133
146
  # Trim oldest text in llm_request.contents when over budget (see context_budget.py).
134
147
  context_shrink_enabled: bool = field(
135
148
  default_factory=lambda: _truthy_env("GEMCODE_CONTEXT_SHRINK", default=True)
@@ -0,0 +1,117 @@
1
+ """
2
+ Dynamic token budgeting / caps.
3
+
4
+ Optimization must not make the agent dumb:
5
+ - When context pressure is low, allow richer tool outputs and wider reads.
6
+ - When context pressure is high, tighten caps and offload aggressively.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from typing import Any
13
+
14
+
15
+ def _truthy(v: Any, *, default: bool = False) -> bool:
16
+ if v is None:
17
+ return default
18
+ if isinstance(v, bool):
19
+ return v
20
+ if isinstance(v, str):
21
+ return v.lower() in ("1", "true", "yes", "on")
22
+ return bool(v)
23
+
24
+
25
+ def _pct_left(cfg) -> int | None:
26
+ try:
27
+ v = getattr(cfg, "_context_percent_left", None)
28
+ if isinstance(v, int):
29
+ return v
30
+ except Exception:
31
+ return None
32
+ return None
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class DynamicCaps:
37
+ tool_inline_chars: int
38
+ read_file_max_bytes: int
39
+ web_fetch_max_chars: int
40
+ bash_stdout_chars: int
41
+ bash_stderr_chars: int
42
+ run_stdout_chars: int
43
+ run_stderr_chars: int
44
+ grep_max_matches: int
45
+
46
+
47
+ def get_dynamic_caps(cfg) -> DynamicCaps:
48
+ """
49
+ Compute caps based on current context pressure.
50
+
51
+ Policy:
52
+ - Healthy (>=45% left): generous caps (better evidence, less re-asking).
53
+ - Warning (20-44%): moderate caps.
54
+ - Tight (<20%): strict caps + prefer offload.
55
+ """
56
+ # cfg can be None in some tool contexts; treat as enabled with defaults.
57
+ enabled = _truthy(getattr(cfg, "dynamic_token_policy", True) if cfg is not None else True, default=True)
58
+ if not enabled:
59
+ # Essentially "no-op" high caps; tools still apply their explicit maxes.
60
+ return DynamicCaps(
61
+ tool_inline_chars=int(getattr(cfg, "tool_result_max_chars", 12000) or 12000),
62
+ read_file_max_bytes=200_000,
63
+ web_fetch_max_chars=40_000,
64
+ bash_stdout_chars=80_000,
65
+ bash_stderr_chars=20_000,
66
+ run_stdout_chars=50_000,
67
+ run_stderr_chars=50_000,
68
+ grep_max_matches=80,
69
+ )
70
+
71
+ pct = _pct_left(cfg) if cfg is not None else None
72
+ if pct is None:
73
+ pct = 35
74
+
75
+ # Base knobs from config (so users can still tune globally).
76
+ base_tool = int(getattr(cfg, "tool_result_max_chars", 12000) or 12000) if cfg is not None else 12000
77
+ base_tool = max(1000, base_tool)
78
+
79
+ if pct >= 45:
80
+ mult = 1.4
81
+ return DynamicCaps(
82
+ tool_inline_chars=min(24_000, int(base_tool * mult)),
83
+ read_file_max_bytes=140_000,
84
+ web_fetch_max_chars=30_000,
85
+ bash_stdout_chars=30_000,
86
+ bash_stderr_chars=15_000,
87
+ run_stdout_chars=30_000,
88
+ run_stderr_chars=30_000,
89
+ grep_max_matches=60,
90
+ )
91
+
92
+ if pct >= 20:
93
+ mult = 1.0
94
+ return DynamicCaps(
95
+ tool_inline_chars=min(18_000, int(base_tool * mult)),
96
+ read_file_max_bytes=80_000,
97
+ web_fetch_max_chars=20_000,
98
+ bash_stdout_chars=20_000,
99
+ bash_stderr_chars=10_000,
100
+ run_stdout_chars=20_000,
101
+ run_stderr_chars=20_000,
102
+ grep_max_matches=40,
103
+ )
104
+
105
+ # Tight
106
+ mult = 0.6
107
+ return DynamicCaps(
108
+ tool_inline_chars=max(2000, int(base_tool * mult)),
109
+ read_file_max_bytes=35_000,
110
+ web_fetch_max_chars=10_000,
111
+ bash_stdout_chars=10_000,
112
+ bash_stderr_chars=8_000,
113
+ run_stdout_chars=10_000,
114
+ run_stderr_chars=10_000,
115
+ grep_max_matches=20,
116
+ )
117
+
@@ -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)
@@ -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
+
gemcode/tools/__init__.py CHANGED
@@ -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,14 @@ 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)
86
+
87
+ # Attach cfg for dynamic policy inside web_fetch (no cfg param in signature).
88
+ try:
89
+ setattr(web_fetch, "_cfg", cfg)
90
+ except Exception:
91
+ pass
63
92
 
64
93
  # bash and run_command are the most common long-running tools (builds, tests,
65
94
  # installs). Wrap them with LongRunningFunctionTool so ADK can handle slow
@@ -77,6 +106,7 @@ def build_function_tools(cfg: GemCodeConfig, *, include_subtask: bool = True) ->
77
106
  list_directory,
78
107
  glob_files,
79
108
  grep_content,
109
+ repo_map,
80
110
  # Notebooks
81
111
  notebook_read,
82
112
  notebook_edit,
@@ -95,6 +125,8 @@ def build_function_tools(cfg: GemCodeConfig, *, include_subtask: bool = True) ->
95
125
  # Web / research
96
126
  web_search,
97
127
  web_fetch,
128
+ # Tool output offload loader
129
+ load_tool_result,
98
130
  ]
99
131
 
100
132
  # ADK load_memory: explicit on-demand memory search (complements preload_memory).
gemcode/tools/bash.py CHANGED
@@ -182,8 +182,21 @@ 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
+ try:
186
+ from gemcode.dynamic_policy import get_dynamic_caps
187
+ caps = get_dynamic_caps(cfg)
188
+ stdout_cap = caps.bash_stdout_chars
189
+ stderr_cap = caps.bash_stderr_chars
190
+ except Exception:
191
+ stdout_cap = 20_000
192
+ stderr_cap = 10_000
193
+
194
+ # Keep more stderr when failing; it is usually the most informative.
195
+ if proc.returncode != 0:
196
+ stderr_cap = max(stderr_cap, 12_000)
197
+
198
+ stdout = proc.stdout[:stdout_cap]
199
+ stderr = proc.stderr[:stderr_cap]
187
200
  result: dict = {
188
201
  "command": command,
189
202
  "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:
@@ -43,6 +43,15 @@ def make_filesystem_tools(cfg: GemCodeConfig):
43
43
  return {"error": str(e)}
44
44
  if not p.is_file():
45
45
  return {"error": f"Not a file: {path}"}
46
+
47
+ # Dynamic caps: allow bigger reads when context is healthy, tighten when tight.
48
+ try:
49
+ from gemcode.dynamic_policy import get_dynamic_caps
50
+ caps = get_dynamic_caps(cfg)
51
+ if isinstance(max_bytes, int) and max_bytes > caps.read_file_max_bytes:
52
+ max_bytes = caps.read_file_max_bytes
53
+ except Exception:
54
+ pass
46
55
  total_bytes = p.stat().st_size
47
56
  data = p.read_bytes()
48
57
  text_full = data.decode("utf-8", errors="replace")
@@ -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
+
gemcode/tools/search.py CHANGED
@@ -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:
@@ -61,6 +61,15 @@ def make_grep_tool(cfg: GemCodeConfig):
61
61
  Issue multiple grep_content calls in the same turn when searching for
62
62
  different patterns — they run in parallel.
63
63
  """
64
+ # Dynamic caps: allow richer search when context is healthy.
65
+ try:
66
+ from gemcode.dynamic_policy import get_dynamic_caps
67
+ caps = get_dynamic_caps(cfg)
68
+ if isinstance(max_matches, int) and max_matches > caps.grep_max_matches:
69
+ max_matches = caps.grep_max_matches
70
+ except Exception:
71
+ pass
72
+
64
73
  if max_matches < 1:
65
74
  max_matches = 1
66
75
  if max_matches > 500:
gemcode/tools/shell.py CHANGED
@@ -157,12 +157,20 @@ def make_run_command(cfg: GemCodeConfig):
157
157
  env=child_env,
158
158
  check=False,
159
159
  )
160
+ try:
161
+ from gemcode.dynamic_policy import get_dynamic_caps
162
+ caps = get_dynamic_caps(cfg)
163
+ out_cap = caps.run_stdout_chars
164
+ err_cap = caps.run_stderr_chars
165
+ except Exception:
166
+ out_cap = 20_000
167
+ err_cap = 20_000
160
168
  return {
161
169
  "command": [exe, *args],
162
170
  "cwd": str(exec_cwd.relative_to(root)) if exec_cwd != root else ".",
163
171
  "exit_code": proc.returncode,
164
- "stdout": proc.stdout[:50_000],
165
- "stderr": proc.stderr[:50_000],
172
+ "stdout": proc.stdout[:out_cap],
173
+ "stderr": proc.stderr[:err_cap],
166
174
  }
167
175
  except subprocess.TimeoutExpired:
168
176
  return {"error": f"Timeout after {timeout_seconds}s"}
gemcode/tools/subtask.py CHANGED
@@ -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
gemcode/tools/web.py CHANGED
@@ -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,13 +64,25 @@ 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"}
71
71
  url = url.strip()
72
72
  if not url.startswith(("http://", "https://")):
73
73
  return {"error": "Only http:// and https:// URLs are supported"}
74
+ try:
75
+ from gemcode.dynamic_policy import get_dynamic_caps
76
+ caps = get_dynamic_caps(getattr(web_fetch, "_cfg", None) or None) # type: ignore[arg-type]
77
+ except Exception:
78
+ caps = None
79
+ try:
80
+ # make_web_fetch_tool() has no cfg, so we attach one in the builder.
81
+ # If present, apply the dynamic cap.
82
+ if caps is not None and max_chars > getattr(caps, "web_fetch_max_chars", 20_000):
83
+ max_chars = int(getattr(caps, "web_fetch_max_chars", 20_000))
84
+ except Exception:
85
+ pass
74
86
  if max_chars < 1000:
75
87
  max_chars = 1000
76
88
  if max_chars > 200_000:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.65
3
+ Version: 0.3.67
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -1,16 +1,17 @@
1
1
  gemcode/__init__.py,sha256=l0DCRYqK7KM7Fb7u49fqh-5_SlpeIL7r3LjMeJWMgSg,112
2
2
  gemcode/__main__.py,sha256=EX2s1hxq2Yvli_-tnBN3w5Qv4bOjsBBbjyISF0pDIQw,37
3
- gemcode/agent.py,sha256=Yrali2aW95nq6jCBTrs5j_5kH5X4923YUkJn9h6sa8k,51236
3
+ gemcode/agent.py,sha256=yft-8lLJbX4abM_bUW156JWfVT3b4OHO-_hIoKdVaOs,52032
4
4
  gemcode/audit.py,sha256=bh9uhXaeh8wqxqoZtz3ZAowd8Ndk1ss-mw9993Vlrgo,469
5
5
  gemcode/autocompact.py,sha256=77h5tgFzJ2rjrhlCL2oIc28IHwLbP4Pqlo7cSNgDwiA,6727
6
- gemcode/callbacks.py,sha256=VSxV9EbjkrF8fguiEPfKrO0JZ5LvfclMEAQuuV1ZFCE,24800
6
+ gemcode/callbacks.py,sha256=7kPMDsc2vvY98Ogoi4D0XHB96qh_kx4l4CRsRtePNdk,26171
7
7
  gemcode/capability_routing.py,sha256=yvQXwKtrfHXbgbNunU0Dxh9GCDN4cbySXIeccrdzr2o,3471
8
8
  gemcode/cli.py,sha256=kBXb4b4JCG3u0XewCJn8lCyOT62Y8bOvlVoDc2R-GKQ,25320
9
9
  gemcode/compaction.py,sha256=9YtA_qa23_8dHWVHx7AJwUduuI7jJQtq-m6sT8jgPWI,1186
10
- gemcode/config.py,sha256=nygtUWCo0iBocXFHq8mpQC-BRvtYg5F0ChlmkgC6R0g,14059
10
+ gemcode/config.py,sha256=bCo_Qz4oI_Sfi0EzptULCjfTbtYNhHtMeXTil2vUi8M,14697
11
11
  gemcode/context_budget.py,sha256=Nhox9vFBtLbb7jtO7cyGW1MxtN7SVjlIeQ7d-cgGyKM,10544
12
12
  gemcode/context_warning.py,sha256=Q8mg5Vojj7EglPhsGAVL7vb8ROLuHVPgdzw25yw-Q2c,4263
13
13
  gemcode/credentials.py,sha256=04v-rLD8_Ams69FQdof2FwcL3ZgsroGUnMcHNQFuBZo,1296
14
+ gemcode/dynamic_policy.py,sha256=0ZYv3nzMsEN4kPpjhMbXVSMoTvA_IArbMfe3fF7hhzw,3158
14
15
  gemcode/hitl_session.py,sha256=oNiI7odFJGUcmqPavjKLJOEumZKrgklLvwjjrIG9GPg,281
15
16
  gemcode/hooks.py,sha256=FHz175d_18j-4ByZZVdEIagmdOvLHcjDjo7HD2Cikf4,6339
16
17
  gemcode/intent_classifier.py,sha256=YfRVEe8gHeKlRkjuSWef1bZ0MPBwyYMp5jymP5Vig5U,8507
@@ -39,6 +40,7 @@ gemcode/slash_commands.py,sha256=Qylzsj1notk0xN_hvd3CR4HD8g-l99UENDMcg1pKeBA,794
39
40
  gemcode/thinking.py,sha256=RanBf_x9fKv1o4DNyNXPLfOdn2xT0KybJb65nYgmMEE,4885
40
41
  gemcode/tool_prompt_manifest.py,sha256=MS_eSJg2BTp6yv1Ih2p93okPmnK3B2dYAMjnG6yaEVY,8695
41
42
  gemcode/tool_registry.py,sha256=ifqxtr2uLwEUwnJLKYLza_tIz-paaZediJa75y9MiyA,1795
43
+ gemcode/tool_result_store.py,sha256=Wfm_JHLdYAI4jfjTLHOEAeK2yho9OCLUF_qErhj-wV0,4428
42
44
  gemcode/tools_inspector.py,sha256=okmu4PDYAQQ7nthDvuzSHmy2zArFTG4ftIPRadzLnxA,4100
43
45
  gemcode/trust.py,sha256=fxe57Xg6aL_KU24bQDUtD-rXjsNpaq7g-eQTInZnudE,1336
44
46
  gemcode/version.py,sha256=uwynYS-RmK8CDoqGtt8976kFkJv0zELkEAlwebnp_io,380
@@ -47,8 +49,8 @@ gemcode/workspace_hints.py,sha256=WQFwyoGnVrzzYSl0s5MNcd1-UP11Ao8KGgrdFIB5m9g,62
47
49
  gemcode/computer_use/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
50
  gemcode/computer_use/browser_computer.py,sha256=bWXXgj7K6ZrDqT49iPEcnBd_ZJBwRxSgKhzKaJpn4cQ,13212
49
51
  gemcode/memory/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
- gemcode/memory/embedding_memory_service.py,sha256=45mp4dDXMxdtafIq6LBPbFs5iij6lRnp4M9Zinvw9eY,8544
51
- gemcode/memory/file_memory_service.py,sha256=LLe-aRnEzyZ9FYPb4z_czwkR8D16ACwhCspbUc_BSM4,4909
52
+ gemcode/memory/embedding_memory_service.py,sha256=4iMZUw80GY8SPrJcuT4CwOsTmZ600SOuYkm-nv2c634,9422
53
+ gemcode/memory/file_memory_service.py,sha256=yAXCspfSPBfQXDrwPX8ZZsqaprnC_CKvgmN_GiQVzuo,6092
52
54
  gemcode/plugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
55
  gemcode/plugins/terminal_hooks_plugin.py,sha256=zxmWuEHyZ6-C7yhu27v9YJUI6ZZQaJJyP4JLMbX-7YA,5673
54
56
  gemcode/plugins/tool_recovery_plugin.py,sha256=N3nMDMuthwGJNqpV3tjXLdCYjUl9Dlwf-xtzZ5QkqZA,3876
@@ -59,21 +61,22 @@ gemcode/query/engine.py,sha256=GPuvUgTRpWmyA39I_3ayVEADlHVFhPrC2FGW_yKs2yw,1420
59
61
  gemcode/query/stop_hooks.py,sha256=jaXMN2OptwHeGXF8o630zIVr62T8jVg-eIyREG0GyxA,1847
60
62
  gemcode/query/token_budget.py,sha256=JTq2TrGkFY5t5KOs-P9XQPqahyjcdTzN3wctZ1JxFV0,2973
61
63
  gemcode/query/transitions.py,sha256=vJ77cv4cFwdvsxyGr7MxXz6uGVB5IDqOClqR1MkWvqw,933
62
- gemcode/tools/__init__.py,sha256=SiNU28a3wZ-022Mf1NeZGWs4N_0M_LRbkViV0OquoYg,3665
63
- gemcode/tools/bash.py,sha256=aW-sCrT1GJOcFEB-cKqImuInv9atWNeu5rTZqsVtB84,12248
64
+ gemcode/tools/__init__.py,sha256=nSHcbts_cgAPGa2izrvczXJAHqtPjah8YhzbzERoGTI,4608
65
+ gemcode/tools/bash.py,sha256=u9z7OI3iBGl5Df_GGipDsR8i3OcN59juw1hQHbO1PPk,12765
64
66
  gemcode/tools/browser.py,sha256=StWRttiyGkR4qaG5urRviJgdoj2hiFB2OuHPItaVzJY,7250
65
67
  gemcode/tools/edit.py,sha256=1fo-wlRT6fyuib0JnmUbPVG3cpjKNJsnmj9jxTgrZTs,3182
66
- gemcode/tools/filesystem.py,sha256=1taQpeGM4JlrjPdj56MIOCGbSQ0heYn969YmKG6QTi4,6124
68
+ gemcode/tools/filesystem.py,sha256=LfdPcGciW4z1-fKgfwjQC7cvMlfCNymCNtGh8-vEdKc,6466
67
69
  gemcode/tools/notebook.py,sha256=zR-bVU6mniJYI9u2RUgm0BmnHmTc-TmEETzLBsXzueA,9197
68
70
  gemcode/tools/notes.py,sha256=7Rp7eJPANELDBKfwNPzi4pco_uP3GP07vNun9YJxOCw,4544
69
- gemcode/tools/search.py,sha256=SAK88xq-VvGD2ZmEZjipUPNA87ZmiNaen7uavprYHtc,5618
70
- gemcode/tools/shell.py,sha256=0F0MpUIC3zbbCmBfyQhNAKaEctlb-8ECH7ZK99pAsho,5403
71
+ gemcode/tools/repo_map.py,sha256=hFjLcuZpJrlj08kJd2iXKIpTQRYAJQyaKPnuVNemZbQ,3650
72
+ gemcode/tools/search.py,sha256=1I41K5galCa8hTHLZTc7OsFWGz0e92QsEyd7Jw6veUQ,5986
73
+ gemcode/tools/shell.py,sha256=K9wstCKAasCxNTuu_s3w7yDZ_esXe-fgGH7UKcadOvc,5667
71
74
  gemcode/tools/shell_gate.py,sha256=8wLfdjBsfgH8PnMaEIjdJWMJMtvLMGjgP93lFMcbFqE,929
72
- gemcode/tools/subtask.py,sha256=qyMH-hYBgE7h-rs15ERHX2h2IxaXqQTc-SH2_UCc0XA,8500
75
+ gemcode/tools/subtask.py,sha256=mmuVp7BazlZiJQk-sI72TmtSBq7omC4H43jQ-X8YJ3I,10070
73
76
  gemcode/tools/tasks.py,sha256=sPb9Ru0BBBcx_BMqLJBeOws6OR_-R0gyHAaotulsBBk,7657
74
77
  gemcode/tools/think.py,sha256=WrNATR-bi97aLkbSsOFOYYAGxbzihe9AnPDZfw3z5-Q,1704
75
78
  gemcode/tools/todo.py,sha256=jzhqu-lczlO0-576zZsv4rJqpxZi_Gx46JBLNGugt1w,5810
76
- gemcode/tools/web.py,sha256=ULg1e3inG4FjPSUCYI8dVBzTrcCHINNRo76SIU9qw-A,4489
79
+ gemcode/tools/web.py,sha256=Oect8wEX6jOcrIESxb6k3NSHlMPyv4OoHupttjDuoJ4,5078
77
80
  gemcode/tools/web_search.py,sha256=oD6dXyvaalYlzZxiURETpUekpJBo3F3K5TYGx_wrrH0,8816
78
81
  gemcode/tui/input_handler.py,sha256=Off7hBXXqw7JxkKzlmtMHtg25Y9dkO275dC35brK-TE,10676
79
82
  gemcode/tui/scrollback.py,sha256=4_dKi_k43Pz7c-Kpm7pM35494piQiDL4ZZpuTdqwpdk,33079
@@ -83,9 +86,9 @@ gemcode/tui/welcome_rich.py,sha256=8FEZzLXrzqly5JWiDgV9ooRV1LNXDk-CXV1a7K6ua-U,4
83
86
  gemcode/web/__init__.py,sha256=EysmUAWs6g-lmMk4VFljKfaHVrEgb_FiIzwQmBdORJc,40
84
87
  gemcode/web/claude_sse_adapter.py,sha256=HcNp0Lh4DdBZBLOpstsqa-VzfqAUrRngZ6FSuJ-mIMg,8609
85
88
  gemcode/web/terminal_repl.py,sha256=k2irvFGbCY8gDm_pbirR7b_cakaeafcctoTIvnJkVXk,3902
86
- gemcode-0.3.65.dist-info/licenses/LICENSE,sha256=TD4524qn-W8Z07GTDnag-9jJPFutFZNB0a1WbMHPC54,8388
87
- gemcode-0.3.65.dist-info/METADATA,sha256=xSz_xChNe0HbpI1ThKlNNNbM1EvvLkRBo2t0-khUdGo,23695
88
- gemcode-0.3.65.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
89
- gemcode-0.3.65.dist-info/entry_points.txt,sha256=cZdLTLDiHbks7OSUCuxCh66dCWeQdpLR8BozoqfEjV4,45
90
- gemcode-0.3.65.dist-info/top_level.txt,sha256=UYrjULLBY2bcgK6KI6flomJWmsbDXu7n0rvW2SWFrbo,8
91
- gemcode-0.3.65.dist-info/RECORD,,
89
+ gemcode-0.3.67.dist-info/licenses/LICENSE,sha256=TD4524qn-W8Z07GTDnag-9jJPFutFZNB0a1WbMHPC54,8388
90
+ gemcode-0.3.67.dist-info/METADATA,sha256=rZb2AHzSETy2XUb5L5w88ECMQWXoQmNKhKOJ78SyGcs,23695
91
+ gemcode-0.3.67.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
92
+ gemcode-0.3.67.dist-info/entry_points.txt,sha256=cZdLTLDiHbks7OSUCuxCh66dCWeQdpLR8BozoqfEjV4,45
93
+ gemcode-0.3.67.dist-info/top_level.txt,sha256=UYrjULLBY2bcgK6KI6flomJWmsbDXu7n0rvW2SWFrbo,8
94
+ gemcode-0.3.67.dist-info/RECORD,,