gemcode 0.3.66__tar.gz → 0.3.68__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. {gemcode-0.3.66/src/gemcode.egg-info → gemcode-0.3.68}/PKG-INFO +1 -1
  2. {gemcode-0.3.66 → gemcode-0.3.68}/pyproject.toml +1 -1
  3. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/agent.py +4 -0
  4. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/callbacks.py +39 -4
  5. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/config.py +16 -0
  6. gemcode-0.3.68/src/gemcode/dynamic_policy.py +143 -0
  7. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/invoke.py +26 -0
  8. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/tools/__init__.py +6 -0
  9. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/tools/bash.py +15 -2
  10. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/tools/filesystem.py +9 -0
  11. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/tools/search.py +9 -0
  12. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/tools/shell.py +10 -2
  13. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/tools/web.py +12 -0
  14. {gemcode-0.3.66 → gemcode-0.3.68/src/gemcode.egg-info}/PKG-INFO +1 -1
  15. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode.egg-info/SOURCES.txt +1 -0
  16. {gemcode-0.3.66 → gemcode-0.3.68}/LICENSE +0 -0
  17. {gemcode-0.3.66 → gemcode-0.3.68}/MANIFEST.in +0 -0
  18. {gemcode-0.3.66 → gemcode-0.3.68}/README.md +0 -0
  19. {gemcode-0.3.66 → gemcode-0.3.68}/setup.cfg +0 -0
  20. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/__init__.py +0 -0
  21. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/__main__.py +0 -0
  22. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/audit.py +0 -0
  23. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/autocompact.py +0 -0
  24. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/capability_routing.py +0 -0
  25. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/cli.py +0 -0
  26. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/compaction.py +0 -0
  27. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/computer_use/__init__.py +0 -0
  28. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/computer_use/browser_computer.py +0 -0
  29. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/context_budget.py +0 -0
  30. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/context_warning.py +0 -0
  31. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/credentials.py +0 -0
  32. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/hitl_session.py +0 -0
  33. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/hooks.py +0 -0
  34. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/intent_classifier.py +0 -0
  35. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/interactions.py +0 -0
  36. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/kairos_daemon.py +0 -0
  37. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/limits.py +0 -0
  38. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/live_audio_engine.py +0 -0
  39. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/logging_config.py +0 -0
  40. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/mcp_loader.py +0 -0
  41. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/memory/__init__.py +0 -0
  42. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/memory/embedding_memory_service.py +0 -0
  43. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/memory/file_memory_service.py +0 -0
  44. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/modality_tools.py +0 -0
  45. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/model_errors.py +0 -0
  46. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/model_routing.py +0 -0
  47. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/openapi_loader.py +0 -0
  48. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/paths.py +0 -0
  49. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/permissions.py +0 -0
  50. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/plugins/__init__.py +0 -0
  51. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  52. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  53. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/pricing.py +0 -0
  54. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/prompt_suggestions.py +0 -0
  55. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/query/__init__.py +0 -0
  56. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/query/config.py +0 -0
  57. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/query/deps.py +0 -0
  58. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/query/engine.py +0 -0
  59. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/query/stop_hooks.py +0 -0
  60. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/query/token_budget.py +0 -0
  61. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/query/transitions.py +0 -0
  62. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/refine.py +0 -0
  63. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/repl_commands.py +0 -0
  64. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/repl_slash.py +0 -0
  65. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/review_agent.py +0 -0
  66. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/session_runtime.py +0 -0
  67. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/session_store.py +0 -0
  68. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/slash_commands.py +0 -0
  69. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/thinking.py +0 -0
  70. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/tool_prompt_manifest.py +0 -0
  71. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/tool_registry.py +0 -0
  72. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/tool_result_store.py +0 -0
  73. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/tools/browser.py +0 -0
  74. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/tools/edit.py +0 -0
  75. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/tools/notebook.py +0 -0
  76. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/tools/notes.py +0 -0
  77. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/tools/repo_map.py +0 -0
  78. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/tools/shell_gate.py +0 -0
  79. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/tools/subtask.py +0 -0
  80. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/tools/tasks.py +0 -0
  81. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/tools/think.py +0 -0
  82. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/tools/todo.py +0 -0
  83. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/tools/web_search.py +0 -0
  84. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/tools_inspector.py +0 -0
  85. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/trust.py +0 -0
  86. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/tui/input_handler.py +0 -0
  87. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/tui/scrollback.py +0 -0
  88. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/tui/spinner.py +0 -0
  89. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/tui/welcome_banner.py +0 -0
  90. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/tui/welcome_rich.py +0 -0
  91. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/version.py +0 -0
  92. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/vertex.py +0 -0
  93. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/web/__init__.py +0 -0
  94. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/web/claude_sse_adapter.py +0 -0
  95. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/web/terminal_repl.py +0 -0
  96. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode/workspace_hints.py +0 -0
  97. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode.egg-info/dependency_links.txt +0 -0
  98. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode.egg-info/entry_points.txt +0 -0
  99. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode.egg-info/requires.txt +0 -0
  100. {gemcode-0.3.66 → gemcode-0.3.68}/src/gemcode.egg-info/top_level.txt +0 -0
  101. {gemcode-0.3.66 → gemcode-0.3.68}/tests/test_agent_instruction.py +0 -0
  102. {gemcode-0.3.66 → gemcode-0.3.68}/tests/test_autocompact.py +0 -0
  103. {gemcode-0.3.66 → gemcode-0.3.68}/tests/test_capability_routing.py +0 -0
  104. {gemcode-0.3.66 → gemcode-0.3.68}/tests/test_claude_web_adapter_sse.py +0 -0
  105. {gemcode-0.3.66 → gemcode-0.3.68}/tests/test_cli_init.py +0 -0
  106. {gemcode-0.3.66 → gemcode-0.3.68}/tests/test_computer_use_permissions.py +0 -0
  107. {gemcode-0.3.66 → gemcode-0.3.68}/tests/test_context_budget.py +0 -0
  108. {gemcode-0.3.66 → gemcode-0.3.68}/tests/test_context_warning.py +0 -0
  109. {gemcode-0.3.66 → gemcode-0.3.68}/tests/test_credentials.py +0 -0
  110. {gemcode-0.3.66 → gemcode-0.3.68}/tests/test_interactive_permission_ask.py +0 -0
  111. {gemcode-0.3.66 → gemcode-0.3.68}/tests/test_kairos_scheduler.py +0 -0
  112. {gemcode-0.3.66 → gemcode-0.3.68}/tests/test_modality_tools.py +0 -0
  113. {gemcode-0.3.66 → gemcode-0.3.68}/tests/test_model_error_retry.py +0 -0
  114. {gemcode-0.3.66 → gemcode-0.3.68}/tests/test_model_errors.py +0 -0
  115. {gemcode-0.3.66 → gemcode-0.3.68}/tests/test_model_routing.py +0 -0
  116. {gemcode-0.3.66 → gemcode-0.3.68}/tests/test_paths.py +0 -0
  117. {gemcode-0.3.66 → gemcode-0.3.68}/tests/test_permissions.py +0 -0
  118. {gemcode-0.3.66 → gemcode-0.3.68}/tests/test_prompt_suggestions.py +0 -0
  119. {gemcode-0.3.66 → gemcode-0.3.68}/tests/test_repl_commands.py +0 -0
  120. {gemcode-0.3.66 → gemcode-0.3.68}/tests/test_repl_slash.py +0 -0
  121. {gemcode-0.3.66 → gemcode-0.3.68}/tests/test_slash_commands.py +0 -0
  122. {gemcode-0.3.66 → gemcode-0.3.68}/tests/test_thinking_config.py +0 -0
  123. {gemcode-0.3.66 → gemcode-0.3.68}/tests/test_token_budget.py +0 -0
  124. {gemcode-0.3.66 → gemcode-0.3.68}/tests/test_tool_context_circulation.py +0 -0
  125. {gemcode-0.3.66 → gemcode-0.3.68}/tests/test_tools.py +0 -0
  126. {gemcode-0.3.66 → gemcode-0.3.68}/tests/test_tools_inspector.py +0 -0
  127. {gemcode-0.3.66 → gemcode-0.3.68}/tests/test_workspace_hints.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.66
