gemcode 0.3.68__tar.gz → 0.3.70__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 (128) hide show
  1. {gemcode-0.3.68/src/gemcode.egg-info → gemcode-0.3.70}/PKG-INFO +1 -1
  2. {gemcode-0.3.68 → gemcode-0.3.70}/pyproject.toml +1 -1
  3. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/callbacks.py +69 -0
  4. gemcode-0.3.70/src/gemcode/policy_profile.py +135 -0
  5. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/repl_slash.py +21 -0
  6. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/session_runtime.py +10 -0
  7. {gemcode-0.3.68 → gemcode-0.3.70/src/gemcode.egg-info}/PKG-INFO +1 -1
  8. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode.egg-info/SOURCES.txt +1 -0
  9. {gemcode-0.3.68 → gemcode-0.3.70}/LICENSE +0 -0
  10. {gemcode-0.3.68 → gemcode-0.3.70}/MANIFEST.in +0 -0
  11. {gemcode-0.3.68 → gemcode-0.3.70}/README.md +0 -0
  12. {gemcode-0.3.68 → gemcode-0.3.70}/setup.cfg +0 -0
  13. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/__init__.py +0 -0
  14. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/__main__.py +0 -0
  15. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/agent.py +0 -0
  16. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/audit.py +0 -0
  17. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/autocompact.py +0 -0
  18. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/capability_routing.py +0 -0
  19. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/cli.py +0 -0
  20. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/compaction.py +0 -0
  21. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/computer_use/__init__.py +0 -0
  22. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/computer_use/browser_computer.py +0 -0
  23. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/config.py +0 -0
  24. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/context_budget.py +0 -0
  25. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/context_warning.py +0 -0
  26. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/credentials.py +0 -0
  27. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/dynamic_policy.py +0 -0
  28. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/hitl_session.py +0 -0
  29. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/hooks.py +0 -0
  30. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/intent_classifier.py +0 -0
  31. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/interactions.py +0 -0
  32. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/invoke.py +0 -0
  33. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/kairos_daemon.py +0 -0
  34. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/limits.py +0 -0
  35. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/live_audio_engine.py +0 -0
  36. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/logging_config.py +0 -0
  37. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/mcp_loader.py +0 -0
  38. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/memory/__init__.py +0 -0
  39. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/memory/embedding_memory_service.py +0 -0
  40. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/memory/file_memory_service.py +0 -0
  41. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/modality_tools.py +0 -0
  42. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/model_errors.py +0 -0
  43. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/model_routing.py +0 -0
  44. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/openapi_loader.py +0 -0
  45. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/paths.py +0 -0
  46. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/permissions.py +0 -0
  47. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/plugins/__init__.py +0 -0
  48. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  49. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  50. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/pricing.py +0 -0
  51. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/prompt_suggestions.py +0 -0
  52. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/query/__init__.py +0 -0
  53. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/query/config.py +0 -0
  54. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/query/deps.py +0 -0
  55. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/query/engine.py +0 -0
  56. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/query/stop_hooks.py +0 -0
  57. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/query/token_budget.py +0 -0
  58. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/query/transitions.py +0 -0
  59. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/refine.py +0 -0
  60. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/repl_commands.py +0 -0
  61. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/review_agent.py +0 -0
  62. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/session_store.py +0 -0
  63. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/slash_commands.py +0 -0
  64. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/thinking.py +0 -0
  65. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/tool_prompt_manifest.py +0 -0
  66. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/tool_registry.py +0 -0
  67. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/tool_result_store.py +0 -0
  68. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/tools/__init__.py +0 -0
  69. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/tools/bash.py +0 -0
  70. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/tools/browser.py +0 -0
  71. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/tools/edit.py +0 -0
  72. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/tools/filesystem.py +0 -0
  73. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/tools/notebook.py +0 -0
  74. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/tools/notes.py +0 -0
  75. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/tools/repo_map.py +0 -0
  76. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/tools/search.py +0 -0
  77. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/tools/shell.py +0 -0
  78. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/tools/shell_gate.py +0 -0
  79. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/tools/subtask.py +0 -0
  80. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/tools/tasks.py +0 -0
  81. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/tools/think.py +0 -0
  82. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/tools/todo.py +0 -0
  83. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/tools/web.py +0 -0
  84. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/tools/web_search.py +0 -0
  85. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/tools_inspector.py +0 -0
  86. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/trust.py +0 -0
  87. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/tui/input_handler.py +0 -0
  88. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/tui/scrollback.py +0 -0
  89. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/tui/spinner.py +0 -0
  90. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/tui/welcome_banner.py +0 -0
  91. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/tui/welcome_rich.py +0 -0
  92. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/version.py +0 -0
  93. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/vertex.py +0 -0
  94. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/web/__init__.py +0 -0
  95. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/web/claude_sse_adapter.py +0 -0
  96. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/web/terminal_repl.py +0 -0
  97. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode/workspace_hints.py +0 -0
  98. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode.egg-info/dependency_links.txt +0 -0
  99. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode.egg-info/entry_points.txt +0 -0
  100. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode.egg-info/requires.txt +0 -0
  101. {gemcode-0.3.68 → gemcode-0.3.70}/src/gemcode.egg-info/top_level.txt +0 -0
  102. {gemcode-0.3.68 → gemcode-0.3.70}/tests/test_agent_instruction.py +0 -0
  103. {gemcode-0.3.68 → gemcode-0.3.70}/tests/test_autocompact.py +0 -0
  104. {gemcode-0.3.68 → gemcode-0.3.70}/tests/test_capability_routing.py +0 -0
  105. {gemcode-0.3.68 → gemcode-0.3.70}/tests/test_claude_web_adapter_sse.py +0 -0
  106. {gemcode-0.3.68 → gemcode-0.3.70}/tests/test_cli_init.py +0 -0
  107. {gemcode-0.3.68 → gemcode-0.3.70}/tests/test_computer_use_permissions.py +0 -0
  108. {gemcode-0.3.68 → gemcode-0.3.70}/tests/test_context_budget.py +0 -0
  109. {gemcode-0.3.68 → gemcode-0.3.70}/tests/test_context_warning.py +0 -0
  110. {gemcode-0.3.68 → gemcode-0.3.70}/tests/test_credentials.py +0 -0
  111. {gemcode-0.3.68 → gemcode-0.3.70}/tests/test_interactive_permission_ask.py +0 -0
  112. {gemcode-0.3.68 → gemcode-0.3.70}/tests/test_kairos_scheduler.py +0 -0
  113. {gemcode-0.3.68 → gemcode-0.3.70}/tests/test_modality_tools.py +0 -0
  114. {gemcode-0.3.68 → gemcode-0.3.70}/tests/test_model_error_retry.py +0 -0
  115. {gemcode-0.3.68 → gemcode-0.3.70}/tests/test_model_errors.py +0 -0
  116. {gemcode-0.3.68 → gemcode-0.3.70}/tests/test_model_routing.py +0 -0
  117. {gemcode-0.3.68 → gemcode-0.3.70}/tests/test_paths.py +0 -0
  118. {gemcode-0.3.68 → gemcode-0.3.70}/tests/test_permissions.py +0 -0
  119. {gemcode-0.3.68 → gemcode-0.3.70}/tests/test_prompt_suggestions.py +0 -0
  120. {gemcode-0.3.68 → gemcode-0.3.70}/tests/test_repl_commands.py +0 -0
  121. {gemcode-0.3.68 → gemcode-0.3.70}/tests/test_repl_slash.py +0 -0
  122. {gemcode-0.3.68 → gemcode-0.3.70}/tests/test_slash_commands.py +0 -0
  123. {gemcode-0.3.68 → gemcode-0.3.70}/tests/test_thinking_config.py +0 -0
  124. {gemcode-0.3.68 → gemcode-0.3.70}/tests/test_token_budget.py +0 -0
  125. {gemcode-0.3.68 → gemcode-0.3.70}/tests/test_tool_context_circulation.py +0 -0
  126. {gemcode-0.3.68 → gemcode-0.3.70}/tests/test_tools.py +0 -0
  127. {gemcode-0.3.68 → gemcode-0.3.70}/tests/test_tools_inspector.py +0 -0
  128. {gemcode-0.3.68 → gemcode-0.3.70}/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.68
