gemcode 0.3.69__tar.gz → 0.3.71__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.69/src/gemcode.egg-info → gemcode-0.3.71}/PKG-INFO +67 -1
- {gemcode-0.3.69 → gemcode-0.3.71}/README.md +66 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/pyproject.toml +1 -1
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/callbacks.py +76 -2
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/config.py +10 -0
- gemcode-0.3.71/src/gemcode/policy_profile.py +135 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/repl_slash.py +7 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/session_runtime.py +10 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/tool_result_store.py +67 -0
- {gemcode-0.3.69 → gemcode-0.3.71/src/gemcode.egg-info}/PKG-INFO +67 -1
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode.egg-info/SOURCES.txt +1 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/LICENSE +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/MANIFEST.in +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/setup.cfg +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/__init__.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/__main__.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/agent.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/audit.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/cli.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/compaction.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/credentials.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/dynamic_policy.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/hooks.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/intent_classifier.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/interactions.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/invoke.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/kairos_daemon.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/limits.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/openapi_loader.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/paths.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/permissions.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/pricing.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/query/config.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/refine.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/repl_commands.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/review_agent.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/session_store.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/thinking.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/tools/__init__.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/tools/bash.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/tools/browser.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/tools/notebook.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/tools/notes.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/tools/repo_map.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/tools/subtask.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/tools/tasks.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/tools/think.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/tools/web.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/tools/web_search.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/trust.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/tui/input_handler.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/tui/scrollback.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/tui/spinner.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/tui/welcome_banner.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/tui/welcome_rich.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/version.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/vertex.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/web/claude_sse_adapter.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/tests/test_autocompact.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/tests/test_capability_routing.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/tests/test_claude_web_adapter_sse.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/tests/test_cli_init.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/tests/test_context_budget.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/tests/test_context_warning.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/tests/test_credentials.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/tests/test_kairos_scheduler.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/tests/test_modality_tools.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/tests/test_model_errors.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/tests/test_model_routing.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/tests/test_paths.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/tests/test_permissions.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/tests/test_repl_commands.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/tests/test_repl_slash.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/tests/test_slash_commands.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/tests/test_thinking_config.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/tests/test_token_budget.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/tests/test_tools.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.3.69 → gemcode-0.3.71}/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.
|
|
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.
|
|
@@ -28,6 +28,16 @@ gemcode --yes "Add a module docstring to src/foo.py"
|
|
|
28
28
|
gemcode --session mysess --yes "Continue: run tests and fix failures"
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
### What GemCode writes to `.gemcode/`
|
|
32
|
+
|
|
33
|
+
GemCode keeps project-local state under `.gemcode/`:
|
|
34
|
+
|
|
35
|
+
- **`sessions.sqlite`**: session events/history (ADK `SqliteSessionService`)
|
|
36
|
+
- **`audit.log`**: JSONL audit trail for tool usage + model usage + stop reasons
|
|
37
|
+
- **`tool-results/`**: oversized tool outputs offloaded to stable refs (`tool_result:<sha>`)
|
|
38
|
+
- **`artifacts/`**: file artifacts (ADK `FileArtifactService`)
|
|
39
|
+
- **`policy.json`**: self-tuning per-repo profile used to calibrate dynamic budgets
|
|
40
|
+
|
|
31
41
|
- **`--yes`**: allow mutating tools (`write_file`, `search_replace`). Shell execution is still restricted by the `.env.example` allowlist.
|
|
32
42
|
- **`--session`**: Conversation history is stored under `.gemcode/sessions.sqlite` (ADK `SqliteSessionService`). Reuse the same `--session` id to continue.
|
|
33
43
|
- **`--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`.
|
|
@@ -54,6 +64,33 @@ gemcode --session mysess --yes "Continue: run tests and fix failures"
|
|
|
54
64
|
- **Recovery-loop**: ADK `ReflectAndRetryToolPlugin`-based retries on tool failures.
|
|
55
65
|
- Set `GEMCODE_ENABLE_TOOL_RECOVERY_RETRY=0` to disable.
|
|
56
66
|
- Set `GEMCODE_TOOL_REFLECT_MAX_RETRIES=1` to control retries per tool.
|
|
67
|
+
|
|
68
|
+
### Token efficiency (dynamic + intelligent)
|
|
69
|
+
|
|
70
|
+
GemCode optimizes tokens without losing capability by using a dynamic policy:
|
|
71
|
+
|
|
72
|
+
- **Context-pressure aware**: tool caps tighten when context is tight, loosen when there is room.
|
|
73
|
+
- **Risk/complexity aware**: caps increase for risky tasks (writes, shell, failures, many files).
|
|
74
|
+
- **Self-tuning per repo**: `.gemcode/policy.json` calibrates baseline evidence budgets over time.
|
|
75
|
+
|
|
76
|
+
Key env toggles:
|
|
77
|
+
|
|
78
|
+
- `GEMCODE_DYNAMIC_TOKEN_POLICY=0|1`
|
|
79
|
+
- `GEMCODE_DYNAMIC_RISK_POLICY=0|1`
|
|
80
|
+
- `GEMCODE_DYNAMIC_RISK_BOOST=<float>` (default `0.6`)
|
|
81
|
+
- `GEMCODE_TOOL_RESULT_OFFLOAD=0|1` (default `1`)
|
|
82
|
+
|
|
83
|
+
Live telemetry:
|
|
84
|
+
|
|
85
|
+
- `/status` shows `risk_score`, `context_percent_left`, and profile EMAs.
|
|
86
|
+
|
|
87
|
+
### Stable tool output offloading (OpenClaude-style)
|
|
88
|
+
|
|
89
|
+
Oversized tool outputs are automatically offloaded and replaced with stable refs:
|
|
90
|
+
|
|
91
|
+
- Stored in: `.gemcode/tool-results/`
|
|
92
|
+
- References: `tool_result:<sha256>`
|
|
93
|
+
- Load on demand: `load_tool_result(ref)`
|
|
57
94
|
- **Gemini thinking controls (Claude-like)**:
|
|
58
95
|
- By default GemCode lets Gemini use its dynamic/adaptive thinking behavior.
|
|
59
96
|
- Set `GEMCODE_DISABLE_THINKING=1` to force a best-effort “low thinking” mode:
|
|
@@ -112,12 +149,24 @@ the user’s project.
|
|
|
112
149
|
- `list_directory`
|
|
113
150
|
- `glob_files`
|
|
114
151
|
- `grep_content`
|
|
152
|
+
- `repo_map`
|
|
153
|
+
- `web_search`
|
|
154
|
+
- `web_fetch`
|
|
155
|
+
- `notebook_read`
|
|
156
|
+
- `notebook_edit`
|
|
115
157
|
- Mutating tools (require `--yes` unless your policy blocks them):
|
|
116
158
|
- `write_file`
|
|
117
159
|
- `search_replace`
|
|
118
160
|
- Shell execution:
|
|
119
161
|
- `run_command` (guarded by `GEMCODE_ALLOW_COMMANDS` from `.env.example` and
|
|
120
162
|
`GEMCODE_PERMISSION_MODE`).
|
|
163
|
+
- `bash` (pipelines + redirects; supports `background=True`)
|
|
164
|
+
|
|
165
|
+
- Background task management (for processes started via `bash(..., background=True)`):
|
|
166
|
+
- `list_tasks`, `task_output`, `kill_task`
|
|
167
|
+
|
|
168
|
+
- Tool offload loader:
|
|
169
|
+
- `load_tool_result(ref)`
|
|
121
170
|
|
|
122
171
|
Tool execution is still controlled by permission gates and then governed by
|
|
123
172
|
GemCode’s circuit breaker + recovery behavior.
|
|
@@ -245,6 +294,23 @@ pip install -e ".[dev]"
|
|
|
245
294
|
pytest
|
|
246
295
|
```
|
|
247
296
|
|
|
297
|
+
## Release workflow (GitHub Actions → PyPI)
|
|
298
|
+
|
|
299
|
+
This repository publishes to PyPI on tag pushes:
|
|
300
|
+
|
|
301
|
+
- `.github/workflows/publish-pypi.yml` triggers on tags matching `v*`
|
|
302
|
+
|
|
303
|
+
Typical release steps:
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
# 1) bump gemcode/pyproject.toml version
|
|
307
|
+
git add -A
|
|
308
|
+
git commit -m "release: vX.Y.Z"
|
|
309
|
+
git tag -a vX.Y.Z -m "vX.Y.Z"
|
|
310
|
+
git push origin HEAD
|
|
311
|
+
git push origin vX.Y.Z
|
|
312
|
+
```
|
|
313
|
+
|
|
248
314
|
## References (local only)
|
|
249
315
|
|
|
250
316
|
Do not commit proprietary leaked trees into this package. Keep `claude-code-leaked/` and similar folders outside version control or in a private mirror.
|
|
@@ -45,6 +45,12 @@ _LAST_CONTEXT_PCT = "gemcode:last_context_percent_left"
|
|
|
45
45
|
_LAST_CONTEXT_LEVEL = "gemcode:last_context_alert_level"
|
|
46
46
|
_RISK_FILES_TOUCHED = "gemcode:risk_files_touched"
|
|
47
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"
|
|
51
|
+
_TOOL_GROUP_CHARS = "gemcode:tool_group_chars"
|
|
52
|
+
_TOOL_GROUP_EXCEEDED = "gemcode:tool_group_budget_exceeded"
|
|
53
|
+
_TOOL_SEQ = "gemcode:tool_seq"
|
|
48
54
|
|
|
49
55
|
def _truthy_env(name: str, *, default: bool = False) -> bool:
|
|
50
56
|
v = os.environ.get(name)
|
|
@@ -147,6 +153,8 @@ def make_before_tool_callback(cfg: GemCodeConfig):
|
|
|
147
153
|
try:
|
|
148
154
|
if tool_context is not None:
|
|
149
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
|
|
150
158
|
st[_RISK_TOOL_CALLS] = int(st.get(_RISK_TOOL_CALLS, 0) or 0) + 1
|
|
151
159
|
if name == "read_file":
|
|
152
160
|
p = (args or {}).get("path")
|
|
@@ -165,9 +173,11 @@ def make_before_tool_callback(cfg: GemCodeConfig):
|
|
|
165
173
|
object.__setattr__(cfg, "_risk_score", cur)
|
|
166
174
|
# Writes / shell are inherently higher risk; allow more evidence.
|
|
167
175
|
if name in MUTATING_TOOLS:
|
|
176
|
+
st[_RISK_HAD_WRITE] = True
|
|
168
177
|
cur = float(getattr(cfg, "_risk_score", 0.0) or 0.0)
|
|
169
178
|
object.__setattr__(cfg, "_risk_score", min(1.0, cur + 0.12))
|
|
170
179
|
if name in SHELL_TOOLS:
|
|
180
|
+
st[_RISK_HAD_SHELL] = True
|
|
171
181
|
cur = float(getattr(cfg, "_risk_score", 0.0) or 0.0)
|
|
172
182
|
object.__setattr__(cfg, "_risk_score", min(1.0, cur + 0.08))
|
|
173
183
|
except Exception:
|
|
@@ -355,18 +365,40 @@ def make_after_tool_callback(cfg: GemCodeConfig):
|
|
|
355
365
|
except Exception:
|
|
356
366
|
pass
|
|
357
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
|
+
|
|
358
378
|
if (
|
|
359
379
|
isinstance(tool_response, dict)
|
|
360
380
|
and getattr(cfg, "tool_result_offload_enabled", False)
|
|
361
381
|
and effective_tool_chars > 0
|
|
362
382
|
):
|
|
363
383
|
try:
|
|
364
|
-
from gemcode.tool_result_store import
|
|
365
|
-
|
|
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(
|
|
366
395
|
project_root=cfg.project_root,
|
|
367
396
|
tool_name=name,
|
|
397
|
+
args=args or {},
|
|
368
398
|
payload=tool_response,
|
|
369
399
|
max_inline_chars=int(effective_tool_chars),
|
|
400
|
+
state=st,
|
|
401
|
+
seq=seq,
|
|
370
402
|
)
|
|
371
403
|
if did and isinstance(new_payload, dict):
|
|
372
404
|
tool_response = new_payload
|
|
@@ -387,6 +419,19 @@ def make_after_tool_callback(cfg: GemCodeConfig):
|
|
|
387
419
|
st = tool_context.state
|
|
388
420
|
except Exception:
|
|
389
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
|
|
390
435
|
err = isinstance(tool_response, dict) and tool_response.get("error")
|
|
391
436
|
err_kind = (
|
|
392
437
|
isinstance(tool_response, dict) and tool_response.get("error_kind")
|
|
@@ -423,9 +468,17 @@ def make_after_tool_callback(cfg: GemCodeConfig):
|
|
|
423
468
|
cur = float(getattr(cfg, "_risk_score", 0.0) or 0.0)
|
|
424
469
|
bump = 0.0
|
|
425
470
|
if err:
|
|
471
|
+
try:
|
|
472
|
+
st[_RISK_HAD_FAILURE] = True
|
|
473
|
+
except Exception:
|
|
474
|
+
pass
|
|
426
475
|
bump += 0.15
|
|
427
476
|
if isinstance(tool_response, dict) and isinstance(tool_response.get("exit_code"), int):
|
|
428
477
|
if int(tool_response["exit_code"]) != 0:
|
|
478
|
+
try:
|
|
479
|
+
st[_RISK_HAD_FAILURE] = True
|
|
480
|
+
except Exception:
|
|
481
|
+
pass
|
|
429
482
|
bump += 0.10
|
|
430
483
|
# Test/build failures should boost evidence allowance more.
|
|
431
484
|
if name in ("bash", "run_command"):
|
|
@@ -438,6 +491,27 @@ def make_after_tool_callback(cfg: GemCodeConfig):
|
|
|
438
491
|
object.__setattr__(cfg, "_risk_score", cur)
|
|
439
492
|
except Exception:
|
|
440
493
|
pass
|
|
494
|
+
|
|
495
|
+
# Persist repo calibration profile (best-effort).
|
|
496
|
+
try:
|
|
497
|
+
files = st.get(_RISK_FILES_TOUCHED, []) or []
|
|
498
|
+
files_n = len(files) if isinstance(files, list) else 0
|
|
499
|
+
tool_calls = int(st.get(_RISK_TOOL_CALLS, 0) or 0)
|
|
500
|
+
had_shell = bool(st.get(_RISK_HAD_SHELL, False))
|
|
501
|
+
had_write = bool(st.get(_RISK_HAD_WRITE, False))
|
|
502
|
+
had_failure = bool(st.get(_RISK_HAD_FAILURE, False))
|
|
503
|
+
from gemcode.policy_profile import update_profile
|
|
504
|
+
prof = update_profile(
|
|
505
|
+
cfg.project_root,
|
|
506
|
+
files_touched=files_n,
|
|
507
|
+
tool_calls=tool_calls,
|
|
508
|
+
had_shell=had_shell,
|
|
509
|
+
had_write=had_write,
|
|
510
|
+
had_failure=had_failure,
|
|
511
|
+
)
|
|
512
|
+
object.__setattr__(cfg, "_policy_profile", prof.to_dict())
|
|
513
|
+
except Exception:
|
|
514
|
+
pass
|
|
441
515
|
# ── Shell hooks: post_tool_use ────────────────────────────────────────
|
|
442
516
|
try:
|
|
443
517
|
from gemcode.hooks import run_post_tool_use_hook
|
|
@@ -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.
|
|
@@ -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
|
+
|
|
@@ -395,6 +395,7 @@ async def process_repl_slash(
|
|
|
395
395
|
try:
|
|
396
396
|
risk = float(getattr(cfg, "_risk_score", 0.0) or 0.0)
|
|
397
397
|
pct = getattr(cfg, "_context_percent_left", None)
|
|
398
|
+
prof = getattr(cfg, "_policy_profile", None)
|
|
398
399
|
out(" Dynamic policy:")
|
|
399
400
|
out(f" dynamic_token_policy: {getattr(cfg, 'dynamic_token_policy', True)}")
|
|
400
401
|
out(f" dynamic_risk_policy: {getattr(cfg, 'dynamic_risk_policy', True)}")
|
|
@@ -402,6 +403,12 @@ async def process_repl_slash(
|
|
|
402
403
|
out(f" risk_score: {risk:.2f}")
|
|
403
404
|
if isinstance(pct, int):
|
|
404
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
|
|
405
412
|
out()
|
|
406
413
|
except Exception:
|
|
407
414
|
pass
|
|
@@ -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:
|
|
@@ -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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|