3
+ Version: 0.3.68
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gemcode"
7
- version = "0.3.66"
7
+ version = "0.3.68"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -517,6 +517,10 @@ You have native deep thinking capability — use it actively:
517
517
  - When something fails, diagnose (re-read the error, check assumptions) before switching strategy. Do not repeat the same failed call.
518
518
  - When asked to analyse or explain something: read the actual files, produce concrete findings, not hypotheses.
519
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
+
520
524
  ## Tool selection guide (only when needed)
521
525
 
522
526
  Keep tool usage minimal. Prefer short, targeted calls and keep tool outputs small.
@@ -315,10 +315,18 @@ def make_after_tool_callback(cfg: GemCodeConfig):
315
315
  name = getattr(tool, "name", None) or ""
316
316
 
317
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
+
318
326
  if (
319
327
  isinstance(tool_response, dict)
320
328
  and getattr(cfg, "tool_result_offload_enabled", False)
321
- and getattr(cfg, "tool_result_max_chars", 0) > 0
329
+ and effective_tool_chars > 0
322
330
  ):
323
331
  try:
324
332
  from gemcode.tool_result_store import maybe_offload_tool_result
@@ -326,7 +334,7 @@ def make_after_tool_callback(cfg: GemCodeConfig):
326
334
  project_root=cfg.project_root,
