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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
gemcode/callbacks.py CHANGED
@@ -43,6 +43,8 @@ _CTX_WARN_LEVEL_NOTIFIED = "gemcode:ctx_warn_level_notified"
43
43
  _LAST_PROMPT_TOKENS = "gemcode:last_prompt_tokens"
44
44
  _LAST_CONTEXT_PCT = "gemcode:last_context_percent_left"
45
45
  _LAST_CONTEXT_LEVEL = "gemcode:last_context_alert_level"
46
+ _RISK_FILES_TOUCHED = "gemcode:risk_files_touched"
47
+ _RISK_TOOL_CALLS = "gemcode:risk_tool_calls"
46
48
 
47
49
  def _truthy_env(name: str, *, default: bool = False) -> bool:
48
50
  v = os.environ.get(name)
@@ -141,6 +143,36 @@ def make_before_tool_callback(cfg: GemCodeConfig):
141
143
  record = {"tool": name, "args": _redact_args(name, args)}
142
144
  append_audit(cfg.project_root, record)
143
145
 
146
+ # Dynamic risk signals from actual repo interaction.
147
+ try:
148
+ if tool_context is not None:
149
+ st = tool_context.state
150
+ st[_RISK_TOOL_CALLS] = int(st.get(_RISK_TOOL_CALLS, 0) or 0) + 1
151
+ if name == "read_file":
152
+ p = (args or {}).get("path")
153
+ if isinstance(p, str) and p.strip():
154
+ touched: set[str] = set(st.get(_RISK_FILES_TOUCHED, []) or [])
155
+ touched.add(p.strip())
156
+ # Store as list for JSON-serializable session state.
157
+ st[_RISK_FILES_TOUCHED] = list(sorted(touched))[:200]
158
+ # More files touched => higher complexity.
159
+ n = len(touched)
160
+ cur = float(getattr(cfg, "_risk_score", 0.0) or 0.0)
161
+ if n >= 10:
162
+ cur = min(1.0, cur + 0.08)
163
+ elif n >= 5:
164
+ cur = min(1.0, cur + 0.04)
165
+ object.__setattr__(cfg, "_risk_score", cur)
166
+ # Writes / shell are inherently higher risk; allow more evidence.
167
+ if name in MUTATING_TOOLS:
168
+ cur = float(getattr(cfg, "_risk_score", 0.0) or 0.0)
169
+ object.__setattr__(cfg, "_risk_score", min(1.0, cur + 0.12))
170
+ if name in SHELL_TOOLS:
171
+ cur = float(getattr(cfg, "_risk_score", 0.0) or 0.0)
172
+ object.__setattr__(cfg, "_risk_score", min(1.0, cur + 0.08))
173
+ except Exception:
174
+ pass
175
+
144
176
  # ── Shell hooks: pre_tool_use ─────────────────────────────────────────
145
177
  # If the project has a .gemcode/hooks/pre_tool_use.sh, run it now.
146
178
  # Non-zero exit or {"decision":"deny"} stdout will block the tool call.
@@ -384,6 +416,28 @@ def make_after_tool_callback(cfg: GemCodeConfig):
384
416
  st[_STATE_FAILURE_KEY] = 0
385
417
  else:
386
418
  st[_STATE_FAILURE_KEY] = 0
419
+
420
+ # Risk feedback: if tools are failing or commands return non-zero, treat the
421
+ # task as higher-risk and allow more evidence in subsequent tool outputs.
422
+ try:
423
+ cur = float(getattr(cfg, "_risk_score", 0.0) or 0.0)
424
+ bump = 0.0
425
+ if err:
426
+ bump += 0.15
427
+ if isinstance(tool_response, dict) and isinstance(tool_response.get("exit_code"), int):
428
+ if int(tool_response["exit_code"]) != 0:
429
+ bump += 0.10
430
+ # Test/build failures should boost evidence allowance more.
431
+ if name in ("bash", "run_command"):
432
+ bump += 0.05
433
+ # decay slowly when things are healthy
434
+ if bump == 0.0:
435
+ cur = max(0.0, cur * 0.90)
436
+ else:
437
+ cur = min(1.0, cur + bump)
438
+ object.__setattr__(cfg, "_risk_score", cur)
439
+ except Exception:
440
+ pass
387
441
  # ── Shell hooks: post_tool_use ────────────────────────────────────────
