gemcode 0.3.70__py3-none-any.whl → 0.3.71__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
@@ -48,6 +48,9 @@ _RISK_TOOL_CALLS = "gemcode:risk_tool_calls"
48
48
  _RISK_HAD_SHELL = "gemcode:risk_had_shell"
49
49
  _RISK_HAD_WRITE = "gemcode:risk_had_write"
50
50
  _RISK_HAD_FAILURE = "gemcode:risk_had_failure"
51
+ _TOOL_GROUP_CHARS = "gemcode:tool_group_chars"
52
+ _TOOL_GROUP_EXCEEDED = "gemcode:tool_group_budget_exceeded"
53
+ _TOOL_SEQ = "gemcode:tool_seq"
51
54
 
52
55
  def _truthy_env(name: str, *, default: bool = False) -> bool:
53
56
  v = os.environ.get(name)
@@ -150,6 +153,8 @@ def make_before_tool_callback(cfg: GemCodeConfig):
150
153
  try:
151
154
  if tool_context is not None:
152
155
  st = tool_context.state
156
+ # Per-turn tool sequence (used for stable tool-result replacement keys).
157
+ st[_TOOL_SEQ] = int(st.get(_TOOL_SEQ, 0) or 0) + 1
153
158
  st[_RISK_TOOL_CALLS] = int(st.get(_RISK_TOOL_CALLS, 0) or 0) + 1
154
159
  if name == "read_file":
155
160
  p = (args or {}).get("path")
@@ -360,18 +365,40 @@ def make_after_tool_callback(cfg: GemCodeConfig):
360
365
  except Exception:
361
366
  pass
362
367
 
368
+ # Aggregate per-turn tool-result budget: if we already exceeded the budget,
369
+ # tighten caps further for the rest of this user message.
370
+ try:
371
+ if tool_context is not None:
372
+ st = tool_context.state
373
+ if bool(st.get(_TOOL_GROUP_EXCEEDED, False)):
374
+ effective_tool_chars = max(1500, int(effective_tool_chars * 0.5))
375
+ except Exception:
376
+ pass
377
+
363
378
  if (
364
379
  isinstance(tool_response, dict)
365
380
  and getattr(cfg, "tool_result_offload_enabled", False)
366
381
  and effective_tool_chars > 0
367
382
  ):
368
383
  try:
369
- from gemcode.tool_result_store import maybe_offload_tool_result
370
- new_payload, did = maybe_offload_tool_result(
384
+ from gemcode.tool_result_store import maybe_offload_tool_result_stable
385
+ seq = None
386
+ st = None
387
+ try:
388
+ if tool_context is not None:
389
+ st = tool_context.state
390
+ seq = int(st.get(_TOOL_SEQ, 0) or 0)
391
+ except Exception:
392
+ st = None
393
+ seq = None
394
+ new_payload, did = maybe_offload_tool_result_stable(
371
395
  project_root=cfg.project_root,
372
396
  tool_name=name,
397
+ args=args or {},
373
398
  payload=tool_response,
374
399
  max_inline_chars=int(effective_tool_chars),
400
+ state=st,
401
+ seq=seq,
375
402
  )
376
403
  if did and isinstance(new_payload, dict):
377
404
  tool_response = new_payload
@@ -392,6 +419,19 @@ def make_after_tool_callback(cfg: GemCodeConfig):
392
419
  st = tool_context.state
393
420
  except Exception:
394
421
  return tool_response if (truncated or offloaded) else None
422
+
423
+ # Update aggregate per-turn budget counters (best-effort).
424
+ try:
425
+ from gemcode.context_budget import estimate_obj_string_chars
426
+ budget = int(getattr(cfg, "tool_result_group_budget_chars", 0) or 0)
427
+ if budget > 0:
428
+ used = int(st.get(_TOOL_GROUP_CHARS, 0) or 0)
429
+ used += int(estimate_obj_string_chars(tool_response))
430
+ st[_TOOL_GROUP_CHARS] = used
431
+ if used >= budget:
432
+ st[_TOOL_GROUP_EXCEEDED] = True
433
+ except Exception:
434
+ pass
395
435
  err = isinstance(tool_response, dict) and tool_response.get("error")
396
436
  err_kind = (
397
437
  isinstance(tool_response, dict) and tool_response.get("error_kind")
gemcode/config.py CHANGED
@@ -60,6 +60,10 @@ def token_budget_invocation_reset() -> dict:
60
60
  "gemcode:bt_t0": t,
61
61
  "gemcode:bt_base_total_tokens": -1,
62
62
  "gemcode:bt_token_budget_stop": False,
63
+ # Tool-result aggregate budget (per user message)
64
+ "gemcode:tool_group_chars": 0,
65
+ "gemcode:tool_group_budget_exceeded": False,
66
+ "gemcode:tool_seq": 0,
63
67
  }
64
68
 
65
69
 
@@ -131,6 +135,12 @@ class GemCodeConfig:
131
135
  )
