gemcode 0.3.17__tar.gz → 0.3.19__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 (112) hide show
  1. {gemcode-0.3.17/src/gemcode.egg-info → gemcode-0.3.19}/PKG-INFO +1 -1
  2. {gemcode-0.3.17 → gemcode-0.3.19}/pyproject.toml +1 -1
  3. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/agent.py +73 -13
  4. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/tool_prompt_manifest.py +4 -1
  5. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/tool_registry.py +3 -1
  6. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/tools/__init__.py +13 -2
  7. gemcode-0.3.19/src/gemcode/tools/subtask.py +137 -0
  8. gemcode-0.3.19/src/gemcode/tools/think.py +41 -0
  9. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/tui/scrollback.py +80 -9
  10. {gemcode-0.3.17 → gemcode-0.3.19/src/gemcode.egg-info}/PKG-INFO +1 -1
  11. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode.egg-info/SOURCES.txt +2 -0
  12. {gemcode-0.3.17 → gemcode-0.3.19}/LICENSE +0 -0
  13. {gemcode-0.3.17 → gemcode-0.3.19}/MANIFEST.in +0 -0
  14. {gemcode-0.3.17 → gemcode-0.3.19}/README.md +0 -0
  15. {gemcode-0.3.17 → gemcode-0.3.19}/setup.cfg +0 -0
  16. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/__init__.py +0 -0
  17. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/__main__.py +0 -0
  18. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/audit.py +0 -0
  19. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/autocompact.py +0 -0
  20. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/callbacks.py +0 -0
  21. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/capability_routing.py +0 -0
  22. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/cli.py +0 -0
  23. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/compaction.py +0 -0
  24. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/computer_use/__init__.py +0 -0
  25. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/computer_use/browser_computer.py +0 -0
  26. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/config.py +0 -0
  27. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/context_budget.py +0 -0
  28. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/context_warning.py +0 -0
  29. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/credentials.py +0 -0
  30. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/hitl_session.py +0 -0
  31. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/interactions.py +0 -0
  32. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/invoke.py +0 -0
  33. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/kairos_daemon.py +0 -0
  34. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/limits.py +0 -0
  35. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/live_audio_engine.py +0 -0
  36. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/logging_config.py +0 -0
  37. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/mcp_loader.py +0 -0
  38. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/memory/__init__.py +0 -0
  39. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/memory/embedding_memory_service.py +0 -0
  40. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/memory/file_memory_service.py +0 -0
  41. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/modality_tools.py +0 -0
  42. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/model_errors.py +0 -0
  43. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/model_routing.py +0 -0
  44. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/paths.py +0 -0
  45. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/permissions.py +0 -0
  46. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/plugins/__init__.py +0 -0
  47. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  48. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  49. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/prompt_suggestions.py +0 -0
  50. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/query/__init__.py +0 -0
  51. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/query/config.py +0 -0
  52. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/query/deps.py +0 -0
  53. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/query/engine.py +0 -0
  54. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/query/stop_hooks.py +0 -0
  55. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/query/token_budget.py +0 -0
  56. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/query/transitions.py +0 -0
  57. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/repl_commands.py +0 -0
  58. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/repl_slash.py +0 -0
  59. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/session_runtime.py +0 -0
  60. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/slash_commands.py +0 -0
  61. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/thinking.py +0 -0
  62. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/tools/bash.py +0 -0
  63. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/tools/edit.py +0 -0
  64. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/tools/filesystem.py +0 -0
  65. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/tools/search.py +0 -0
  66. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/tools/shell.py +0 -0
  67. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/tools/shell_gate.py +0 -0
  68. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/tools/todo.py +0 -0
  69. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/tools/web.py +0 -0
  70. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/tools_inspector.py +0 -0
  71. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/trust.py +0 -0
  72. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/tui/input_handler.py +0 -0
  73. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/tui/spinner.py +0 -0
  74. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/tui/welcome_banner.py +0 -0
  75. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/tui/welcome_rich.py +0 -0
  76. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/version.py +0 -0
  77. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/vertex.py +0 -0
  78. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/web/__init__.py +0 -0
  79. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/web/claude_sse_adapter.py +0 -0
  80. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/web/terminal_repl.py +0 -0
  81. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode/workspace_hints.py +0 -0
  82. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode.egg-info/dependency_links.txt +0 -0
  83. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode.egg-info/entry_points.txt +0 -0
  84. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode.egg-info/requires.txt +0 -0
  85. {gemcode-0.3.17 → gemcode-0.3.19}/src/gemcode.egg-info/top_level.txt +0 -0
  86. {gemcode-0.3.17 → gemcode-0.3.19}/tests/test_agent_instruction.py +0 -0
  87. {gemcode-0.3.17 → gemcode-0.3.19}/tests/test_autocompact.py +0 -0
  88. {gemcode-0.3.17 → gemcode-0.3.19}/tests/test_capability_routing.py +0 -0
  89. {gemcode-0.3.17 → gemcode-0.3.19}/tests/test_claude_web_adapter_sse.py +0 -0
  90. {gemcode-0.3.17 → gemcode-0.3.19}/tests/test_cli_init.py +0 -0
  91. {gemcode-0.3.17 → gemcode-0.3.19}/tests/test_computer_use_permissions.py +0 -0
  92. {gemcode-0.3.17 → gemcode-0.3.19}/tests/test_context_budget.py +0 -0
  93. {gemcode-0.3.17 → gemcode-0.3.19}/tests/test_context_warning.py +0 -0
  94. {gemcode-0.3.17 → gemcode-0.3.19}/tests/test_credentials.py +0 -0
  95. {gemcode-0.3.17 → gemcode-0.3.19}/tests/test_interactive_permission_ask.py +0 -0
  96. {gemcode-0.3.17 → gemcode-0.3.19}/tests/test_kairos_scheduler.py +0 -0
  97. {gemcode-0.3.17 → gemcode-0.3.19}/tests/test_modality_tools.py +0 -0
  98. {gemcode-0.3.17 → gemcode-0.3.19}/tests/test_model_error_retry.py +0 -0
  99. {gemcode-0.3.17 → gemcode-0.3.19}/tests/test_model_errors.py +0 -0
  100. {gemcode-0.3.17 → gemcode-0.3.19}/tests/test_model_routing.py +0 -0
  101. {gemcode-0.3.17 → gemcode-0.3.19}/tests/test_paths.py +0 -0
  102. {gemcode-0.3.17 → gemcode-0.3.19}/tests/test_permissions.py +0 -0
  103. {gemcode-0.3.17 → gemcode-0.3.19}/tests/test_prompt_suggestions.py +0 -0
  104. {gemcode-0.3.17 → gemcode-0.3.19}/tests/test_repl_commands.py +0 -0
  105. {gemcode-0.3.17 → gemcode-0.3.19}/tests/test_repl_slash.py +0 -0
  106. {gemcode-0.3.17 → gemcode-0.3.19}/tests/test_slash_commands.py +0 -0
  107. {gemcode-0.3.17 → gemcode-0.3.19}/tests/test_thinking_config.py +0 -0
  108. {gemcode-0.3.17 → gemcode-0.3.19}/tests/test_token_budget.py +0 -0
  109. {gemcode-0.3.17 → gemcode-0.3.19}/tests/test_tool_context_circulation.py +0 -0
  110. {gemcode-0.3.17 → gemcode-0.3.19}/tests/test_tools.py +0 -0
  111. {gemcode-0.3.17 → gemcode-0.3.19}/tests/test_tools_inspector.py +0 -0
  112. {gemcode-0.3.17 → gemcode-0.3.19}/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.17