3
+ Version: 0.3.70
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.68"
7
+ version = "0.3.70"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -43,6 +43,11 @@ _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"
48
+ _RISK_HAD_SHELL = "gemcode:risk_had_shell"
49
+ _RISK_HAD_WRITE = "gemcode:risk_had_write"
50
+ _RISK_HAD_FAILURE = "gemcode:risk_had_failure"
46
51
 
47
52
  def _truthy_env(name: str, *, default: bool = False) -> bool:
48
53
  v = os.environ.get(name)
@@ -141,6 +146,38 @@ def make_before_tool_callback(cfg: GemCodeConfig):
141
146
  record = {"tool": name, "args": _redact_args(name, args)}
142
147
  append_audit(cfg.project_root, record)
143
148
 
149
+ # Dynamic risk signals from actual repo interaction.
150
+ try:
151
+ if tool_context is not None:
152
+ st = tool_context.state
153
+ st[_RISK_TOOL_CALLS] = int(st.get(_RISK_TOOL_CALLS, 0) or 0) + 1
154
+ if name == "read_file":
155
+ p = (args or {}).get("path")
156
+ if isinstance(p, str) and p.strip():
157
+ touched: set[str] = set(st.get(_RISK_FILES_TOUCHED, []) or [])
158
+ touched.add(p.strip())
159
+ # Store as list for JSON-serializable session state.
160
+ st[_RISK_FILES_TOUCHED] = list(sorted(touched))[:200]
161
+ # More files touched => higher complexity.
162
+ n = len(touched)
163
+ cur = float(getattr(cfg, "_risk_score", 0.0) or 0.0)
164
+ if n >= 10:
165
+ cur = min(1.0, cur + 0.08)
166
+ elif n >= 5:
167
+ cur = min(1.0, cur + 0.04)
168
+ object.__setattr__(cfg, "_risk_score", cur)
169
+ # Writes / shell are inherently higher risk; allow more evidence.
170
+ if name in MUTATING_TOOLS:
171
+ st[_RISK_HAD_WRITE] = True
172
+ cur = float(getattr(cfg, "_risk_score", 0.0) or 0.0)
173
+ object.__setattr__(cfg, "_risk_score", min(1.0, cur + 0.12))
174
+ if name in SHELL_TOOLS:
175
+ st[_RISK_HAD_SHELL] = True
176
+ cur = float(getattr(cfg, "_risk_score", 0.0) or 0.0)
177
+ object.__setattr__(cfg, "_risk_score", min(1.0, cur + 0.08))
178
+ except Exception:
179
+ pass
180
+
144
181
  # ── Shell hooks: pre_tool_use ─────────────────────────────────────────