132
136
  )
133
137
 
138
+ # Aggregate tool-result budget per user message (approx. characters of tool payloads).
139
+ # When exceeded, GemCode tightens subsequent tool output caps for the remainder of the turn.
140
+ tool_result_group_budget_chars: int = field(
141
+ default_factory=lambda: int(os.environ.get("GEMCODE_TOOL_RESULT_GROUP_BUDGET_CHARS", "60000"))
142
+ )
143
+
134
144
  # When enabled, oversized tool outputs are offloaded to disk under
135
145
  # .gemcode/tool-results/ and replaced in history with stable refs + previews.
136
146
  # This reduces context bloat and improves prompt-cache stability.
@@ -18,6 +18,73 @@ from pathlib import Path
18
18
  from typing import Any
19
19
 
20
20
  _REF_PREFIX = "tool_result:"
21
+ _REPL_STATE_KEY = "gemcode:tool_replacement_state"
22
+
23
+
24
+ def _stable_key(tool_name: str, args: dict[str, Any] | None, seq: int | None) -> str:
25
+ """
26
+ Build a stable key for replacement decisions.
27
+
28
+ ADK does not expose tool_use_id directly to callbacks in all versions, so we use:
29
+ - per-turn tool sequence number (preferred when available)
30
+ - tool name
31
+ - a stable hash of args (best-effort)
32
+ """
33
+ import json
34
+ try:
35
+ args_s = json.dumps(args or {}, sort_keys=True, ensure_ascii=False)
36
+ except Exception:
37
+ args_s = str(args or {})
38
+ b = (f"{seq or 0}:{tool_name}:{args_s}").encode("utf-8", errors="replace")
39
+ return _sha256_bytes(b)
40
+
41
+
42
+ def maybe_offload_tool_result_stable(
43
+ *,
44
+ project_root: Path,
45
+ tool_name: str,
46
+ args: dict[str, Any] | None,
47
+ payload: Any,
48
+ max_inline_chars: int,
49
+ state: dict[str, Any] | None,
50
+ seq: int | None,
51
+ ) -> tuple[Any, bool]:
52
+ """
53
+ Stable offload wrapper.
54
+
55
+ - If we've already processed an identical tool call in this session, we re-apply
56
+ the exact same replacement structure to preserve prompt byte stability.
57
+ - Otherwise, we apply offload and remember the replacement result.
58
+ """
59
+ if state is None:
60
+ return maybe_offload_tool_result(
61
+ project_root=project_root,
62
+ tool_name=tool_name,
63
+ payload=payload,
64
+ max_inline_chars=max_inline_chars,
65
+ )
66
+
67
+ repl_state = state.get(_REPL_STATE_KEY)
68
+ if not isinstance(repl_state, dict):
69
+ repl_state = {}
70
+ state[_REPL_STATE_KEY] = repl_state
71
+
72
+ key = _stable_key(tool_name, args, seq)
73
+ if key in repl_state:
74
+ return repl_state[key], False
75
+
76
+ new_payload, did = maybe_offload_tool_result(
77
+ project_root=project_root,
78
+ tool_name=tool_name,
79
+ payload=payload,
80
+ max_inline_chars=max_inline_chars,
81
+ )
82
+ if did:
83
+ repl_state[key] = new_payload
84
+ else:
85
+ # Freeze "no replacement" decision too (prevents later shape drift).
86
+ repl_state[key] = payload
87
+ return new_payload, did
21
88
 
22
89
 
23
90
  def _store_dir(project_root: Path) -> Path:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.70
3
+ Version: 0.3.71
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -217,6 +217,16 @@ gemcode --yes "Add a module docstring to src/foo.py"
217
217
  gemcode --session mysess --yes "Continue: run tests and fix failures"