327
335
  tool_name=name,
328
336
  payload=tool_response,
329
- max_inline_chars=int(cfg.tool_result_max_chars),
337
+ max_inline_chars=int(effective_tool_chars),
330
338
  )
331
339
  if did and isinstance(new_payload, dict):
332
340
  tool_response = new_payload
@@ -334,9 +342,9 @@ def make_after_tool_callback(cfg: GemCodeConfig):
334
342
  except Exception:
335
343
  pass
336
344
 
337
- if isinstance(tool_response, dict) and getattr(cfg, "tool_result_max_chars", 0) > 0:
345
+ if isinstance(tool_response, dict) and effective_tool_chars > 0:
338
346
  new_d, did = truncate_tool_result_dict(
339
- tool_response, int(cfg.tool_result_max_chars)
347
+ tool_response, int(effective_tool_chars)
340
348
  )
341
349
  if did:
342
350
  tool_response = new_d
@@ -376,6 +384,25 @@ def make_after_tool_callback(cfg: GemCodeConfig):
376
384
  st[_STATE_FAILURE_KEY] = 0
377
385
  else:
378
386
  st[_STATE_FAILURE_KEY] = 0
387
+
388
+ # Risk feedback: if tools are failing or commands return non-zero, treat the
389
+ # task as higher-risk and allow more evidence in subsequent tool outputs.
390
+ try:
391
+ cur = float(getattr(cfg, "_risk_score", 0.0) or 0.0)
392
+ bump = 0.0
393
+ if err:
394
+ bump += 0.15
395
+ if isinstance(tool_response, dict) and isinstance(tool_response.get("exit_code"), int):
396
+ if int(tool_response["exit_code"]) != 0:
397
+ bump += 0.10
398
+ # decay slowly when things are healthy
399
+ if bump == 0.0:
400
+ cur = max(0.0, cur * 0.90)
401
+ else:
402
+ cur = min(1.0, cur + bump)
403
+ object.__setattr__(cfg, "_risk_score", cur)
404
+ except Exception:
405
+ pass
379
406
  # ── Shell hooks: post_tool_use ────────────────────────────────────────
380
407
  try:
381
408
  from gemcode.hooks import run_post_tool_use_hook
@@ -532,6 +559,14 @@ def make_after_model_callback(cfg: GemCodeConfig):
532
559
  st[_LAST_PROMPT_TOKENS] = pt
533
560
  st[_LAST_CONTEXT_PCT] = cw.get("percent_left")
534
561
  st[_LAST_CONTEXT_LEVEL] = level
