gemcode 0.3.16__tar.gz → 0.3.18__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.16/src/gemcode.egg-info → gemcode-0.3.18}/PKG-INFO +2 -1
  2. {gemcode-0.3.16 → gemcode-0.3.18}/pyproject.toml +2 -1
  3. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/agent.py +51 -4
  4. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/tool_prompt_manifest.py +4 -1
  5. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/tool_registry.py +3 -1
  6. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/tools/__init__.py +13 -2
  7. gemcode-0.3.18/src/gemcode/tools/subtask.py +137 -0
  8. gemcode-0.3.18/src/gemcode/tools/think.py +41 -0
  9. gemcode-0.3.18/src/gemcode/tui/input_handler.py +182 -0
  10. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/tui/scrollback.py +20 -4
  11. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/tui/welcome_rich.py +6 -1
  12. {gemcode-0.3.16 → gemcode-0.3.18/src/gemcode.egg-info}/PKG-INFO +2 -1
  13. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode.egg-info/SOURCES.txt +3 -0
  14. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode.egg-info/requires.txt +1 -0
  15. {gemcode-0.3.16 → gemcode-0.3.18}/LICENSE +0 -0
  16. {gemcode-0.3.16 → gemcode-0.3.18}/MANIFEST.in +0 -0
  17. {gemcode-0.3.16 → gemcode-0.3.18}/README.md +0 -0
  18. {gemcode-0.3.16 → gemcode-0.3.18}/setup.cfg +0 -0
  19. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/__init__.py +0 -0
  20. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/__main__.py +0 -0
  21. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/audit.py +0 -0
  22. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/autocompact.py +0 -0
  23. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/callbacks.py +0 -0
  24. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/capability_routing.py +0 -0
  25. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/cli.py +0 -0
  26. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/compaction.py +0 -0
  27. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/computer_use/__init__.py +0 -0
  28. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/computer_use/browser_computer.py +0 -0
  29. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/config.py +0 -0
  30. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/context_budget.py +0 -0
  31. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/context_warning.py +0 -0
  32. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/credentials.py +0 -0
  33. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/hitl_session.py +0 -0
  34. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/interactions.py +0 -0
  35. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/invoke.py +0 -0
  36. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/kairos_daemon.py +0 -0
  37. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/limits.py +0 -0
  38. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/live_audio_engine.py +0 -0
  39. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/logging_config.py +0 -0
  40. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/mcp_loader.py +0 -0
  41. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/memory/__init__.py +0 -0
  42. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/memory/embedding_memory_service.py +0 -0
  43. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/memory/file_memory_service.py +0 -0
  44. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/modality_tools.py +0 -0
  45. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/model_errors.py +0 -0
  46. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/model_routing.py +0 -0
  47. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/paths.py +0 -0
  48. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/permissions.py +0 -0
  49. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/plugins/__init__.py +0 -0
  50. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  51. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  52. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/prompt_suggestions.py +0 -0
  53. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/query/__init__.py +0 -0
  54. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/query/config.py +0 -0
  55. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/query/deps.py +0 -0
  56. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/query/engine.py +0 -0
  57. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/query/stop_hooks.py +0 -0
  58. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/query/token_budget.py +0 -0
  59. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/query/transitions.py +0 -0
  60. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/repl_commands.py +0 -0
  61. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/repl_slash.py +0 -0
  62. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/session_runtime.py +0 -0
  63. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/slash_commands.py +0 -0
  64. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/thinking.py +0 -0
  65. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/tools/bash.py +0 -0
  66. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/tools/edit.py +0 -0
  67. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/tools/filesystem.py +0 -0
  68. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/tools/search.py +0 -0
  69. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/tools/shell.py +0 -0
  70. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/tools/shell_gate.py +0 -0
  71. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/tools/todo.py +0 -0
  72. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/tools/web.py +0 -0
  73. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/tools_inspector.py +0 -0
  74. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/trust.py +0 -0
  75. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/tui/spinner.py +0 -0
  76. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/tui/welcome_banner.py +0 -0
  77. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/version.py +0 -0
  78. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/vertex.py +0 -0
  79. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/web/__init__.py +0 -0
  80. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/web/claude_sse_adapter.py +0 -0
  81. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/web/terminal_repl.py +0 -0
  82. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode/workspace_hints.py +0 -0
  83. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode.egg-info/dependency_links.txt +0 -0
  84. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode.egg-info/entry_points.txt +0 -0
  85. {gemcode-0.3.16 → gemcode-0.3.18}/src/gemcode.egg-info/top_level.txt +0 -0
  86. {gemcode-0.3.16 → gemcode-0.3.18}/tests/test_agent_instruction.py +0 -0
  87. {gemcode-0.3.16 → gemcode-0.3.18}/tests/test_autocompact.py +0 -0
  88. {gemcode-0.3.16 → gemcode-0.3.18}/tests/test_capability_routing.py +0 -0
  89. {gemcode-0.3.16 → gemcode-0.3.18}/tests/test_claude_web_adapter_sse.py +0 -0
  90. {gemcode-0.3.16 → gemcode-0.3.18}/tests/test_cli_init.py +0 -0
  91. {gemcode-0.3.16 → gemcode-0.3.18}/tests/test_computer_use_permissions.py +0 -0
  92. {gemcode-0.3.16 → gemcode-0.3.18}/tests/test_context_budget.py +0 -0
  93. {gemcode-0.3.16 → gemcode-0.3.18}/tests/test_context_warning.py +0 -0
  94. {gemcode-0.3.16 → gemcode-0.3.18}/tests/test_credentials.py +0 -0
  95. {gemcode-0.3.16 → gemcode-0.3.18}/tests/test_interactive_permission_ask.py +0 -0
  96. {gemcode-0.3.16 → gemcode-0.3.18}/tests/test_kairos_scheduler.py +0 -0
  97. {gemcode-0.3.16 → gemcode-0.3.18}/tests/test_modality_tools.py +0 -0
  98. {gemcode-0.3.16 → gemcode-0.3.18}/tests/test_model_error_retry.py +0 -0
  99. {gemcode-0.3.16 → gemcode-0.3.18}/tests/test_model_errors.py +0 -0
  100. {gemcode-0.3.16 → gemcode-0.3.18}/tests/test_model_routing.py +0 -0
  101. {gemcode-0.3.16 → gemcode-0.3.18}/tests/test_paths.py +0 -0
  102. {gemcode-0.3.16 → gemcode-0.3.18}/tests/test_permissions.py +0 -0
  103. {gemcode-0.3.16 → gemcode-0.3.18}/tests/test_prompt_suggestions.py +0 -0
  104. {gemcode-0.3.16 → gemcode-0.3.18}/tests/test_repl_commands.py +0 -0
  105. {gemcode-0.3.16 → gemcode-0.3.18}/tests/test_repl_slash.py +0 -0
  106. {gemcode-0.3.16 → gemcode-0.3.18}/tests/test_slash_commands.py +0 -0
  107. {gemcode-0.3.16 → gemcode-0.3.18}/tests/test_thinking_config.py +0 -0
  108. {gemcode-0.3.16 → gemcode-0.3.18}/tests/test_token_budget.py +0 -0
  109. {gemcode-0.3.16 → gemcode-0.3.18}/tests/test_tool_context_circulation.py +0 -0
  110. {gemcode-0.3.16 → gemcode-0.3.18}/tests/test_tools.py +0 -0
  111. {gemcode-0.3.16 → gemcode-0.3.18}/tests/test_tools_inspector.py +0 -0
  112. {gemcode-0.3.16 → gemcode-0.3.18}/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.16