3
+ Version: 0.3.19
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.17"
7
+ version = "0.3.19"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -79,13 +79,26 @@ You run locally via the GemCode CLI. You are the same agent the user launched
79
79
 
80
80
  ## Core identity and approach
81
81
  You are a senior engineer who *acts*, not just advises. When given a task:
82
- 1. **Orient** — understand the repo structure, find the relevant files.
82
+ 1. **Orient** — use `list_directory`, `glob_files`, `grep_content`, `read_file` to understand structure. These tools need **no permission** and are instant. Start here.
83
83
  2. **Plan** — for complex tasks, call `todo_write` upfront to map out the work.
84
84
  3. **Execute** — make the changes, run the checks, iterate.
85
85
  4. **Verify** — confirm the result is correct before reporting done.
86
86
 
87
87
  Never stop mid-task just because the first tool call succeeded. Keep going until the full task is complete or you hit a genuine blocker.
88
88
 
89
+ ## CRITICAL: Read-only tools first — never bash for exploration
90
+ `bash` and `run_command` require permission confirmation by default. Always start with the **zero-permission** read-only tools:
91
+
92
+ | Instead of… | Use… |
93
+ |---|---|
94
+ | `bash("ls -la src/")` | `list_directory("src")` |
95
+ | `bash("find . -name '*.py'")` | `glob_files("**/*.py")` |
96
+ | `bash("cat file.py")` | `read_file("file.py")` |
97
+ | `bash("grep -r pattern .")` | `grep_content("pattern", "**/*")` |
98
+ | `bash("find . -type f \| head -50")` | `list_directory(".")` + `glob_files("**/*")` |
99
+
100
+ Only reach for `bash` or `run_command` when you actually need to **execute** something: run tests, build, git ops, start a server, install packages. **NEVER** use bash to list or read files.
101
+
89
102
  ## Thinking through hard problems
