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.
- {gemcode-0.3.64/src/gemcode.egg-info → gemcode-0.3.66}/PKG-INFO +1 -1
- {gemcode-0.3.64 → gemcode-0.3.66}/pyproject.toml +1 -1
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/agent.py +20 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/callbacks.py +27 -3
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/config.py +9 -2
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/memory/embedding_memory_service.py +28 -2
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/memory/file_memory_service.py +36 -1
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/repl_commands.py +1 -1
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/repl_slash.py +2 -2
- gemcode-0.3.66/src/gemcode/tool_result_store.py +162 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/__init__.py +26 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/bash.py +2 -2
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/filesystem.py +1 -1
- gemcode-0.3.66/src/gemcode/tools/repo_map.py +132 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/search.py +1 -1
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/shell.py +2 -2
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/subtask.py +40 -3
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/web.py +2 -2
- {gemcode-0.3.64 → gemcode-0.3.66/src/gemcode.egg-info}/PKG-INFO +1 -1
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode.egg-info/SOURCES.txt +2 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/LICENSE +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/MANIFEST.in +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/README.md +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/setup.cfg +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/__init__.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/__main__.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/audit.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/cli.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/compaction.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/credentials.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/hooks.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/intent_classifier.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/interactions.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/invoke.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/kairos_daemon.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/limits.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/openapi_loader.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/paths.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/permissions.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/pricing.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/query/config.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/refine.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/review_agent.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/session_store.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/thinking.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/browser.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/notebook.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/notes.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/tasks.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/think.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools/web_search.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/trust.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tui/input_handler.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tui/scrollback.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tui/spinner.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tui/welcome_banner.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/tui/welcome_rich.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/version.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/vertex.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/web/claude_sse_adapter.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_autocompact.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_capability_routing.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_claude_web_adapter_sse.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_cli_init.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_context_budget.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_context_warning.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_credentials.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_kairos_scheduler.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_modality_tools.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_model_errors.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_model_routing.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_paths.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_permissions.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_repl_commands.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_repl_slash.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_slash_commands.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_thinking_config.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_token_budget.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_tools.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.3.64 → gemcode-0.3.66}/tests/test_workspace_hints.py +0 -0
|
@@ -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
|
|
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=
|
|
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 =
|
|
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)
|
|
@@ -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
|
|
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
|
|
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
|
|
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[:
|
|
186
|
-
stderr = proc.stderr[:
|
|
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 ".",
|
|
@@ -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
|
+
|
|
@@ -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[:
|
|
165
|
-
"stderr": proc.stderr[:
|
|
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
|
-
|
|
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
|
|
@@ -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,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
|
|
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"}
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|