218
218
  ```
219
219
 
220
+ ### What GemCode writes to `.gemcode/`
221
+
222
+ GemCode keeps project-local state under `.gemcode/`:
223
+
224
+ - **`sessions.sqlite`**: session events/history (ADK `SqliteSessionService`)
225
+ - **`audit.log`**: JSONL audit trail for tool usage + model usage + stop reasons
226
+ - **`tool-results/`**: oversized tool outputs offloaded to stable refs (`tool_result:<sha>`)
227
+ - **`artifacts/`**: file artifacts (ADK `FileArtifactService`)
228
+ - **`policy.json`**: self-tuning per-repo profile used to calibrate dynamic budgets
229
+
220
230
  - **`--yes`**: allow mutating tools (`write_file`, `search_replace`). Shell execution is still restricted by the `.env.example` allowlist.
221
231
  - **`--session`**: Conversation history is stored under `.gemcode/sessions.sqlite` (ADK `SqliteSessionService`). Reuse the same `--session` id to continue.
222
232
  - **`--max-llm-calls`**: cap model↔tool iterations for this message (maps to ADK `RunConfig.max_llm_calls`). You can also set `GEMCODE_MAX_LLM_CALLS`.
@@ -243,6 +253,33 @@ gemcode --session mysess --yes "Continue: run tests and fix failures"
243
253
  - **Recovery-loop**: ADK `ReflectAndRetryToolPlugin`-based retries on tool failures.
244
254
  - Set `GEMCODE_ENABLE_TOOL_RECOVERY_RETRY=0` to disable.
245
255
  - Set `GEMCODE_TOOL_REFLECT_MAX_RETRIES=1` to control retries per tool.
256
+
257
+ ### Token efficiency (dynamic + intelligent)
258
+
259
+ GemCode optimizes tokens without losing capability by using a dynamic policy:
260
+
261
+ - **Context-pressure aware**: tool caps tighten when context is tight, loosen when there is room.
262
+ - **Risk/complexity aware**: caps increase for risky tasks (writes, shell, failures, many files).
263
+ - **Self-tuning per repo**: `.gemcode/policy.json` calibrates baseline evidence budgets over time.
264
+
265
+ Key env toggles:
266
+
267
+ - `GEMCODE_DYNAMIC_TOKEN_POLICY=0|1`
268
+ - `GEMCODE_DYNAMIC_RISK_POLICY=0|1`
269
+ - `GEMCODE_DYNAMIC_RISK_BOOST=<float>` (default `0.6`)
270
+ - `GEMCODE_TOOL_RESULT_OFFLOAD=0|1` (default `1`)
271
+
272
+ Live telemetry:
273
+
274
+ - `/status` shows `risk_score`, `context_percent_left`, and profile EMAs.
275
+
276
+ ### Stable tool output offloading (OpenClaude-style)
277
+
278
+ Oversized tool outputs are automatically offloaded and replaced with stable refs:
279
+
280
+ - Stored in: `.gemcode/tool-results/`
281
+ - References: `tool_result:<sha256>`
282
+ - Load on demand: `load_tool_result(ref)`
246
283
  - **Gemini thinking controls (Claude-like)**:
247
284
  - By default GemCode lets Gemini use its dynamic/adaptive thinking behavior.
248
285
  - Set `GEMCODE_DISABLE_THINKING=1` to force a best-effort “low thinking” mode:
@@ -301,12 +338,24 @@ the user’s project.
301
338
  - `list_directory`
302
339
  - `glob_files`
303
340
  - `grep_content`
341
+ - `repo_map`
342
+ - `web_search`
343
+ - `web_fetch`
344
+ - `notebook_read`
345
+ - `notebook_edit`
304
346
  - Mutating tools (require `--yes` unless your policy blocks them):
305
347
  - `write_file`
306
348
  - `search_replace`
307
349
  - Shell execution:
308
350
  - `run_command` (guarded by `GEMCODE_ALLOW_COMMANDS` from `.env.example` and
309
351
  `GEMCODE_PERMISSION_MODE`).
352
+ - `bash` (pipelines + redirects; supports `background=True`)
353
+
354
+ - Background task management (for processes started via `bash(..., background=True)`):
355
+ - `list_tasks`, `task_output`, `kill_task`
356
+
357
+ - Tool offload loader:
358
+ - `load_tool_result(ref)`
310
359
 
311
360
  Tool execution is still controlled by permission gates and then governed by
312
361
  GemCode’s circuit breaker + recovery behavior.
@@ -434,6 +483,23 @@ pip install -e ".[dev]"
434
483
  pytest
435
484
  ```
436
485
 
486
+ ## Release workflow (GitHub Actions → PyPI)
487
+
488
+ This repository publishes to PyPI on tag pushes:
489
+
490
+ - `.github/workflows/publish-pypi.yml` triggers on tags matching `v*`
491
+
492
+ Typical release steps:
493
+
494
+ ```bash
495
+ # 1) bump gemcode/pyproject.toml version
496
+ git add -A
497
+ git commit -m "release: vX.Y.Z"
498
+ git tag -a vX.Y.Z -m "vX.Y.Z"
499
+ git push origin HEAD
500
+ git push origin vX.Y.Z
501
+ ```
502
+
437
503
  ## References (local only)
438
504
 