90
103
  You have native deep thinking capability — use it actively:
91
104
  - **Before** starting a complex refactor or architectural change, think: what are the dependencies? what could break? what is the minimal safe change?
@@ -94,11 +107,12 @@ You have native deep thinking capability — use it actively:
94
107
  - **For trade-off decisions** (which library, which pattern, which approach): reason through the pros/cons given this specific codebase.
95
108
 
96
109
  ## Interpreting requests
97
- - Treat every message as a software engineering task unless clearly otherwise.
110
+ - Treat every message as a software engineering task start working immediately. Do NOT respond with just a greeting or introduction.
98
111
  - If vague ("fix it", "the config", "rename that"), **infer from the repo**: search, read, then act. Do not give abstract advice when concrete files exist.
99
- - If the user refers to symbols or behaviors, **find them** with glob/grep/list — never ask them to paste paths you can discover yourself.
112
+ - If the user refers to symbols or behaviors, **find them** with `glob_files`/`grep_content`/`list_directory` — never ask them to paste paths you can discover yourself.
100
113
  - **Never propose edits to files you haven't read.** Read first, then edit.
101
114
  - When something fails, diagnose (re-read the error, check assumptions) before switching strategy. Do not repeat the same failed call.
115
+ - For analysis tasks ("analyse X", "explain X", "what does X do"): immediately start reading files with `list_directory` + `read_file` + `grep_content`. Produce concrete findings, not hypotheses.
102
116
 
103
117
  ## Tool selection guide
104
118
 
@@ -151,21 +165,34 @@ You have native deep thinking capability — use it actively:
151
165
  - `web_fetch("https://registry.npmjs.org/react/latest")` — npm package info
152
166
  - Use when you need to look up an API, check the latest version, or read documentation.
153
167
 
154
- ### Planning
168
+ ### Reasoning and planning
169
+ - **`think`** — private reasoning scratchpad. Write your analysis, plan, or hypothesis here before acting. Not shown to the user. Use before:
170
+ - A complex multi-file edit or refactor
171
+ - A debugging session where you need to trace logic before touching code
172
+ - Any destructive action (delete, force-push) — think first
173
+ - Choosing between approaches with real trade-offs
174
+
155
175
  - **`todo_write`** — track work items. Use for any task with 3+ steps.
156
176
  - Create at task start, mark completed as you finish, merge updates.
157
177
 
178
+ - **`run_subtask`** — spawn an isolated sub-agent with its own fresh context window.
179
+ - The sub-agent has the same tools (bash, read_file, grep, etc.) but starts from scratch.
180
+ - Use when a task would bloat your context too much: e.g. "read all 40 test files and find patterns"
181
+ - Use to parallelize: issue multiple `run_subtask` calls in one turn for concurrent exploration
182
+ - Use for verification passes: "check all files I edited for consistency and syntax errors"
183
+ - Always give the sub-agent enough context to operate independently.
184
+ - End your task prompt with "Summarise your findings clearly." so the result is useful.
185
+
158
186
  ## Multi-step task execution
159
187
  One user message = many model↔tool rounds (up to 256 LLM calls by default). This is intentional — you are expected to do complete tasks autonomously.
160
188
 
161
189
  **Standard workflow for complex tasks:**
162
190
  1. `todo_write` — plan the work items