3
+ Version: 0.3.18
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -179,6 +179,7 @@ Requires-Dist: google-adk>=1.0.0
179
179
  Requires-Dist: google-genai>=1.0.0
180
180
  Requires-Dist: python-dotenv>=1.0.0
181
181
  Requires-Dist: rich>=13.0.0
182
+ Requires-Dist: prompt_toolkit>=3.0.0
182
183
  Provides-Extra: dev
183
184
  Requires-Dist: pytest>=8.0.0; extra == "dev"
184
185
  Requires-Dist: pytest-asyncio>=0.24.0; extra == "dev"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gemcode"
7
- version = "0.3.16"
7
+ version = "0.3.18"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -31,6 +31,7 @@ dependencies = [
31
31
  "google-genai>=1.0.0",
32
32
  "python-dotenv>=1.0.0",
33
33
  "rich>=13.0.0",
34
+ "prompt_toolkit>=3.0.0",
34
35
  ]
35
36
 
36
37
  [project.urls]
@@ -151,10 +151,24 @@ You have native deep thinking capability — use it actively:
151
151
  - `web_fetch("https://registry.npmjs.org/react/latest")` — npm package info
152
152
  - Use when you need to look up an API, check the latest version, or read documentation.
153
153
 
154
- ### Planning
154
+ ### Reasoning and planning
155
+ - **`think`** — private reasoning scratchpad. Write your analysis, plan, or hypothesis here before acting. Not shown to the user. Use before:
156
+ - A complex multi-file edit or refactor
157
+ - A debugging session where you need to trace logic before touching code
158
+ - Any destructive action (delete, force-push) — think first
159
+ - Choosing between approaches with real trade-offs
160
+
155
161
  - **`todo_write`** — track work items. Use for any task with 3+ steps.