439
505
  Do not commit proprietary leaked trees into this package. Keep `claude-code-leaked/` and similar folders outside version control or in a private mirror.
@@ -3,11 +3,11 @@ 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=GlRVLTks2XRBeMZkpEuafBEyyA1iIWxfCemsPtXRwQk,29604
6
+ gemcode/callbacks.py,sha256=EYkJTRcHjCeqTgVZGat2Y3BYWg4tNtHl-cgl5cdUxpo,31076
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=it_F4haaUlveJXZkX8mGI_nmx1JHsh56XBSkLyUfQ4I,15101
10
+ gemcode/config.py,sha256=APBKPIpGvKd46Xt0ZGOZfVr8Tl6EAPbwqDsuM45jc1U,15609
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
@@ -41,7 +41,7 @@ gemcode/slash_commands.py,sha256=Qylzsj1notk0xN_hvd3CR4HD8g-l99UENDMcg1pKeBA,794
41
41
  gemcode/thinking.py,sha256=RanBf_x9fKv1o4DNyNXPLfOdn2xT0KybJb65nYgmMEE,4885
42
42
  gemcode/tool_prompt_manifest.py,sha256=MS_eSJg2BTp6yv1Ih2p93okPmnK3B2dYAMjnG6yaEVY,8695
43
43
  gemcode/tool_registry.py,sha256=ifqxtr2uLwEUwnJLKYLza_tIz-paaZediJa75y9MiyA,1795
44
- gemcode/tool_result_store.py,sha256=Wfm_JHLdYAI4jfjTLHOEAeK2yho9OCLUF_qErhj-wV0,4428
44
+ gemcode/tool_result_store.py,sha256=pkeV5ekvsp6v7679yOTB2Rof4vPv3RdSl8urzawofq4,6351
45
45
  gemcode/tools_inspector.py,sha256=okmu4PDYAQQ7nthDvuzSHmy2zArFTG4ftIPRadzLnxA,4100
46
46
  gemcode/trust.py,sha256=fxe57Xg6aL_KU24bQDUtD-rXjsNpaq7g-eQTInZnudE,1336
47
47
  gemcode/version.py,sha256=uwynYS-RmK8CDoqGtt8976kFkJv0zELkEAlwebnp_io,380
@@ -87,9 +87,9 @@ gemcode/tui/welcome_rich.py,sha256=8FEZzLXrzqly5JWiDgV9ooRV1LNXDk-CXV1a7K6ua-U,4
87
87
  gemcode/web/__init__.py,sha256=EysmUAWs6g-lmMk4VFljKfaHVrEgb_FiIzwQmBdORJc,40
88
88
  gemcode/web/claude_sse_adapter.py,sha256=HcNp0Lh4DdBZBLOpstsqa-VzfqAUrRngZ6FSuJ-mIMg,8609
89
89
  gemcode/web/terminal_repl.py,sha256=k2irvFGbCY8gDm_pbirR7b_cakaeafcctoTIvnJkVXk,3902
90
- gemcode-0.3.70.dist-info/licenses/LICENSE,sha256=TD4524qn-W8Z07GTDnag-9jJPFutFZNB0a1WbMHPC54,8388
91
- gemcode-0.3.70.dist-info/METADATA,sha256=_gp14z7C90hBaT9dX7SzUGmjQFhU0ZfhBaTb1jXz_oI,23695
92
- gemcode-0.3.70.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
93
- gemcode-0.3.70.dist-info/entry_points.txt,sha256=cZdLTLDiHbks7OSUCuxCh66dCWeQdpLR8BozoqfEjV4,45
94
- gemcode-0.3.70.dist-info/top_level.txt,sha256=UYrjULLBY2bcgK6KI6flomJWmsbDXu7n0rvW2SWFrbo,8
95
- gemcode-0.3.70.dist-info/RECORD,,
90
+ gemcode-0.3.71.dist-info/licenses/LICENSE,sha256=TD4524qn-W8Z07GTDnag-9jJPFutFZNB0a1WbMHPC54,8388
91
+ gemcode-0.3.71.dist-info/METADATA,sha256=OkyL8aZtBhHoGZhMZslyEHdaEeMQf8uwUAU75YTXHYY,25852
92
+ gemcode-0.3.71.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
93
+ gemcode-0.3.71.dist-info/entry_points.txt,sha256=cZdLTLDiHbks7OSUCuxCh66dCWeQdpLR8BozoqfEjV4,45
94
+ gemcode-0.3.71.dist-info/top_level.txt,sha256=UYrjULLBY2bcgK6KI6flomJWmsbDXu7n0rvW2SWFrbo,8
95
+ gemcode-0.3.71.dist-info/RECORD,,