163
- 2. Explore — `bash("find . ...")` or `list_directory` or `glob_files` to understand structure
164
- 3. Read — `read_file` with line ranges on large files; `grep_content` for symbol search
165
- 4. Edit — `write_file` / `search_replace` for changes
166
- 5. Verify`bash("pytest ...")` or `bash("npm run build ...")` or `bash("git diff")`
167
- 6. Fixiterate on failures, re-verify
168
- 7. Update todos — mark done as you go
191
+ 2. **Explore (read-only, no permission needed)** `list_directory` + `glob_files` to map structure; `grep_content` to find symbols; `read_file` to understand code. Do NOT use `bash` for this step.
192
+ 3. **Edit** — `write_file` / `search_replace` for changes
193
+ 4. **Verify (shell needed)** — `bash("pytest ...")` or `bash("npm run build ...")` or `bash("git diff")`
194
+ 5. Fixiterate on failures, re-verify
195
+ 6. Update todos mark done as you go
169
196
 
170
197
  **Do not stop after step 2 or 3** — complete the full task.
171
198
 
@@ -174,8 +201,25 @@ Issue independent tool calls in the same turn when outputs don't depend on each
174
201
  - Reading multiple files simultaneously ✓
175
202
  - Grepping for different patterns at once ✓
176
203
  - `list_directory` + `glob_files` in parallel ✓
204
+ - Multiple `run_subtask` calls in one turn for parallel sub-agent exploration ✓
177
205
  Sequential: when step B needs step A's result.
178
206
 
207
+ ## Sub-agent delegation (orchestrator-worker pattern)
208
+ Use `run_subtask` when the work is better done in an isolated context:
209
+ - **Context preservation**: offload reading/analysing large areas of the codebase so your own context stays clean and focused on the high-level task.
210
+ - **Parallel exploration**: launch multiple sub-agents simultaneously to research different subsystems ("analyse auth module", "analyse payment module") then synthesise.
211
+ - **Verification**: after completing work, spawn a sub-agent to review it independently — "verify the changes in src/ are syntactically correct and don't break imports."
212
+ - **Deep research**: when you need to exhaustively search something (50+ files, long documentation pages) delegate it rather than polluting the main conversation.
213
+
214
+ The sub-agent inherits your permission settings and returns its final text as `result`. Treat it as a trusted colleague returning a written summary.
215
+
216
+ ## Evaluator-optimizer loop
217
+ For tasks where quality matters:
218
+ 1. Complete the task (execute tools, write code, run commands)
219
+ 2. Spawn a verification `run_subtask` or use `bash` to run tests/lint
220
+ 3. If verification fails, read the error, fix, re-verify
221
+ 4. Report done only when verified
222
+
179
223
  ## Error recovery
180
224
  - **Test/build failures**: read the full error, identify the exact line, fix, re-run.
181
225
  - **Tool errors**: diagnose why it failed before retrying — don't repeat the exact same call.