145
182
  # If the project has a .gemcode/hooks/pre_tool_use.sh, run it now.
146
183
  # Non-zero exit or {"decision":"deny"} stdout will block the tool call.
@@ -391,10 +428,21 @@ def make_after_tool_callback(cfg: GemCodeConfig):
391
428
  cur = float(getattr(cfg, "_risk_score", 0.0) or 0.0)
392
429
  bump = 0.0
393
430
  if err:
431
+ try:
432
+ st[_RISK_HAD_FAILURE] = True
433
+ except Exception:
434
+ pass
394
435
  bump += 0.15
395
436
  if isinstance(tool_response, dict) and isinstance(tool_response.get("exit_code"), int):
396
437
  if int(tool_response["exit_code"]) != 0:
438
+ try:
439
+ st[_RISK_HAD_FAILURE] = True
440
+ except Exception:
441
+ pass
397
442
  bump += 0.10
443
+ # Test/build failures should boost evidence allowance more.
444
+ if name in ("bash", "run_command"):
445
+ bump += 0.05
398
446
  # decay slowly when things are healthy
399
447
  if bump == 0.0:
400
448
  cur = max(0.0, cur * 0.90)
@@ -403,6 +451,27 @@ def make_after_tool_callback(cfg: GemCodeConfig):
403
451
  object.__setattr__(cfg, "_risk_score", cur)
404
452
  except Exception:
405
453
  pass
454
+
455
+ # Persist repo calibration profile (best-effort).
456
+ try:
457
+ files = st.get(_RISK_FILES_TOUCHED, []) or []
458
+ files_n = len(files) if isinstance(files, list) else 0
459
+ tool_calls = int(st.get(_RISK_TOOL_CALLS, 0) or 0)
460
+ had_shell = bool(st.get(_RISK_HAD_SHELL, False))
461
+ had_write = bool(st.get(_RISK_HAD_WRITE, False))
462
+ had_failure = bool(st.get(_RISK_HAD_FAILURE, False))
463
+ from gemcode.policy_profile import update_profile
464
+ prof = update_profile(
465
+ cfg.project_root,
466
+ files_touched=files_n,
467
+ tool_calls=tool_calls,
468
+ had_shell=had_shell,
469
+ had_write=had_write,
470
+ had_failure=had_failure,
471
+ )
472
+ object.__setattr__(cfg, "_policy_profile", prof.to_dict())
473
+ except Exception:
474
+ pass
406
475
  # ── Shell hooks: post_tool_use ────────────────────────────────────────