156
162
  - Create at task start, mark completed as you finish, merge updates.
157
163
 
164
+ - **`run_subtask`** — spawn an isolated sub-agent with its own fresh context window.
165
+ - The sub-agent has the same tools (bash, read_file, grep, etc.) but starts from scratch.
166
+ - Use when a task would bloat your context too much: e.g. "read all 40 test files and find patterns"
167
+ - Use to parallelize: issue multiple `run_subtask` calls in one turn for concurrent exploration
168
+ - Use for verification passes: "check all files I edited for consistency and syntax errors"
169
+ - Always give the sub-agent enough context to operate independently.
170
+ - End your task prompt with "Summarise your findings clearly." so the result is useful.
171
+
158
172
  ## Multi-step task execution
159
173
  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
174
 
@@ -174,8 +188,25 @@ Issue independent tool calls in the same turn when outputs don't depend on each
174
188
  - Reading multiple files simultaneously ✓
175
189
  - Grepping for different patterns at once ✓
176
190
  - `list_directory` + `glob_files` in parallel ✓
191
+ - Multiple `run_subtask` calls in one turn for parallel sub-agent exploration ✓
177
192
  Sequential: when step B needs step A's result.
178
193
 
194
+ ## Sub-agent delegation (orchestrator-worker pattern)
195
+ Use `run_subtask` when the work is better done in an isolated context:
196
+ - **Context preservation**: offload reading/analysing large areas of the codebase so your own context stays clean and focused on the high-level task.
197
+ - **Parallel exploration**: launch multiple sub-agents simultaneously to research different subsystems ("analyse auth module", "analyse payment module") then synthesise.
198
+ - **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."
199
+ - **Deep research**: when you need to exhaustively search something (50+ files, long documentation pages) delegate it rather than polluting the main conversation.
200
+
201
+ The sub-agent inherits your permission settings and returns its final text as `result`. Treat it as a trusted colleague returning a written summary.
202
+
203
+ ## Evaluator-optimizer loop
204
+ For tasks where quality matters:
205
+ 1. Complete the task (execute tools, write code, run commands)
206
+ 2. Spawn a verification `run_subtask` or use `bash` to run tests/lint
207
+ 3. If verification fails, read the error, fix, re-verify
208
+ 4. Report done only when verified
209
+
179
210
  ## Error recovery
180
211
  - **Test/build failures**: read the full error, identify the exact line, fix, re-run.
181
212
  - **Tool errors**: diagnose why it failed before retrying — don't repeat the exact same call.
@@ -207,9 +238,25 @@ All file tools use paths **relative to the project root** (where GemCode was sta
207
238
  return base
208
239
 
209
240
 
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)
241
+ def build_root_agent(
242
+ cfg: GemCodeConfig,
243
+ extra_tools: list | None = None,
244
+ *,
245
+ _tools: list | None = None,
246
+ ) -> LlmAgent:
247
+ """Create the root LlmAgent with tools and callbacks (no Runner).
248
+
249
+ Args:
250
+ cfg: Runtime configuration.
251
+ extra_tools: Additional tools to append (e.g. modality tools from session_runtime).
252
+ _tools: Override the entire tool list (used by run_subtask sub-agents to pass a
253
+ pre-built list that excludes run_subtask itself, preventing recursion).
254
+ When set, build_function_tools() is NOT called.
255
+ """
256
+ if _tools is not None:
257
+ tools = list(_tools)
258
+ else:
259
+ tools = build_function_tools(cfg)
213
260
  if getattr(cfg, "enable_memory", False):