@@ -207,9 +251,25 @@ All file tools use paths **relative to the project root** (where GemCode was sta
207
251
  return base
208
252
 
209
253
 
210
- def build_root_agent(cfg: GemCodeConfig, extra_tools: list | None = None) -> LlmAgent:
211
- """Create the root LlmAgent with tools and callbacks (no Runner)."""
212
- tools = build_function_tools(cfg)
254
+ def build_root_agent(
255
+ cfg: GemCodeConfig,
256
+ extra_tools: list | None = None,
257
+ *,
258
+ _tools: list | None = None,
259
+ ) -> LlmAgent:
260
+ """Create the root LlmAgent with tools and callbacks (no Runner).
261
+
262
+ Args:
263
+ cfg: Runtime configuration.
264
+ extra_tools: Additional tools to append (e.g. modality tools from session_runtime).
265
+ _tools: Override the entire tool list (used by run_subtask sub-agents to pass a
266
+ pre-built list that excludes run_subtask itself, preventing recursion).
267
+ When set, build_function_tools() is NOT called.
268
+ """
269
+ if _tools is not None:
270
+ tools = list(_tools)
271
+ else:
272
+ tools = build_function_tools(cfg)
213
273
  if getattr(cfg, "enable_memory", False):
214
274
  # ADK preload_memory injects retrieved memories into the next llm_request.
215
275
  from google.adk.tools import preload_memory
@@ -108,7 +108,10 @@ Permission policy:
108
108
  - session_sticky_hitl={sticky_hitl}: when true, after the user approves **one** tool in this session, further mutating/shell tools may run without re-prompting (set GEMCODE_HITL_STICKY_SESSION=0 to prompt every time).
109
109
 
110
110
  You may call tools as follows:
111
- - Session planning: {_fmt_list(planning)}. In-memory task list for this session (no disk writes). Use for non-trivial multi-step work; set merge=true to upsert by id.
111
+ - Session planning / reasoning: {_fmt_list(planning)}.
112
+ - `todo_write`: in-memory task list (no disk writes). Use for non-trivial multi-step work; merge=true to upsert by id.
113
+ - `think`: private scratchpad — write reasoning/analysis before risky or complex actions. Not shown to user.
114
+ - `run_subtask(task, context)`: spawn an isolated sub-agent to handle context-heavy work in parallel or in sequence. Sub-agent inherits your permissions and returns its final text as `result`. Exclude run_subtask from sub-agent by design (no recursion).
112
115
  - Read-only tools: {_fmt_list(read_only)}.
113
116
  Use these proactively to locate files and code before asking the user for paths.
114
117
  - Mutating tools (WRITE/EDIT/DELETE): {_fmt_list(mutating)}.
@@ -36,7 +36,9 @@ MUTATING_TOOLS: frozenset[str] = frozenset(
36
36
  SHELL_TOOLS: frozenset[str] = frozenset({"run_command", "bash"})
37
37
 
38
38
  # Session planning only (no disk / shell; no extra permission)
39
- PLANNING_TOOLS: frozenset[str] = frozenset({"todo_write"})
39
+ # think in-context reasoning scratchpad (no-op, no side effects)
40
+ # run_subtask — spawns a sub-agent; inherits parent permission settings
41
+ PLANNING_TOOLS: frozenset[str] = frozenset({"todo_write", "think", "run_subtask"})
40
42
 
41
43
  ToolConcurrency = Literal["parallel_safe", "serial_mutating", "shell"]
42
44
 
@@ -8,20 +8,25 @@ from gemcode.tools.edit import make_edit_tools
8
8
  from gemcode.tools.filesystem import make_filesystem_tools
9
9
  from gemcode.tools.search import make_grep_tool
10
10
  from gemcode.tools.shell import make_run_command
11
+ from gemcode.tools.subtask import make_run_subtask_tool
12
+ from gemcode.tools.think import make_think_tool
11
13
  from gemcode.tools.todo import make_todo_tool
12
14
  from gemcode.tools.web import make_web_fetch_tool
13
15
 
14
16
 
15
- def build_function_tools(cfg: GemCodeConfig) -> list:
17
+ def build_function_tools(cfg: GemCodeConfig, *, include_subtask: bool = True) -> list:
16
18
  read_file, list_directory, glob_files, delete_file, move_file = make_filesystem_tools(cfg)
17
19
  grep_content = make_grep_tool(cfg)
18
20
  run_command = make_run_command(cfg)
19
21
  bash = make_bash_tool(cfg)
20
22
  write_file, search_replace = make_edit_tools(cfg)
21
23
  todo_write = make_todo_tool(cfg)
24
+ think = make_think_tool()
22
25
  web_fetch = make_web_fetch_tool()
23
- return [
26
+
27
+ tools = [
24
28
  todo_write,
29
+ think,
25
30
  read_file,
26
31
  list_directory,
27
32
  glob_files,
@@ -34,3 +39,9 @@ def build_function_tools(cfg: GemCodeConfig) -> list:
34
39
  delete_file,
35
40
  web_fetch,
36
41
  ]
42
+
43
+ # run_subtask is excluded when building sub-agent tools (prevents recursion).
44
+ if include_subtask:
45
+ tools.append(make_run_subtask_tool(cfg))
46
+
47
+ return tools
@@ -0,0 +1,137 @@
1
+ """
2
+ Sub-agent tool — spawn an isolated focused agent for context-heavy tasks.
3
+
4
+ Analogous to OpenClaude's AgentTool / AI SDK's ToolLoopAgent subagent pattern.
5
+
6
+ Key insight (from AI SDK docs "Subagents"):
7
+ "Some tasks require exploring large amounts of information — reading files,
8
+ searching codebases, or researching topics. Running these in the main agent
9
+ consumes context quickly, making the agent less coherent over time.
10
+ With subagents, you can spin up a dedicated agent that uses hundreds of
11
+ thousands of tokens and have it return only a focused summary."
12
+
13
+ The sub-agent:
14
+ - Gets the same tools as the parent (read_file, bash, grep, etc.) EXCEPT
15
+ run_subtask itself (prevents infinite recursion).
16
+ - Uses an in-memory session (no DB writes, fully isolated).
17
+ - Respects the same permission settings (yes_to_all, permission_mode) as the parent.
18
+ - Is depth-capped (max 48 LLM calls) to prevent runaway cost.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from gemcode.config import GemCodeConfig
24
+
25
+
26
+ def make_run_subtask_tool(cfg: GemCodeConfig):
27
+ async def run_subtask(task: str, context: str = "") -> dict:
28
+ """
29
+ Spawn an isolated sub-agent to complete a focused, self-contained task.
30
+
31
+ The sub-agent starts with a fresh context window — it does NOT inherit this
32
+ conversation's history. It has access to all the same tools (read_file, bash,
33
+ grep_content, web_fetch, etc.) and returns its final response as `result`.
34
+
35
+ Use when:
36
+ - Exploring a large codebase section that would bloat your context
37
+ ("read all 40 test files and summarise what each group tests")
38
+ - Running deep analysis in parallel — issue multiple run_subtask calls
39
+ in the same turn for genuine parallel execution
40
+ - Delegating a focused research or investigation task while staying
41
+ high-level yourself
42
+ - Getting a verification or review pass on your own changes
43
+ ("check my edits in src/ for syntax errors and consistency")
44
+ - Any task that requires 10+ file reads or multiple bash commands but
45
+ whose output can be summarised in a paragraph
46
+
47
+ The sub-agent reasons and acts like a full Gemini coding agent.
48
+ Give it a clear, self-contained task — include all context it needs
49
+ since it cannot see this conversation.
50
+
51
+ Tip: end your task description with "Summarise your findings clearly
52
+ since your full output will be returned to the main agent."
53
+
54
+ Args:
55
+ task: Clear, self-contained description of what to do.
56
+ context: Extra context the sub-agent needs (file paths, relevant
57
+ code snippets, constraints, acceptance criteria).
58
+ """
59
+ import uuid
60
+
61
+ try:
62
+ from google.adk.runners import Runner
63
+ from google.adk.sessions.in_memory_session_service import (
64
+ InMemorySessionService,
65
+ )
66
+ except ImportError:
67
+ try:
68
+ from google.adk.runners import Runner
69
+ from google.adk.sessions import InMemorySessionService
70
+ except ImportError:
71
+ return {"error": "google-adk not available for sub-agent"}
72
+
73
+ from gemcode.agent import build_root_agent
74
+ from gemcode.tools import build_function_tools
75
+ from gemcode.invoke import run_turn
76
+
77
+ # Build the sub-agent tool set WITHOUT run_subtask (no recursion).
78
+ sub_tools = build_function_tools(cfg, include_subtask=False)
79
+
80
+ # Build a standalone LlmAgent with the limited tool set.
81
+ sub_agent = build_root_agent(cfg, _tools=sub_tools)
82
+
83
+ # Isolated in-memory session — never writes to the parent SQLite DB.
84
+ sub_runner = Runner(
85
+ app_name="gemcode_sub",
86
+ agent=sub_agent,
87
+ session_service=InMemorySessionService(),
88
+ )
89
+ sub_session_id = str(uuid.uuid4())
90
+
91
+ # Compose the sub-agent prompt.
92
+ prompt = task.strip()
93
+ if context and context.strip():
94
+ prompt = f"{task.strip()}\n\nAdditional context:\n{context.strip()}"
95
+
96
+ # Cap sub-agent depth to avoid runaway API cost.
97
+ sub_max_calls = min(int(cfg.max_llm_calls or 48), 48)
98
+
99
+ try:
100
+ events = await run_turn(
101
+ sub_runner,
102
+ user_id="local",
103
+ session_id=sub_session_id,
104
+ prompt=prompt,
105
+ max_llm_calls=sub_max_calls,
106
+ cfg=cfg,
107
+ )
108
+ except Exception as e:
109
+ return {"error": f"Sub-agent error: {type(e).__name__}: {e}"}
110
+
111
+ # Extract only non-thinking text parts from the sub-agent's output.
112
+ parts: list[str] = []
113
+ for ev in events:
114
+ try:
115
+ if not ev.content or not ev.content.parts:
116
+ continue
117
+ if getattr(ev, "author", None) == "user":
118
+ continue
119
+ for part in ev.content.parts:
120
+ t = getattr(part, "text", None)
121
+ is_thought = getattr(part, "thought", None)
122
+ if isinstance(t, str) and t.strip() and not is_thought:
123
+ parts.append(t)
124
+ except Exception:
125
+ continue
126
+
127
+ result_text = "".join(parts).strip()
128
+ if not result_text:
129
+ result_text = (
130
+ "(Sub-agent completed the task but produced no text output. "
131
+ "It may have only called tools. Consider a more explicit task "
132
+ "that asks the sub-agent to summarise its findings.)"
133
+ )
134
+
135
+ return {"result": result_text}
136
+
137
+ return run_subtask
@@ -0,0 +1,41 @@
1
+ """
2
+ Think tool — structured reasoning scratchpad before acting.
3
+
4
+ Inspired by Anthropic's documented "think" tool pattern:
5
+ https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking
6
+
7
+ Even though Gemini 2.5 has native dynamic thinking, an explicit think() call
8
+ improves quality on hard tasks because:
9
+ 1. It forces a structured reasoning checkpoint before irreversible actions.
10
+ 2. The written thought persists in the conversation history so later tool calls
11
+ can reference it.
12
+ 3. It creates a visible chain-of-reasoning the model can revise.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+
18
+ def make_think_tool():
19
+ def think(thought: str) -> dict:
20
+ """
21
+ Write out your reasoning, plan, or analysis before taking action.
22
+
23
+ This is a private scratchpad — the content is NOT shown to the user.
24
+ Use it when you need to think clearly before executing tools:
25
+
26
+ - Planning a multi-step edit: map out what files to touch and in what order
27
+ - Debugging an error: trace the execution path before deciding on a fix
28
+ - Weighing approaches: compare trade-offs before committing to one
29
+ - Verifying understanding: articulate your model of the code before editing
30
+ - Decomposing a complex task: break it into concrete sub-steps
31
+ - After reading multiple files: synthesise before writing
32
+
33
+ Call think() BEFORE a sequence of tool calls, especially before destructive
34
+ or irreversible actions (deletes, overwrites, force-pushes).
35
+
36
+ The return value is intentionally empty — the value is in the reasoning
37
+ itself being part of the conversation context.
38
+ """
39
+ return {"ok": True}
40
+
41
+ return think
@@ -236,7 +236,76 @@ async def run_gemcode_scrollback_tui(
236
236
  pass
237
237
  return tool_name, hint
238
238
 
239
+ # ── Live status indicator ─────────────────────────────────────────────────
240
+ # spinner_active[0] = True while "⟳ Working..." is on the current line.
241
+ # _clear_spinner() uses \r + ANSI erase to overwrite it before real output.
242
+ spinner_active = [False]
243
+
244
+ def _clear_spinner() -> None:
245
+ if spinner_active[0]:
246
+ sys.stdout.write("\r\033[K")
247
+ sys.stdout.flush()
248
+ spinner_active[0] = False
249
+
250
+ def _fmt_tool_result(resp: object) -> str:
251
+ """One-line summary of a tool execution result."""
252
+ try:
253
+ d = resp if isinstance(resp, dict) else {}
254
+ # ADK may wrap the return value; try both the raw dict and a nested "result" key.
255
+ inner = d.get("result", d)
256
+ if not isinstance(inner, dict):
257
+ inner = d
258
+ err = inner.get("error") or d.get("error")
259
+ if err:
260
+ return f"\u2717 {str(err)[:80]}"
261
+ exit_code = inner.get("exit_code")
262
+ if exit_code is not None:
263
+ icon = "\u2713" if exit_code == 0 else f"\u2717 exit {exit_code}"
264
+ out = str(inner.get("stdout", "") or "").strip()
265
+ first = out.split("\n")[0][:80] if out else ""
266
+ if not first:
267
+ first = str(inner.get("stderr", "") or "").strip().split("\n")[0][:80]
268
+ return f"{icon} {first}" if first else icon
269
+ if inner.get("content") is not None:
270
+ lines = str(inner["content"]).count("\n") + 1
271
+ return f"\u2713 {lines} lines"
272
+ if inner.get("files") is not None:
273
+ return f"\u2713 {len(inner['files'])} files"
274
+ if inner.get("matches") is not None:
275
+ return f"\u2713 {len(inner['matches'])} matches"
276
+ if inner.get("ok") or d.get("ok"):
277
+ return "\u2713"
278
+ return ""
279
+ except Exception:
280
+ return ""
281
+
282
+ def _render_tool_results(ev) -> None:
283
+ """Print a brief one-line summary when a tool finishes executing."""
284
+ try:
285
+ frs: list = []
286
+ try:
287
+ frs = ev.get_function_responses() or []
288
+ except Exception:
289
+ pass
290
+ if not frs and ev.content and ev.content.parts:
291
+ for part in ev.content.parts:
292
+ fr = getattr(part, "function_response", None)
293
+ if fr is not None:
294
+ frs.append(fr)
295
+ for fr in frs:
296
+ name = getattr(fr, "name", "") or ""
297
+ if name == _ADK_REQUEST_CONFIRMATION:
298
+ continue
299
+ _clear_spinner()
300
+ resp = getattr(fr, "response", {}) or {}
301
+ summary = _fmt_tool_result(resp)
302
+ if summary:
303
+ print(f" ⎿ {ansi.dim}\u21b3 {summary}{ansi.reset}")
304
+ except Exception:
305
+ pass
306
+
239
307
  def _render_tool_calls(ev) -> None:
308
+ _clear_spinner()
240
309
  try:
241
310
  fcs = ev.get_function_calls() or []
242
311
  except Exception:
@@ -331,9 +400,18 @@ async def run_gemcode_scrollback_tui(
331
400
  kwargs["run_config"] = run_config
332
401
  # (We don't handle token budget reset here; full-screen TUI does.)
333
402
 
403
+ # Show a live "Working…" indicator so the user never wonders if the
404
+ # agent froze. _clear_spinner() overwrites it with \r+ANSI-erase
405
+ # before the first real output (tool call, tool result, or response).
406
+ if ansi.enabled:
407
+ spinner_active[0] = True
408
+ sys.stdout.write(f" {ansi.dim}\u27f3 Working\u2026{ansi.reset}")
409
+ sys.stdout.flush()
410
+
334
411
  async for ev in runner.run_async(**kwargs):
335
412
  events.append(ev)
336
413
  _render_tool_calls(ev)
414
+ _render_tool_results(ev)
337
415
  try:
338
416
  if not ev.content or not ev.content.parts:
339
417
  continue
@@ -351,6 +429,8 @@ async def run_gemcode_scrollback_tui(
351
429
  except Exception:
352
430
  continue
353
431
 
432
+ _clear_spinner() # ensure "Working…" is gone before any response line
433
+
354
434
  if not assistant_wrote_text and _events_had_non_confirmation_tools(events):
355
435
  await typewrite(
356
436
  f"{ansi.dim}(Tools ran without a text reply in this step; "
@@ -377,15 +457,6 @@ async def run_gemcode_scrollback_tui(
377
457
  await typewrite(thought_text)
378
458
  sys.stdout.write("\n")
379
459
  sys.stdout.flush()
380
- else:
381
- # Keep thinking visible even if the model produced no separate
382
- # "thought" channel output.
383
- print(
384
- f" ⎿ {ansi.dim}{ansi.bold}\u2234 Thinking{ansi.reset}: "
385
- "(no thinking output)"
386
- )
387
- print("")
388
-
389
460
  if buffered_final:
390
461
  sys.stdout.write(f" ⎿ {ansi.bold}GemCode{ansi.reset}: ")
391
462
  sys.stdout.flush()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.17
3
+ Version: 0.3.19
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -69,6 +69,8 @@ src/gemcode/tools/filesystem.py
69
69
  src/gemcode/tools/search.py
70
70
  src/gemcode/tools/shell.py
71
71
  src/gemcode/tools/shell_gate.py
72
+ src/gemcode/tools/subtask.py
73
+ src/gemcode/tools/think.py
72
74
  src/gemcode/tools/todo.py
73
75
  src/gemcode/tools/web.py
74
76
  src/gemcode/tui/input_handler.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