562
+ # Expose to tool layer (dynamic token policy).
563
+ try:
564
+ pct = cw.get("percent_left")
565
+ if isinstance(pct, int):
566
+ object.__setattr__(cfg, "_context_percent_left", pct)
567
+ object.__setattr__(cfg, "_context_alert_level", int(level))
568
+ except Exception:
569
+ pass
535
570
  append_audit(
536
571
  cfg.project_root,
537
572
  {
@@ -137,6 +137,22 @@ class GemCodeConfig:
137
137
  tool_result_offload_enabled: bool = field(
138
138
  default_factory=lambda: _truthy_env("GEMCODE_TOOL_RESULT_OFFLOAD", default=True)
139
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
+ )
146
+
147
+ # Dynamic risk policy: boosts caps when the current task appears risky/complex.
148
+ dynamic_risk_policy: bool = field(
149
+ default_factory=lambda: _truthy_env("GEMCODE_DYNAMIC_RISK_POLICY", default=True)
150
+ )
151
+
152
+ # How much the risk score can expand caps (0.0-1.5 reasonable).
153
+ dynamic_risk_boost: float = field(
154
+ default_factory=lambda: float(os.environ.get("GEMCODE_DYNAMIC_RISK_BOOST", "0.6"))
155
+ )
140
156
  # Trim oldest text in llm_request.contents when over budget (see context_budget.py).
141
157
  context_shrink_enabled: bool = field(
142
158
  default_factory=lambda: _truthy_env("GEMCODE_CONTEXT_SHRINK", default=True)
@@ -0,0 +1,143 @@
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
+ def _risk(cfg) -> float:
36
+ try:
37
+ v = getattr(cfg, "_risk_score", None)
38
+ if isinstance(v, (int, float)):
39
+ return float(v)
40
+ except Exception:
41
+ return 0.0
42
+ return 0.0
43
+
44
+
45
+ def _clamp(x: float, lo: float, hi: float) -> float:
46
+ return lo if x < lo else hi if x > hi else x
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class DynamicCaps:
51
+ tool_inline_chars: int
52
+ read_file_max_bytes: int
53
+ web_fetch_max_chars: int
54
+ bash_stdout_chars: int
55
+ bash_stderr_chars: int
56
+ run_stdout_chars: int
57
+ run_stderr_chars: int
58
+ grep_max_matches: int
59
+
60
+
61
+ def get_dynamic_caps(cfg) -> DynamicCaps:
62
+ """
63
+ Compute caps based on current context pressure.
64
+
65
+ Policy:
66
+ - Healthy (>=45% left): generous caps (better evidence, less re-asking).
67
+ - Warning (20-44%): moderate caps.
68
+ - Tight (<20%): strict caps + prefer offload.
69
+
70
+ Then apply a risk-based boost (if enabled) so complex tasks stay evidence-rich.
71
+ """
72
+ # cfg can be None in some tool contexts; treat as enabled with defaults.
73
+ enabled = _truthy(getattr(cfg, "dynamic_token_policy", True) if cfg is not None else True, default=True)
74
+ if not enabled:
75
+ # Essentially "no-op" high caps; tools still apply their explicit maxes.
76
+ return DynamicCaps(
77
+ tool_inline_chars=int(getattr(cfg, "tool_result_max_chars", 12000) or 12000),
78
+ read_file_max_bytes=200_000,
79
+ web_fetch_max_chars=40_000,
80
+ bash_stdout_chars=80_000,
81
+ bash_stderr_chars=20_000,
82
+ run_stdout_chars=50_000,
83
+ run_stderr_chars=50_000,
84
+ grep_max_matches=80,
85
+ )
86
+
87
+ pct = _pct_left(cfg) if cfg is not None else None
88
+ if pct is None:
89
+ pct = 35
90
+
91
+ # Base knobs from config (so users can still tune globally).
92
+ base_tool = int(getattr(cfg, "tool_result_max_chars", 12000) or 12000) if cfg is not None else 12000
93
+ base_tool = max(1000, base_tool)
94
+
95
+ # Risk boost: scale caps upward for risky tasks, but keep bounded.
96
+ risk_enabled = _truthy(getattr(cfg, "dynamic_risk_policy", True) if cfg is not None else True, default=True)
97
+ risk_boost = float(getattr(cfg, "dynamic_risk_boost", 0.6) if cfg is not None else 0.6)
98
+ risk_score = _risk(cfg) if (cfg is not None and risk_enabled) else 0.0
99
+ risk_score = _clamp(risk_score, 0.0, 1.0)
100
+ boost = 1.0 + (_clamp(risk_boost, 0.0, 1.5) * risk_score)
101
+
102
+ def _scale(n: int, *, cap: int) -> int:
103
+ return min(cap, max(1000, int(n * boost)))
104
+
105
+ if pct >= 45:
106
+ mult = 1.4
107
+ return DynamicCaps(
108
+ tool_inline_chars=_scale(min(24_000, int(base_tool * mult)), cap=30_000),
109
+ read_file_max_bytes=min(200_000, int(140_000 * boost)),
110
+ web_fetch_max_chars=min(60_000, int(30_000 * boost)),
111
+ bash_stdout_chars=min(80_000, int(30_000 * boost)),
112
+ bash_stderr_chars=min(40_000, int(15_000 * boost)),
113
+ run_stdout_chars=min(80_000, int(30_000 * boost)),
114
+ run_stderr_chars=min(80_000, int(30_000 * boost)),
115
+ grep_max_matches=min(200, int(60 * boost)),
116
+ )
117
+
118
+ if pct >= 20:
119
+ mult = 1.0
120
+ return DynamicCaps(
121
+ tool_inline_chars=_scale(min(18_000, int(base_tool * mult)), cap=24_000),
122
+ read_file_max_bytes=min(160_000, int(80_000 * boost)),
123
+ web_fetch_max_chars=min(40_000, int(20_000 * boost)),
124
+ bash_stdout_chars=min(50_000, int(20_000 * boost)),
125
+ bash_stderr_chars=min(30_000, int(10_000 * boost)),
126
+ run_stdout_chars=min(50_000, int(20_000 * boost)),
127
+ run_stderr_chars=min(50_000, int(20_000 * boost)),
128
+ grep_max_matches=min(120, int(40 * boost)),
129
+ )
130
+
131
+ # Tight
132
+ mult = 0.6
133
+ return DynamicCaps(
134
+ tool_inline_chars=max(2000, min(12_000, int(base_tool * mult * boost))),
135
+ read_file_max_bytes=min(90_000, int(35_000 * boost)),
136
+ web_fetch_max_chars=min(20_000, int(10_000 * boost)),
137
+ bash_stdout_chars=min(25_000, int(10_000 * boost)),
138
+ bash_stderr_chars=min(20_000, int(8_000 * boost)),
139
+ run_stdout_chars=min(25_000, int(10_000 * boost)),
140
+ run_stderr_chars=min(25_000, int(10_000 * boost)),
141
+ grep_max_matches=min(80, int(20 * boost)),
142
+ )
143
+
@@ -68,6 +68,32 @@ async def run_turn(
68
68
  cfg: "GemCodeConfig | None" = None,
69
69
  ) -> list:
70
70
  """Execute one user message; collect all Events (caller aggregates text)."""
71
+ # Dynamic risk score: updated each user message; later refined by tool outcomes.
72
+ # This is intentionally heuristic but configurable via env knobs.
73
+ if cfg is not None:
74
+ try:
75
+ import re
76
+ p = (prompt or "")[:20_000]
77
+ risk = 0.0
78
+ # Complexity signals
79
+ if len(p) > 600:
80
+ risk += 0.15
81
+ if len(p) > 2000:
82
+ risk += 0.15
83
+ if re.search(r"\\b(refactor|migrate|rewrite|optimi[sz]e|architecture)\\b", p, re.I):
84
+ risk += 0.2
85
+ if re.search(r"\\b(bug|fix|regression|error|traceback|failing)\\b", p, re.I):
86
+ risk += 0.2
87
+ if re.search(r"\\b(test|pytest|ci|build|deploy|release)\\b", p, re.I):
88
+ risk += 0.1
89
+ # Multi-file hints
90
+ if p.count(\"/\") >= 6 or p.count(\".py\") + p.count(\".ts\") + p.count(\".tsx\") >= 3:
91
+ risk += 0.1
92
+ # Clamp 0..1
93
+ risk = max(0.0, min(1.0, float(risk)))
94
+ object.__setattr__(cfg, \"_risk_score\", risk)
95
+ except Exception:
96
+ pass
71
97
  run_config = (
72
98
  RunConfig(max_llm_calls=max_llm_calls) if max_llm_calls is not None else None
73
99
  )
@@ -84,6 +84,12 @@ def build_function_tools(cfg: GemCodeConfig, *, include_subtask: bool = True) ->
84
84
  load_tool_result = _make_load_tool_result_tool(cfg)
85
85
  repo_map = make_repo_map_tool(cfg)
86
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
92
+
87
93
  # bash and run_command are the most common long-running tools (builds, tests,
88
94
  # installs). Wrap them with LongRunningFunctionTool so ADK can handle slow
89
95
  # processes without hitting streaming timeouts.
@@ -182,8 +182,21 @@ def make_bash_tool(cfg: GemCodeConfig):
182
182
  env=env,
183
183
  check=False,
184
184
  )
185
- stdout = proc.stdout[:20_000]
186
- stderr = proc.stderr[:10_000]
185
+ try:
186
+ from gemcode.dynamic_policy import get_dynamic_caps
187
+ caps = get_dynamic_caps(cfg)
188
+ stdout_cap = caps.bash_stdout_chars
189
+ stderr_cap = caps.bash_stderr_chars
190
+ except Exception:
191
+ stdout_cap = 20_000
192
+ stderr_cap = 10_000
193
+
194
+ # Keep more stderr when failing; it is usually the most informative.
195
+ if proc.returncode != 0:
196
+ stderr_cap = max(stderr_cap, 12_000)
197
+
198
+ stdout = proc.stdout[:stdout_cap]
199
+ stderr = proc.stderr[:stderr_cap]
187
200
  result: dict = {
188
201
  "command": command,
189
202
  "cwd": str(exec_cwd.relative_to(root)) if exec_cwd != root else ".",
@@ -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")
@@ -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:
@@ -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[:20_000],
165
- "stderr": proc.stderr[:20_000],
172
+ "stdout": proc.stdout[:out_cap],
173
+ "stderr": proc.stderr[:err_cap],
166
174
  }
167
175
  except subprocess.TimeoutExpired:
168
176
  return {"error": f"Timeout after {timeout_seconds}s"}
@@ -71,6 +71,18 @@ def make_web_fetch_tool():
71
71
  url = url.strip()
72
72
  if not url.startswith(("http://", "https://")):
73
73
  return {"error": "Only http:// and https:// URLs are supported"}
74
+ try:
75
+ from gemcode.dynamic_policy import get_dynamic_caps
76
+ caps = get_dynamic_caps(getattr(web_fetch, "_cfg", None) or None) # type: ignore[arg-type]
77
+ except Exception:
78
+ caps = None
79
+ try:
80
+ # make_web_fetch_tool() has no cfg, so we attach one in the builder.
81
+ # If present, apply the dynamic cap.
82
+ if caps is not None and max_chars > getattr(caps, "web_fetch_max_chars", 20_000):
83
+ max_chars = int(getattr(caps, "web_fetch_max_chars", 20_000))
84
+ except Exception:
85
+ pass
74
86
  if max_chars < 1000:
75
87
  max_chars = 1000
76
88
  if max_chars > 200_000:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.66
3
+ Version: 0.3.68
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -15,6 +15,7 @@ src/gemcode/config.py
15
15
  src/gemcode/context_budget.py
16
16
  src/gemcode/context_warning.py
17
17
  src/gemcode/credentials.py
18
+ src/gemcode/dynamic_policy.py
18
19
  src/gemcode/hitl_session.py
19
20
  src/gemcode/hooks.py
20
21
  src/gemcode/intent_classifier.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