214
261
  # ADK preload_memory injects retrieved memories into the next llm_request.
215
262
  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
@@ -0,0 +1,182 @@
1
+ """
2
+ Interactive input handler for the GemCode REPL.
3
+
4
+ Provides (when prompt_toolkit is available):
5
+ - Slash command autocomplete — type / and see all commands with descriptions;
6
+ use ↑ / ↓ arrows to navigate the list, Tab or Enter to accept, Esc to dismiss.
7
+ - Conversation history — ↑ / ↓ arrows cycle through previous messages (when
8
+ the completion popup is not open).
9
+ - Status bar — shows the active model and session ID while typing.
10
+ - Styled ❯ prompt in GemCode blue.
11
+
12
+ Falls back gracefully to plain input() when prompt_toolkit is unavailable or
13
+ stdout is not a TTY (e.g. piped input, CI).
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import sys
19
+ from typing import Callable
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Availability guard — import everything inside try so we never hard-crash
23
+ # ---------------------------------------------------------------------------
24
+ _PT_AVAILABLE = False
25
+ try:
26
+ from prompt_toolkit import PromptSession
27
+ from prompt_toolkit.completion import Completer, Completion
28
+ from prompt_toolkit.document import Document # noqa: F401 (type hint only)
29
+ from prompt_toolkit.formatted_text import HTML
30
+ from prompt_toolkit.history import InMemoryHistory
31
+ from prompt_toolkit.key_binding import KeyBindings
32
+ from prompt_toolkit.styles import Style
33
+
34
+ _PT_AVAILABLE = True
35
+ except ImportError:
36
+ pass
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Slash command registry
40
+ # (name without /, description shown in the autocomplete popup)
41
+ # ---------------------------------------------------------------------------
42
+ SLASH_COMMANDS: list[tuple[str, str]] = [
43
+ ("help", "List all available commands"),
44
+ ("clear", "Start a fresh session — clears conversation history"),
45
+ ("model", "View or switch model · /model use <id> · /model list"),
46
+ ("status", "Show session ID, model, and current settings"),
47
+ ("context", "Show context window usage and token counts"),
48
+ ("tools", "List all tools and their permission categories"),
49
+ ("compact", "Summarise conversation history to reclaim context space"),
50
+ ("config", "Show active configuration and environment variables"),
51
+ ("permissions", "Show current permission mode (default / strict / yes)"),
52
+ ("audit", "Show recent audit log · /audit [N lines]"),
53
+ ("memory", "Show memory / storage settings"),
54
+ ("doctor", "Run diagnostics and validate the environment"),
55
+ ("hooks", "Show hooks configuration"),
56
+ ("version", "Show installed GemCode version"),
57
+ ("exit", "Exit GemCode"),
58
+ ]
59
+
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Completer
63
+ # ---------------------------------------------------------------------------
64
+ if _PT_AVAILABLE:
65
+ class _SlashCommandCompleter(Completer):
66
+ """
67
+ Activates when the input starts with '/'.
68
+ Supports prefix matching and substring matching so typing '/mo' shows
69
+ /model, '/compact', etc.
70
+ """
71
+
72
+ def get_completions(self, document, complete_event):
73
+ text = document.text_before_cursor
74
+ if not text.startswith("/"):
75
+ return
76
+ query = text[1:].lower()
77
+ for name, desc in SLASH_COMMANDS:
78
+ if not query or name.startswith(query) or query in name:
79
+ yield Completion(
80
+ "/" + name,
81
+ start_position=-len(text),
82
+ display=HTML(f"<ansiblue>/<b>{name}</b></ansiblue>"),
83
+ display_meta=desc,
84
+ )
85
+
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # Public class
89
+ # ---------------------------------------------------------------------------
90
+ class GemCodeInputHandler:
91
+ """
92
+ Wraps prompt_toolkit PromptSession with GemCode-specific styling and slash
93
+ command autocomplete. Use ``await handler.prompt_async()`` in the async
94
+ REPL loop.
95
+
96
+ When prompt_toolkit is not installed (or stdout is not a TTY) every call
97
+ transparently delegates to ``input()``.
98
+ """
99
+
100
+ def __init__(
101
+ self,
102
+ *,
103
+ ansi_enabled: bool = True,
104
+ get_model: Callable[[], str] | None = None,
105
+ get_session_id: Callable[[], str] | None = None,
106
+ ) -> None:
107
+ self._ansi = ansi_enabled
108
+ self._get_model = get_model or (lambda: "gemini")
109
+ self._get_session_id = get_session_id or (lambda: "")
110
+ self._session: "PromptSession | None" = None
111
+
112
+ if _PT_AVAILABLE and sys.stdin.isatty() and sys.stdout.isatty():
113
+ self._build_session()
114
+
115
+ def _build_session(self) -> None:
116
+ style = Style.from_dict(
117
+ {
118
+ # The ❯ prompt glyph
119
+ "prompt": "#5fafd7 bold",
120
+ # Completion popup rows
121
+ "completion-menu.completion": "bg:#0d2035 fg:#87cefa",
122
+ "completion-menu.completion.current": "bg:#0087d7 fg:#ffffff bold",
123
+ # Meta (description) column
124
+ "completion-menu.meta.completion": "bg:#0d2035 fg:#5a7a9a",
125
+ "completion-menu.meta.completion.current": "bg:#0065a0 fg:#cce8ff",
126
+ "completion-menu.border": "#0087d7",
127
+ # Bottom toolbar
128
+ "bottom-toolbar": "bg:#0d1f2d fg:#4a7a9a",
129
+ }
130
+ )
131
+
132
+ get_model = self._get_model
133
+ get_session_id = self._get_session_id
134
+
135
+ def bottom_toolbar() -> HTML:
136
+ model = get_model() or "gemini"
137
+ sid = get_session_id()
138
+ sid_short = sid[:8] if len(sid) >= 8 else sid
139
+ return HTML(
140
+ f'<style bg="#0d1f2d" fg="#5fafd7"><b> ◆ {model}</b></style>'
141
+ f'<style bg="#0d1f2d" fg="#3d6080"> · session {sid_short}'
142
+ f' · / for commands</style>'
143
+ )
144
+
145
+ kb = KeyBindings()
146
+
147
+ @kb.add("c-c")
148
+ def _ctrl_c(event):
149
+ """Ctrl+C clears current line (like a real shell)."""
150
+ event.app.current_buffer.reset()
151
+
152
+ self._session = PromptSession(
153
+ history=InMemoryHistory(),
154
+ completer=_SlashCommandCompleter(),
155
+ complete_while_typing=True,
156
+ style=style,
157
+ bottom_toolbar=bottom_toolbar,
158
+ key_bindings=kb,
159
+ mouse_support=False,
160
+ complete_in_thread=True,
161
+ reserve_space_for_menu=10,
162
+ # Show completion descriptions to the right (like VS Code)
163
+ complete_style="COLUMN",
164
+ )
165
+
166
+ def is_interactive(self) -> bool:
167
+ return self._session is not None
168
+
169
+ async def prompt_async(self) -> str:
170
+ """
171
+ Async prompt — returns stripped user input.
172
+ Raises EOFError on Ctrl+D (caller should handle gracefully).
173
+ """
174
+ if self._session is not None:
175
+ from prompt_toolkit.formatted_text import HTML as _HTML
176
+
177
+ result = await self._session.prompt_async(
178
+ _HTML('<style fg="#5fafd7"><b>❯</b></style> '),
179
+ )
180
+ return (result or "").strip()
181
+ else:
182
+ return input("❯ ").strip()
@@ -14,6 +14,7 @@ from gemcode.capability_routing import apply_capability_routing
14
14
  from gemcode.config import load_cli_environment
15
15
  from gemcode.model_routing import pick_effective_model
16
16
  from gemcode.repl_slash import process_repl_slash
17
+ from gemcode.tui.input_handler import GemCodeInputHandler
17
18
  from gemcode.tui.welcome_rich import print_shortcuts_hint, print_welcome_dashboard
18
19
 
19
20
  _ADK_REQUEST_CONFIRMATION = "adk_request_confirmation"
@@ -185,6 +186,17 @@ async def run_gemcode_scrollback_tui(
185
186
 
186
187
  char_delay_ms = int(os.environ.get("GEMCODE_TUI_CHAR_DELAY_MS", "0") or "0")
187
188
 
189
+ # Build the interactive input handler (prompt_toolkit when available, plain
190
+ # input() otherwise). Callables let the handler always read the *current*
191
+ # model and session id even after /model or /clear commands.
192
+ _current_session_id_holder = [session_id]
193
+
194
+ input_handler = GemCodeInputHandler(
195
+ ansi_enabled=ansi.enabled,
196
+ get_model=lambda: getattr(cfg, "model", "gemini") or "gemini",
197
+ get_session_id=lambda: _current_session_id_holder[0],
198
+ )
199
+
188
200
  async def typewrite(text: str) -> None:
189
201
  if not text:
190
202
  return
@@ -252,7 +264,7 @@ async def run_gemcode_scrollback_tui(
252
264
 
253
265
  while True:
254
266
  try:
255
- prompt = input(f"{ansi.bold}❯{ansi.reset} ").strip()
267
+ prompt = await input_handler.prompt_async()
256
268
  except EOFError:
257
269
  print("")
258
270
  return
@@ -276,6 +288,7 @@ async def run_gemcode_scrollback_tui(
276
288
  return
277
289
  if slash.new_session_id is not None:
278
290
  current_session_id = slash.new_session_id
291
+ _current_session_id_holder[0] = current_session_id
279
292
  if slash.skip_model_turn:
280
293
  # Runner binds the model at creation time (LlmAgent(model=...)),
281
294
  # so rebuild it when the user overrides the model mid-session.
@@ -400,9 +413,12 @@ async def run_gemcode_scrollback_tui(
400
413
  print(
401
414
  f" ⎿ {ansi.blue}{ansi.bold}Permission needed{ansi.reset} for {ansi.bold}{tool_name}{ansi.reset}."
402
415
  )
403
- ans = input(
404
- f" ⎿ Allow? ({ansi.blue_ok}y{ansi.reset}/{ansi.dim}N{ansi.reset}) "
405
- ).strip().lower()
416
+ try:
417
+ ans = input(
418
+ f" ⎿ Allow? ({ansi.blue_ok}y{ansi.reset}/{ansi.dim}N{ansi.reset}) "
419
+ ).strip().lower()
420
+ except EOFError:
421
+ ans = ""
406
422
  ok = ans in ("y", "yes")
407
423
 
408
424
  parts.append(
@@ -132,4 +132,9 @@ def print_welcome_dashboard(cfg, *, console: Console) -> None:
132
132
  def print_shortcuts_hint(*, console: Console) -> None:
133
133
  from rich.text import Text
134
134
 
135
- console.print(Text(" ? for shortcuts", style=f"dim {_LABEL_GREY}"))
135
+ console.print(
136
+ Text(
137
+ " Type a message to begin · / for commands · ↑↓ history",
138
+ style=f"dim {_LABEL_GREY}",
139
+ )
140
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.16
3
+ Version: 0.3.18
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -179,6 +179,7 @@ Requires-Dist: google-adk>=1.0.0
179
179
  Requires-Dist: google-genai>=1.0.0
180
180
  Requires-Dist: python-dotenv>=1.0.0
181
181
  Requires-Dist: rich>=13.0.0
182
+ Requires-Dist: prompt_toolkit>=3.0.0
182
183
  Provides-Extra: dev
183
184
  Requires-Dist: pytest>=8.0.0; extra == "dev"
184
185
  Requires-Dist: pytest-asyncio>=0.24.0; extra == "dev"
@@ -69,8 +69,11 @@ 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
76
+ src/gemcode/tui/input_handler.py
74
77
  src/gemcode/tui/scrollback.py
75
78
  src/gemcode/tui/spinner.py
76
79
  src/gemcode/tui/welcome_banner.py
@@ -2,6 +2,7 @@ google-adk>=1.0.0
2
2
  google-genai>=1.0.0
3
3
  python-dotenv>=1.0.0
4
4
  rich>=13.0.0
5
+ prompt_toolkit>=3.0.0
5
6
 
6
7
  [dev]
7
8
  pytest>=8.0.0
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