407
476
  try:
408
477
  from gemcode.hooks import run_post_tool_use_hook
@@ -0,0 +1,135 @@
1
+ """
2
+ Persistent per-repo policy profile.
3
+
4
+ Goal: make dynamic budgets self-tuning per repository without requiring manual
5
+ configuration. This stores lightweight rolling stats under `.gemcode/policy.json`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import time
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+
17
+ def _path(root: Path) -> Path:
18
+ d = root / ".gemcode"
19
+ d.mkdir(parents=True, exist_ok=True)
20
+ return d / "policy.json"
21
+
22
+
23
+ def _clamp(x: float, lo: float, hi: float) -> float:
24
+ return lo if x < lo else hi if x > hi else x
25
+
26
+
27
+ def _ema(prev: float, x: float, *, alpha: float) -> float:
28
+ return (alpha * x) + ((1.0 - alpha) * prev)
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class PolicyProfile:
33
+ # Rolling averages in [0,1] where possible.
34
+ failure_rate_ema: float = 0.0
35
+ shell_rate_ema: float = 0.0
36
+ write_rate_ema: float = 0.0
37
+ files_touched_ema: float = 0.0 # scaled 0..1 (e.g. 0.5 ~ 10 files)
38
+ updated_at: int = 0
39
+
40
+ def to_dict(self) -> dict[str, Any]:
41
+ return {
42
+ "failure_rate_ema": self.failure_rate_ema,
43
+ "shell_rate_ema": self.shell_rate_ema,
44
+ "write_rate_ema": self.write_rate_ema,
45
+ "files_touched_ema": self.files_touched_ema,
46
+ "updated_at": self.updated_at,
47
+ "version": 1,
48
+ }
49
+
50
+ @staticmethod
51
+ def from_dict(d: dict[str, Any]) -> "PolicyProfile":
52
+ try:
53
+ return PolicyProfile(
54
+ failure_rate_ema=float(d.get("failure_rate_ema", 0.0) or 0.0),
55
+ shell_rate_ema=float(d.get("shell_rate_ema", 0.0) or 0.0),
56
+ write_rate_ema=float(d.get("write_rate_ema", 0.0) or 0.0),
57
+ files_touched_ema=float(d.get("files_touched_ema", 0.0) or 0.0),
58
+ updated_at=int(d.get("updated_at", 0) or 0),
59
+ )
60
+ except Exception:
61
+ return PolicyProfile()
62
+
63
+
64
+ def load_profile(project_root: Path) -> PolicyProfile:
65
+ p = _path(project_root)
66
+ if not p.exists():
67
+ return PolicyProfile()
68
+ try:
69
+ raw = p.read_text(encoding="utf-8", errors="replace")
70
+ d = json.loads(raw) if raw.strip() else {}
71
+ if isinstance(d, dict):
72
+ return PolicyProfile.from_dict(d)
73
+ except Exception:
74
+ return PolicyProfile()
75
+ return PolicyProfile()
76
+
77
+
78
+ def save_profile(project_root: Path, profile: PolicyProfile) -> None:
79
+ p = _path(project_root)
80
+ p.write_text(
81
+ json.dumps(profile.to_dict(), ensure_ascii=False, indent=2),
82
+ encoding="utf-8",
83
+ errors="replace",
84
+ )
85
+
86
+
87
+ def update_profile(
88
+ project_root: Path,
89
+ *,
90
+ files_touched: int,
91
+ tool_calls: int,
92
+ had_shell: bool,
93
+ had_write: bool,
94
+ had_failure: bool,
95
+ alpha: float = 0.08,
96
+ ) -> PolicyProfile:
97
+ """
98
+ Update profile with a single-turn observation.
99
+
100
+ We scale files_touched into [0,1] via min(files/20, 1).
101
+ """
102
+ prof = load_profile(project_root)
103
+ alpha = _clamp(alpha, 0.01, 0.3)
104
+ ft_scaled = _clamp(float(files_touched) / 20.0, 0.0, 1.0)
105
+ fail = 1.0 if had_failure else 0.0
106
+ shell = 1.0 if had_shell else 0.0
107
+ write = 1.0 if had_write else 0.0
108
+ # tool_calls unused for now, but reserved for future calibration.
109
+ _ = tool_calls
110
+ updated = PolicyProfile(
111
+ failure_rate_ema=_ema(prof.failure_rate_ema, fail, alpha=alpha),
112
+ shell_rate_ema=_ema(prof.shell_rate_ema, shell, alpha=alpha),
113
+ write_rate_ema=_ema(prof.write_rate_ema, write, alpha=alpha),
114
+ files_touched_ema=_ema(prof.files_touched_ema, ft_scaled, alpha=alpha),
115
+ updated_at=int(time.time()),
116
+ )
117
+ save_profile(project_root, updated)
118
+ return updated
119
+
120
+
121
+ def calibrated_baseline_risk(profile: PolicyProfile) -> float:
122
+ """
123
+ Convert profile into a baseline risk prior for a repo.
124
+
125
+ Repos with frequent failures, many writes, and lots of files touched tend to
126
+ benefit from higher evidence budgets by default.
127
+ """
128
+ r = (
129
+ 0.55 * profile.failure_rate_ema
130
+ + 0.20 * profile.write_rate_ema
131
+ + 0.15 * profile.shell_rate_ema
132
+ + 0.10 * profile.files_touched_ema
133
+ )
134
+ return _clamp(r, 0.0, 0.8)
135
+
@@ -391,6 +391,27 @@ 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
+ prof = getattr(cfg, "_policy_profile", None)
399
+ out(" Dynamic policy:")
400
+ out(f" dynamic_token_policy: {getattr(cfg, 'dynamic_token_policy', True)}")
401
+ out(f" dynamic_risk_policy: {getattr(cfg, 'dynamic_risk_policy', True)}")
402
+ out(f" dynamic_risk_boost: {getattr(cfg, 'dynamic_risk_boost', 0.6)}")
403
+ out(f" risk_score: {risk:.2f}")
404
+ if isinstance(pct, int):
405
+ out(f" context_percent_left: {pct}%")
406
+ if isinstance(prof, dict):
407
+ try:
408
+ out(f" profile.failure_rate_ema: {float(prof.get('failure_rate_ema', 0.0) or 0.0):.2f}")
409
+ out(f" profile.files_touched_ema: {float(prof.get('files_touched_ema', 0.0) or 0.0):.2f}")
410
+ except Exception:
411
+ pass
412
+ out()
413
+ except Exception:
414
+ pass
394
415
  out(" Autocompact:")
395
416
  out(f" GEMCODE_AUTOCOMPACT: {os.environ.get('GEMCODE_AUTOCOMPACT', '1')}")
396
417
  out(f" GEMCODE_AUTOCOMPACT_BUFFER_CHARS: {os.environ.get('GEMCODE_AUTOCOMPACT_BUFFER_CHARS', '60000')}")
@@ -314,6 +314,16 @@ def _build_artifact_service(cfg: GemCodeConfig):
314
314
 
315
315
  def create_runner(cfg: GemCodeConfig, extra_tools: list | None = None) -> Runner:
316
316
  """Construct Runner + SQLite session service + root LlmAgent."""
317
+ # Load per-repo calibration profile (self-tuning dynamic policy).
318
+ try:
319
+ from gemcode.policy_profile import calibrated_baseline_risk, load_profile
320
+ prof = load_profile(cfg.project_root)
321
+ base = calibrated_baseline_risk(prof)
322
+ cur = float(getattr(cfg, "_risk_score", 0.0) or 0.0)
323
+ object.__setattr__(cfg, "_risk_score", max(cur, base))
324
+ object.__setattr__(cfg, "_policy_profile", prof.to_dict())
325
+ except Exception:
326
+ pass
317
327
  modality_tools = build_modality_extra_tools(cfg)
318
328
  merged_extra_tools: list | None
319
329
  if extra_tools:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.68
3
+ Version: 0.3.70
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -32,6 +32,7 @@ src/gemcode/model_routing.py
32
32
  src/gemcode/openapi_loader.py
33
33
  src/gemcode/paths.py
34
34
  src/gemcode/permissions.py
35
+ src/gemcode/policy_profile.py
35
36
  src/gemcode/pricing.py
36
37
  src/gemcode/prompt_suggestions.py
37
38
  src/gemcode/refine.py
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes