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