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 +24 -0
- gemcode/callbacks.py +45 -5
- gemcode/config.py +13 -0
- gemcode/dynamic_policy.py +117 -0
- gemcode/memory/embedding_memory_service.py +28 -2
- gemcode/memory/file_memory_service.py +36 -1
- gemcode/tool_result_store.py +162 -0
- gemcode/tools/__init__.py +32 -0
- gemcode/tools/bash.py +15 -2
- gemcode/tools/filesystem.py +10 -1
- gemcode/tools/repo_map.py +132 -0
- gemcode/tools/search.py +10 -1
- gemcode/tools/shell.py +10 -2
- gemcode/tools/subtask.py +40 -3
- gemcode/tools/web.py +14 -2
- {gemcode-0.3.65.dist-info → gemcode-0.3.67.dist-info}/METADATA +1 -1
- {gemcode-0.3.65.dist-info → gemcode-0.3.67.dist-info}/RECORD +21 -18
- {gemcode-0.3.65.dist-info → gemcode-0.3.67.dist-info}/WHEEL +0 -0
- {gemcode-0.3.65.dist-info → gemcode-0.3.67.dist-info}/entry_points.txt +0 -0
- {gemcode-0.3.65.dist-info → gemcode-0.3.67.dist-info}/licenses/LICENSE +0 -0
- {gemcode-0.3.65.dist-info → gemcode-0.3.67.dist-info}/top_level.txt +0 -0
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
|
-
|
|
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(
|
|
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 =
|
|
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":
|
|
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":
|
|
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
|
-
|
|
186
|
-
|
|
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 ".",
|
gemcode/tools/filesystem.py
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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[:
|
|
165
|
-
"stderr": proc.stderr[:
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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 =
|
|
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
|
|
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,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=
|
|
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=
|
|
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=
|
|
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=
|
|
51
|
-
gemcode/memory/file_memory_service.py,sha256=
|
|
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=
|
|
63
|
-
gemcode/tools/bash.py,sha256=
|
|
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=
|
|
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/
|
|
70
|
-
gemcode/tools/
|
|
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=
|
|
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=
|
|
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.
|
|
87
|
-
gemcode-0.3.
|
|
88
|
-
gemcode-0.3.
|
|
89
|
-
gemcode-0.3.
|
|
90
|
-
gemcode-0.3.
|
|
91
|
-
gemcode-0.3.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|