388
442
  try:
389
443
  from gemcode.hooks import run_post_tool_use_hook
gemcode/config.py CHANGED
@@ -143,6 +143,16 @@ class GemCodeConfig:
143
143
  dynamic_token_policy: bool = field(
144
144
  default_factory=lambda: _truthy_env("GEMCODE_DYNAMIC_TOKEN_POLICY", default=True)
145
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
+ )
146
156
  # Trim oldest text in llm_request.contents when over budget (see context_budget.py).
147
157
  context_shrink_enabled: bool = field(
148
158
  default_factory=lambda: _truthy_env("GEMCODE_CONTEXT_SHRINK", default=True)
gemcode/dynamic_policy.py CHANGED
@@ -32,6 +32,20 @@ def _pct_left(cfg) -> int | None:
32
32
  return None
33
33
 
34
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
+
35
49
  @dataclass(frozen=True)
36
50
  class DynamicCaps:
37
51
  tool_inline_chars: int
@@ -52,6 +66,8 @@ def get_dynamic_caps(cfg) -> DynamicCaps:
52
66
  - Healthy (>=45% left): generous caps (better evidence, less re-asking).
53
67
  - Warning (20-44%): moderate caps.
54
68
  - Tight (<20%): strict caps + prefer offload.
69
+
70
+ Then apply a risk-based boost (if enabled) so complex tasks stay evidence-rich.
55
71
  """
56
72
  # cfg can be None in some tool contexts; treat as enabled with defaults.
57
73
  enabled = _truthy(getattr(cfg, "dynamic_token_policy", True) if cfg is not None else True, default=True)
@@ -76,42 +92,52 @@ def get_dynamic_caps(cfg) -> DynamicCaps:
76
92
  base_tool = int(getattr(cfg, "tool_result_max_chars", 12000) or 12000) if cfg is not None else 12000
77
93
  base_tool = max(1000, base_tool)
78
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
+
79
105
  if pct >= 45:
80
106
  mult = 1.4
81
107
  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,
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)),
90
116
  )
91
117
 
92
118
  if pct >= 20:
93
119
  mult = 1.0
94
120
  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,
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)),
103
129
  )
104
130
 
105
131
  # Tight
106
132
  mult = 0.6
107
133
  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,
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)),
116
142
  )
117
143
 
gemcode/invoke.py CHANGED
@@ -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
  )
gemcode/repl_slash.py CHANGED
@@ -391,6 +391,20 @@ async def process_repl_slash(
391
391
  out(f" thinking_budget: {cfg.thinking_budget if cfg.thinking_budget is not None else '(auto)'}")
392
392
  out(f" show_full_thinking: {cfg.show_full_thinking}")
393
393
  out()
394
+ # Dynamic policy telemetry
395
+ try:
396
+ risk = float(getattr(cfg, "_risk_score", 0.0) or 0.0)
397
+ pct = getattr(cfg, "_context_percent_left", None)
398
+ out(" Dynamic policy:")
399
+ out(f" dynamic_token_policy: {getattr(cfg, 'dynamic_token_policy', True)}")
400
+ out(f" dynamic_risk_policy: {getattr(cfg, 'dynamic_risk_policy', True)}")
401
+ out(f" dynamic_risk_boost: {getattr(cfg, 'dynamic_risk_boost', 0.6)}")
402
+ out(f" risk_score: {risk:.2f}")
403
+ if isinstance(pct, int):
404
+ out(f" context_percent_left: {pct}%")
405
+ out()
406
+ except Exception:
407
+ pass
394
408
  out(" Autocompact:")
395
409
  out(f" GEMCODE_AUTOCOMPACT: {os.environ.get('GEMCODE_AUTOCOMPACT', '1')}")
396
410
  out(f" GEMCODE_AUTOCOMPACT_BUFFER_CHARS: {os.environ.get('GEMCODE_AUTOCOMPACT_BUFFER_CHARS', '60000')}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.67
3
+ Version: 0.3.69
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -3,20 +3,20 @@ gemcode/__main__.py,sha256=EX2s1hxq2Yvli_-tnBN3w5Qv4bOjsBBbjyISF0pDIQw,37
3
3
  gemcode/agent.py,sha256=yft-8lLJbX4abM_bUW156JWfVT3b4OHO-_hIoKdVaOs,52032
4
4
  gemcode/audit.py,sha256=bh9uhXaeh8wqxqoZtz3ZAowd8Ndk1ss-mw9993Vlrgo,469
5
5
  gemcode/autocompact.py,sha256=77h5tgFzJ2rjrhlCL2oIc28IHwLbP4Pqlo7cSNgDwiA,6727
6
- gemcode/callbacks.py,sha256=7kPMDsc2vvY98Ogoi4D0XHB96qh_kx4l4CRsRtePNdk,26171
6
+ gemcode/callbacks.py,sha256=JW9jiYBZwgqqZnX4zehd55dGHeVLCVQVU9zNE3SYS_s,28430
7
7
  gemcode/capability_routing.py,sha256=yvQXwKtrfHXbgbNunU0Dxh9GCDN4cbySXIeccrdzr2o,3471
8
8
  gemcode/cli.py,sha256=kBXb4b4JCG3u0XewCJn8lCyOT62Y8bOvlVoDc2R-GKQ,25320
9
9
  gemcode/compaction.py,sha256=9YtA_qa23_8dHWVHx7AJwUduuI7jJQtq-m6sT8jgPWI,1186
10
- gemcode/config.py,sha256=bCo_Qz4oI_Sfi0EzptULCjfTbtYNhHtMeXTil2vUi8M,14697
10
+ gemcode/config.py,sha256=it_F4haaUlveJXZkX8mGI_nmx1JHsh56XBSkLyUfQ4I,15101
11
11
  gemcode/context_budget.py,sha256=Nhox9vFBtLbb7jtO7cyGW1MxtN7SVjlIeQ7d-cgGyKM,10544
12
12
  gemcode/context_warning.py,sha256=Q8mg5Vojj7EglPhsGAVL7vb8ROLuHVPgdzw25yw-Q2c,4263
13
13
  gemcode/credentials.py,sha256=04v-rLD8_Ams69FQdof2FwcL3ZgsroGUnMcHNQFuBZo,1296
14
- gemcode/dynamic_policy.py,sha256=0ZYv3nzMsEN4kPpjhMbXVSMoTvA_IArbMfe3fF7hhzw,3158
14
+ gemcode/dynamic_policy.py,sha256=nWgBN6ffSn1Te4aDI1MURxRaQjTclzIuSf4KaAskE9U,4662
15
15
  gemcode/hitl_session.py,sha256=oNiI7odFJGUcmqPavjKLJOEumZKrgklLvwjjrIG9GPg,281
16
16
  gemcode/hooks.py,sha256=FHz175d_18j-4ByZZVdEIagmdOvLHcjDjo7HD2Cikf4,6339
17
17
  gemcode/intent_classifier.py,sha256=YfRVEe8gHeKlRkjuSWef1bZ0MPBwyYMp5jymP5Vig5U,8507
18
18
  gemcode/interactions.py,sha256=B0b3QNE_I2i5_HtiebX4ehhjlc4Nbqjf_XbvcTLyJT0,641
19
- gemcode/invoke.py,sha256=ZPRv-ZuqMaD-Yd1tSQxgoWP24WEz8nwWsMDywVdZ8Gk,9319
19
+ gemcode/invoke.py,sha256=tP1bBrmd6e0xbqXDfIc5iT09pqN0t82igvgN1PYwCQI,10285
20
20
  gemcode/kairos_daemon.py,sha256=giINipslAIhBtdbqA0o4RYwt4fBsZjtkiKDjp4zSEvw,6980
21
21
  gemcode/limits.py,sha256=1u7w3X13Uqw34ZOVYKa62pxfvlyLTY2Dne1kN8SKJlA,2524
22
22
  gemcode/live_audio_engine.py,sha256=wxdGp5ciAgQ7WIeBpcW_hm8lbMDsh0KPJbqIS9uAGxA,3406
@@ -32,7 +32,7 @@ gemcode/pricing.py,sha256=lftp0SwyDqOzHqC2-6XzgZZhjif5PLdCe1Q3wY-p6kQ,3558
32
32
  gemcode/prompt_suggestions.py,sha256=h-W_9LlfagS91PyoMEjEjsCqoG4XmIh3QBypA59HyGw,2553
33
33
  gemcode/refine.py,sha256=BijEZ4Z32wGa9aK_WottyAhZF-j0xEqRg5UpjedNv2A,7653
34
34
  gemcode/repl_commands.py,sha256=AHjDUhlq7IuSL6Fla17y0aeGZyp6snxwbMG7gz1PrRc,11330
35
- gemcode/repl_slash.py,sha256=uBmMzUuQ52Yx14vxxL404wvC7hVdI_UK2y9cAEe-7ZI,47815
35
+ gemcode/repl_slash.py,sha256=y_nb-0PYRpW2At7mlA-hkpEybe6vdyjMiVSrjOTPb_g,48434
36
36
  gemcode/review_agent.py,sha256=4t7_5-aE60b4-EheJ_eSB_H2eQYf9GppKoui6jw0TME,5264
37
37
  gemcode/session_runtime.py,sha256=crpKBVYZnWeRkBHcbvBGuCc2Kw2hGhVHuM4uRCmXpOI,18771
38
38
  gemcode/session_store.py,sha256=POUT_QQf715c74jbXj0s5vCd4dlAgJz_CLsIWuEUoO0,6051
@@ -86,9 +86,9 @@ gemcode/tui/welcome_rich.py,sha256=8FEZzLXrzqly5JWiDgV9ooRV1LNXDk-CXV1a7K6ua-U,4
86
86
  gemcode/web/__init__.py,sha256=EysmUAWs6g-lmMk4VFljKfaHVrEgb_FiIzwQmBdORJc,40
87
87
  gemcode/web/claude_sse_adapter.py,sha256=HcNp0Lh4DdBZBLOpstsqa-VzfqAUrRngZ6FSuJ-mIMg,8609
88
88
  gemcode/web/terminal_repl.py,sha256=k2irvFGbCY8gDm_pbirR7b_cakaeafcctoTIvnJkVXk,3902
89
- gemcode-0.3.67.dist-info/licenses/LICENSE,sha256=TD4524qn-W8Z07GTDnag-9jJPFutFZNB0a1WbMHPC54,8388
90
- gemcode-0.3.67.dist-info/METADATA,sha256=rZb2AHzSETy2XUb5L5w88ECMQWXoQmNKhKOJ78SyGcs,23695
91
- gemcode-0.3.67.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
92
- gemcode-0.3.67.dist-info/entry_points.txt,sha256=cZdLTLDiHbks7OSUCuxCh66dCWeQdpLR8BozoqfEjV4,45
93
- gemcode-0.3.67.dist-info/top_level.txt,sha256=UYrjULLBY2bcgK6KI6flomJWmsbDXu7n0rvW2SWFrbo,8
94
- gemcode-0.3.67.dist-info/RECORD,,
89
+ gemcode-0.3.69.dist-info/licenses/LICENSE,sha256=TD4524qn-W8Z07GTDnag-9jJPFutFZNB0a1WbMHPC54,8388
90
+ gemcode-0.3.69.dist-info/METADATA,sha256=2yfwQj99pN5poyQL_zvyHGzXqmqWj-pCFYZuxj8KsX0,23695
91
+ gemcode-0.3.69.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
92
+ gemcode-0.3.69.dist-info/entry_points.txt,sha256=cZdLTLDiHbks7OSUCuxCh66dCWeQdpLR8BozoqfEjV4,45
93
+ gemcode-0.3.69.dist-info/top_level.txt,sha256=UYrjULLBY2bcgK6KI6flomJWmsbDXu7n0rvW2SWFrbo,8
94
+ gemcode-0.3.69.dist-info/RECORD,,