gemcode 0.3.28__py3-none-any.whl → 0.3.30__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gemcode/agent.py +116 -4
- gemcode/callbacks.py +68 -0
- gemcode/config.py +31 -0
- gemcode/hooks.py +194 -0
- gemcode/modality_tools.py +15 -1
- gemcode/pricing.py +76 -0
- gemcode/refine.py +204 -0
- gemcode/repl_commands.py +29 -1
- gemcode/repl_slash.py +85 -2
- gemcode/session_runtime.py +24 -1
- gemcode/tool_prompt_manifest.py +3 -0
- gemcode/tui/input_handler.py +20 -1
- gemcode/tui/scrollback.py +44 -1
- {gemcode-0.3.28.dist-info → gemcode-0.3.30.dist-info}/METADATA +1 -1
- {gemcode-0.3.28.dist-info → gemcode-0.3.30.dist-info}/RECORD +19 -16
- {gemcode-0.3.28.dist-info → gemcode-0.3.30.dist-info}/WHEEL +0 -0
- {gemcode-0.3.28.dist-info → gemcode-0.3.30.dist-info}/entry_points.txt +0 -0
- {gemcode-0.3.28.dist-info → gemcode-0.3.30.dist-info}/licenses/LICENSE +0 -0
- {gemcode-0.3.28.dist-info → gemcode-0.3.30.dist-info}/top_level.txt +0 -0
gemcode/agent.py
CHANGED
|
@@ -66,6 +66,8 @@ def _build_runtime_facts(cfg: GemCodeConfig) -> str:
|
|
|
66
66
|
|
|
67
67
|
# ── Active capabilities ──────────────────────────────────────────────────
|
|
68
68
|
caps: list[str] = []
|
|
69
|
+
if getattr(cfg, "enable_web_search", False) and not getattr(cfg, "enable_deep_research", False):
|
|
70
|
+
caps.append("web_search ON (tool: google_search — standalone search without full deep_research)")
|
|
69
71
|
if getattr(cfg, "enable_deep_research", False):
|
|
70
72
|
dr_extras = " + google_maps_grounding" if getattr(cfg, "enable_maps_grounding", False) else ""
|
|
71
73
|
caps.append(f"deep_research ON (tools: google_search, url_context{dr_extras})")
|
|
@@ -77,8 +79,12 @@ def _build_runtime_facts(cfg: GemCodeConfig) -> str:
|
|
|
77
79
|
caps.append(f"memory ON ({mem_kind}, stored at {mem_path}; ADK preload_memory injects relevant memories before each turn)")
|
|
78
80
|
if getattr(cfg, "enable_computer_use", False):
|
|
79
81
|
caps.append("computer_use ON (tools: navigate, click_at, type_text_at, browser_screenshot, browser_find_element, etc.)")
|
|
82
|
+
if getattr(cfg, "enable_code_executor", False):
|
|
83
|
+
caps.append("code_executor ON — you can write Python code blocks and they will be executed safely via Gemini's built-in sandboxed executor; results appear as code_execution_result events. Use this for math, data processing, quick tests, and anything that would otherwise require a shell command.")
|
|
84
|
+
if getattr(cfg, "enable_artifacts", True):
|
|
85
|
+
caps.append("artifacts ON — use save_artifact(filename, bytes, mime_type) / load_artifact(filename) to store large/binary outputs (screenshots, PDFs, generated files) outside session history. Artifacts are keyed by filename; prefix 'user:' for cross-session persistence.")
|
|
80
86
|
if not caps:
|
|
81
|
-
caps.append("none enabled (use /research on, /embeddings on, /memory on, /computer on to enable)")
|
|
87
|
+
caps.append("none enabled (use /research on, /embeddings on, /memory on, /computer on, /code on to enable)")
|
|
82
88
|
caps_text = "\n".join(f" - {c}" for c in caps)
|
|
83
89
|
|
|
84
90
|
# ── Limits ───────────────────────────────────────────────────────────────
|
|
@@ -147,6 +153,40 @@ Memory is **ON** ({kind}). Stored at: `{mem_path}`
|
|
|
147
153
|
"""
|
|
148
154
|
|
|
149
155
|
|
|
156
|
+
def _build_plan_mode_section() -> str:
|
|
157
|
+
"""Injected when plan_mode=True — instructs agent to write explicit plans first."""
|
|
158
|
+
return """
|
|
159
|
+
## PLAN MODE IS ACTIVE
|
|
160
|
+
|
|
161
|
+
You are currently in **Plan Mode**. Before executing ANY tools that modify files or run shell commands, you MUST:
|
|
162
|
+
|
|
163
|
+
1. **Write a numbered plan** in your response text — list every step you intend to take.
|
|
164
|
+
Example:
|
|
165
|
+
```
|
|
166
|
+
Plan:
|
|
167
|
+
1. Read src/auth/login.ts to understand the current flow
|
|
168
|
+
2. Read src/types/user.ts for the User interface
|
|
169
|
+
3. Add `lastLogin: Date` field to User interface
|
|
170
|
+
4. Update login handler to set lastLogin on successful auth
|
|
171
|
+
5. Run `npm run build` to verify no TypeScript errors
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
2. **Pause after the plan** — do not immediately execute tools. Present the plan and wait for the user to confirm ("go", "proceed", "looks good") before starting.
|
|
175
|
+
|
|
176
|
+
3. **Stick to the plan** — if you discover the plan needs changing mid-execution, note the update before proceeding.
|
|
177
|
+
|
|
178
|
+
4. **Report completion** against the plan — when done, confirm each step as completed.
|
|
179
|
+
|
|
180
|
+
**Why plan mode?**
|
|
181
|
+
- Prevents unintended side effects from premature tool execution
|
|
182
|
+
- Gives you and the user visibility into the full scope before any changes
|
|
183
|
+
- Makes complex multi-file tasks reviewable and reversible
|
|
184
|
+
- Catches scope creep early
|
|
185
|
+
|
|
186
|
+
**To turn off plan mode**, type `/plan off` at the prompt.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
|
|
150
190
|
def _build_computer_use_section(cfg: GemCodeConfig) -> str:
|
|
151
191
|
"""Rich computer use guidance, only injected when enable_computer_use=True."""
|
|
152
192
|
w = getattr(cfg, "_cfg", None)
|
|
@@ -426,6 +466,41 @@ Use `run_subtask` when the work is better done in an isolated context:
|
|
|
426
466
|
|
|
427
467
|
The sub-agent inherits your permission settings and returns its final text as `result`. Treat it as a trusted colleague returning a written summary.
|
|
428
468
|
|
|
469
|
+
## ADK Special Tools (always available when ADK supports them)
|
|
470
|
+
|
|
471
|
+
### `get_user_choice`
|
|
472
|
+
Present the user with a structured multi-option prompt rather than open-ended questions.
|
|
473
|
+
Use when you need the user to pick from 2–6 specific options (e.g. "Which framework would you like?", "Choose migration strategy: A, B, or C").
|
|
474
|
+
This provides a better UX than asking them to type a free-form answer.
|
|
475
|
+
|
|
476
|
+
### `load_artifacts`
|
|
477
|
+
Load binary/large artifacts that were saved in a previous turn or by a sub-agent.
|
|
478
|
+
Artifacts are keyed by filename (e.g. "report.pdf", "screenshot.png", "output.json").
|
|
479
|
+
Use `user:filename` prefix for user-scoped artifacts that persist across sessions.
|
|
480
|
+
After loading, the artifact bytes are available for further processing (display, analysis, transformation).
|
|
481
|
+
|
|
482
|
+
### `exit_loop`
|
|
483
|
+
Signal the surrounding LoopAgent to stop iterating and return the final result.
|
|
484
|
+
Only meaningful when this agent is running inside an ADK LoopAgent pipeline.
|
|
485
|
+
Call this when the task is complete and no further iterations are needed.
|
|
486
|
+
|
|
487
|
+
## Artifacts — storing large outputs
|
|
488
|
+
When `artifacts ON` (see Runtime facts above):
|
|
489
|
+
- **Save** large generated content as artifacts instead of printing them inline:
|
|
490
|
+
- Screenshots from computer_use: save as "screenshot.png" artifact
|
|
491
|
+
- Generated reports/PDFs: save as "report.pdf" artifact
|
|
492
|
+
- Large JSON data: save as "data.json" artifact
|
|
493
|
+
- **Reference** artifacts in instructions via `{artifact.filename?}` template syntax
|
|
494
|
+
- Artifacts are keyed by filename; `user:` prefix = cross-session persistence
|
|
495
|
+
|
|
496
|
+
## Code Executor (sandboxed Python)
|
|
497
|
+
When `code_executor ON` (see Runtime facts above):
|
|
498
|
+
- You can write Python code blocks in your response and the Gemini API executes them safely
|
|
499
|
+
- The result appears as a `code_execution_result` event with stdout and the outcome
|
|
500
|
+
- Best for: math calculations, data transformation, unit testing logic, quick experiments
|
|
501
|
+
- The sandbox does NOT have internet access or filesystem access — use for pure computation
|
|
502
|
+
- For file I/O or shell commands, use the standard tools (`bash`, `write_file`, etc.)
|
|
503
|
+
|
|
429
504
|
## Evaluator-optimizer loop
|
|
430
505
|
For tasks where quality matters:
|
|
431
506
|
1. Complete the task (execute tools, write code, run commands)
|
|
@@ -461,6 +536,9 @@ All file tools use paths **relative to the project root** (where GemCode was sta
|
|
|
461
536
|
if getattr(cfg, "enable_memory", False):
|
|
462
537
|
base = f"{base}\n\n{_build_memory_section(cfg)}"
|
|
463
538
|
|
|
539
|
+
if getattr(cfg, "plan_mode", False):
|
|
540
|
+
base = f"{base}\n\n{_build_plan_mode_section()}"
|
|
541
|
+
|
|
464
542
|
tool_manifest = build_tool_manifest(cfg)
|
|
465
543
|
if tool_manifest:
|
|
466
544
|
base = f"{base}\n\n{tool_manifest}"
|
|
@@ -470,6 +548,17 @@ All file tools use paths **relative to the project root** (where GemCode was sta
|
|
|
470
548
|
return base
|
|
471
549
|
|
|
472
550
|
|
|
551
|
+
def _build_code_executor(cfg: GemCodeConfig):
|
|
552
|
+
"""Return an ADK BuiltInCodeExecutor when enable_code_executor=True, else None."""
|
|
553
|
+
if not getattr(cfg, "enable_code_executor", False):
|
|
554
|
+
return None
|
|
555
|
+
try:
|
|
556
|
+
from google.adk.code_executors import BuiltInCodeExecutor
|
|
557
|
+
return BuiltInCodeExecutor()
|
|
558
|
+
except Exception:
|
|
559
|
+
return None
|
|
560
|
+
|
|
561
|
+
|
|
473
562
|
def build_root_agent(
|
|
474
563
|
cfg: GemCodeConfig,
|
|
475
564
|
extra_tools: list | None = None,
|
|
@@ -488,12 +577,19 @@ def build_root_agent(
|
|
|
488
577
|
if _tools is not None:
|
|
489
578
|
tools = list(_tools)
|
|
490
579
|
else:
|
|
491
|
-
|
|
580
|
+
tools = build_function_tools(cfg)
|
|
492
581
|
if getattr(cfg, "enable_memory", False):
|
|
493
582
|
# ADK preload_memory injects retrieved memories into the next llm_request.
|
|
494
583
|
from google.adk.tools import preload_memory
|
|
495
|
-
|
|
496
584
|
tools = [preload_memory, *tools]
|
|
585
|
+
|
|
586
|
+
# ADK built-in interactive + artifact tools — always available when ADK supports them.
|
|
587
|
+
try:
|
|
588
|
+
from google.adk.tools import get_user_choice, load_artifacts, exit_loop
|
|
589
|
+
tools = [*tools, get_user_choice, load_artifacts, exit_loop]
|
|
590
|
+
except Exception:
|
|
591
|
+
pass
|
|
592
|
+
|
|
497
593
|
if extra_tools:
|
|
498
594
|
tools = [*tools, *extra_tools]
|
|
499
595
|
|
|
@@ -548,15 +644,31 @@ def build_root_agent(
|
|
|
548
644
|
tool_config=tool_cfg,
|
|
549
645
|
)
|
|
550
646
|
|
|
551
|
-
|
|
647
|
+
# global_instruction applies to the entire agent tree (including sub-agents
|
|
648
|
+
# spawned via run_subtask or multi-agent delegation). Keep it short — it's
|
|
649
|
+
# prepended to every agent's effective instruction.
|
|
650
|
+
global_instr = (
|
|
651
|
+
"You are GemCode, an expert software engineering agent powered by Google Gemini. "
|
|
652
|
+
"Act, don't advise. Complete tasks fully and autonomously. "
|
|
653
|
+
"Think before destructive actions. Use read-only tools before shell/write tools."
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
agent_kwargs: dict = dict(
|
|
552
657
|
model=cfg.model,
|
|
553
658
|
name="gemcode",
|
|
554
659
|
instruction=build_instruction(cfg),
|
|
660
|
+
global_instruction=global_instr,
|
|
555
661
|
tools=tools,
|
|
556
662
|
generate_content_config=gen_cfg,
|
|
557
663
|
**cb_kwargs,
|
|
558
664
|
)
|
|
559
665
|
|
|
666
|
+
code_executor = _build_code_executor(cfg)
|
|
667
|
+
if code_executor is not None:
|
|
668
|
+
agent_kwargs["code_executor"] = code_executor
|
|
669
|
+
|
|
670
|
+
return LlmAgent(**agent_kwargs)
|
|
671
|
+
|
|
560
672
|
|
|
561
673
|
def create_runner(cfg: GemCodeConfig, extra_tools: list | None = None):
|
|
562
674
|
"""Backward-compatible: prefer `gemcode.session_runtime.create_runner`."""
|
gemcode/callbacks.py
CHANGED
|
@@ -141,6 +141,22 @@ def make_before_tool_callback(cfg: GemCodeConfig):
|
|
|
141
141
|
record = {"tool": name, "args": _redact_args(name, args)}
|
|
142
142
|
append_audit(cfg.project_root, record)
|
|
143
143
|
|
|
144
|
+
# ── Shell hooks: pre_tool_use ─────────────────────────────────────────
|
|
145
|
+
# If the project has a .gemcode/hooks/pre_tool_use.sh, run it now.
|
|
146
|
+
# Non-zero exit or {"decision":"deny"} stdout will block the tool call.
|
|
147
|
+
try:
|
|
148
|
+
from gemcode.hooks import run_pre_tool_use_hook
|
|
149
|
+
hook_result = run_pre_tool_use_hook(
|
|
150
|
+
cfg.project_root,
|
|
151
|
+
model=getattr(cfg, "model", "") or "",
|
|
152
|
+
tool_name=name,
|
|
153
|
+
args=args or {},
|
|
154
|
+
)
|
|
155
|
+
if hook_result is not None:
|
|
156
|
+
return hook_result
|
|
157
|
+
except Exception:
|
|
158
|
+
pass
|
|
159
|
+
|
|
144
160
|
streak = 0
|
|
145
161
|
if tool_context is not None:
|
|
146
162
|
try:
|
|
@@ -316,6 +332,19 @@ def make_after_tool_callback(cfg: GemCodeConfig):
|
|
|
316
332
|
st[_STATE_FAILURE_KEY] = 0
|
|
317
333
|
else:
|
|
318
334
|
st[_STATE_FAILURE_KEY] = 0
|
|
335
|
+
# ── Shell hooks: post_tool_use ────────────────────────────────────────
|
|
336
|
+
try:
|
|
337
|
+
from gemcode.hooks import run_post_tool_use_hook
|
|
338
|
+
run_post_tool_use_hook(
|
|
339
|
+
cfg.project_root,
|
|
340
|
+
model=getattr(cfg, "model", "") or "",
|
|
341
|
+
tool_name=name,
|
|
342
|
+
args=args or {},
|
|
343
|
+
result=tool_response if isinstance(tool_response, dict) else {},
|
|
344
|
+
)
|
|
345
|
+
except Exception:
|
|
346
|
+
pass
|
|
347
|
+
|
|
319
348
|
if _maybe_tool_summary_enabled():
|
|
320
349
|
summary: dict[str, Any] = {
|
|
321
350
|
"phase": "tool_result",
|
|
@@ -399,6 +428,7 @@ def make_after_model_callback(cfg: GemCodeConfig):
|
|
|
399
428
|
"candidates_token_count",
|
|
400
429
|
"cached_content_token_count",
|
|
401
430
|
"total_token_count",
|
|
431
|
+
"thoughts_token_count",
|
|
402
432
|
):
|
|
403
433
|
if hasattr(um, attr):
|
|
404
434
|
v = getattr(um, attr)
|
|
@@ -407,6 +437,44 @@ def make_after_model_callback(cfg: GemCodeConfig):
|
|
|
407
437
|
if d:
|
|
408
438
|
append_audit(cfg.project_root, {"phase": "model_usage", **d})
|
|
409
439
|
|
|
440
|
+
# ── Expose live token stats to the TUI ───────────────────────────────────
|
|
441
|
+
# The TUI reads cfg._last_turn_stats after each turn to display token counts
|
|
442
|
+
# and estimated cost in the footer (like OpenClaude's spinner token display).
|
|
443
|
+
try:
|
|
444
|
+
in_tok = d.get("prompt_token_count", 0) or 0
|
|
445
|
+
out_tok = d.get("candidates_token_count", 0) or 0
|
|
446
|
+
think_tok = d.get("thoughts_token_count", 0) or 0
|
|
447
|
+
cache_tok = d.get("cached_content_token_count", 0) or 0
|
|
448
|
+
total_tok = d.get("total_token_count", 0) or 0
|
|
449
|
+
|
|
450
|
+
prev_session_tokens = int(st.get(SESSION_TOTAL_TOKENS_KEY, 0) or 0)
|
|
451
|
+
session_total = prev_session_tokens + total_tok
|
|
452
|
+
|
|
453
|
+
from gemcode.pricing import estimate_cost
|
|
454
|
+
turn_cost = estimate_cost(
|
|
455
|
+
getattr(cfg, "model", "") or "",
|
|
456
|
+
input_tokens=in_tok,
|
|
457
|
+
output_tokens=out_tok,
|
|
458
|
+
)
|
|
459
|
+
# Accumulate session cost
|
|
460
|
+
prev_cost = getattr(cfg, "_session_cost_usd", 0.0) or 0.0
|
|
461
|
+
session_cost = prev_cost + (turn_cost or 0.0)
|
|
462
|
+
object.__setattr__(cfg, "_session_cost_usd", session_cost)
|
|
463
|
+
|
|
464
|
+
stats: dict[str, Any] = {
|
|
465
|
+
"in": in_tok,
|
|
466
|
+
"out": out_tok,
|
|
467
|
+
"think": think_tok,
|
|
468
|
+
"cache": cache_tok,
|
|
469
|
+
"total": total_tok,
|
|
470
|
+
"session_total": session_total,
|
|
471
|
+
"turn_cost": turn_cost,
|
|
472
|
+
"session_cost": session_cost,
|
|
473
|
+
}
|
|
474
|
+
object.__setattr__(cfg, "_last_turn_stats", stats)
|
|
475
|
+
except Exception:
|
|
476
|
+
pass
|
|
477
|
+
|
|
410
478
|
pt = d.get("prompt_token_count")
|
|
411
479
|
if isinstance(pt, int) and pt >= 0:
|
|
412
480
|
try:
|
gemcode/config.py
CHANGED
|
@@ -251,6 +251,37 @@ class GemCodeConfig:
|
|
|
251
251
|
default_factory=lambda: _truthy_env("GEMCODE_SHOW_FULL_THINKING", default=False)
|
|
252
252
|
)
|
|
253
253
|
|
|
254
|
+
# Enable ADK BuiltInCodeExecutor for safe sandboxed Python execution via
|
|
255
|
+
# Gemini's code execution API. When on, the agent can write and run Python
|
|
256
|
+
# snippets inline (math, data processing, quick tests) without requiring
|
|
257
|
+
# bash/shell permissions. Requires a Gemini model that supports code execution
|
|
258
|
+
# (gemini-2.5-flash, gemini-2.5-pro, gemini-3.x).
|
|
259
|
+
enable_code_executor: bool = field(
|
|
260
|
+
default_factory=lambda: _truthy_env("GEMCODE_ENABLE_CODE_EXECUTOR", default=False)
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Enable ADK artifact service for storing large/binary outputs (screenshots,
|
|
264
|
+
# generated files, reports) outside of session history.
|
|
265
|
+
# When on, the agent can save_artifact / load_artifact keyed by filename.
|
|
266
|
+
enable_artifacts: bool = field(
|
|
267
|
+
default_factory=lambda: _truthy_env("GEMCODE_ENABLE_ARTIFACTS", default=True)
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Plan mode: when ON, the agent explicitly writes out a numbered plan
|
|
271
|
+
# BEFORE executing any tools, then checks the plan before reporting done.
|
|
272
|
+
# Like OpenClaude's EnterPlanMode — great for complex, multi-file tasks.
|
|
273
|
+
# Toggle at runtime with /plan on|off.
|
|
274
|
+
plan_mode: bool = field(
|
|
275
|
+
default_factory=lambda: _truthy_env("GEMCODE_PLAN_MODE", default=False)
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Always-on web search (independent of deep_research / research mode).
|
|
279
|
+
# When True, google_search is available as a basic tool without enabling
|
|
280
|
+
# the full deep_research capability suite (url_context, maps, etc.).
|
|
281
|
+
enable_web_search: bool = field(
|
|
282
|
+
default_factory=lambda: _truthy_env("GEMCODE_ENABLE_WEB_SEARCH", default=False)
|
|
283
|
+
)
|
|
284
|
+
|
|
254
285
|
def __post_init__(self) -> None:
|
|
255
286
|
self.project_root = self.project_root.resolve()
|
|
256
287
|
# Default agentic depth when env omits GEMCODE_MAX_LLM_CALLS (was: None → SDK default).
|
gemcode/hooks.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GemCode shell hooks — scriptable lifecycle events inspired by OpenClaude.
|
|
3
|
+
|
|
4
|
+
Hooks are shell scripts stored under ``<project_root>/.gemcode/hooks/``.
|
|
5
|
+
They receive a JSON payload on stdin and can:
|
|
6
|
+
- pre_tool_use.sh: return non-zero exit to DENY the tool call, or
|
|
7
|
+
print JSON {"decision":"deny","reason":"..."} to stderr.
|
|
8
|
+
- post_tool_use.sh: informational; return value ignored.
|
|
9
|
+
- session_start.sh: runs when a new GemCode session starts.
|
|
10
|
+
- session_stop.sh: runs when the session ends.
|
|
11
|
+
|
|
12
|
+
HOOK ENVIRONMENT
|
|
13
|
+
All hooks receive these env vars:
|
|
14
|
+
GEMCODE_HOOK_TYPE — "pre_tool_use" | "post_tool_use" | "session_start" | "session_stop"
|
|
15
|
+
GEMCODE_PROJECT_ROOT — absolute path to the project root
|
|
16
|
+
GEMCODE_MODEL — active model id
|
|
17
|
+
|
|
18
|
+
HOOK STDIN (pre_tool_use, post_tool_use)
|
|
19
|
+
JSON object with:
|
|
20
|
+
{ "tool": "<tool_name>",
|
|
21
|
+
"args": { ...tool_args... },
|
|
22
|
+
"type": "pre_tool_use" | "post_tool_use",
|
|
23
|
+
"result": { ...tool_result... } // post only
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
PRE_TOOL_USE DECISION
|
|
27
|
+
If exit code is non-zero → the tool is DENIED.
|
|
28
|
+
If exit code is 0 → the tool proceeds normally.
|
|
29
|
+
If stdout starts with '{' and contains "decision": "deny" → the tool is denied.
|
|
30
|
+
|
|
31
|
+
EXAMPLE HOOKS
|
|
32
|
+
# .gemcode/hooks/pre_tool_use.sh
|
|
33
|
+
#!/bin/bash
|
|
34
|
+
# Deny all delete_file calls
|
|
35
|
+
HOOK=$(cat)
|
|
36
|
+
TOOL=$(echo "$HOOK" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['tool'])")
|
|
37
|
+
if [ "$TOOL" = "delete_file" ]; then
|
|
38
|
+
echo "delete_file blocked by hook" >&2
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# .gemcode/hooks/session_start.sh
|
|
43
|
+
#!/bin/bash
|
|
44
|
+
echo "[gemcode hook] session started in $GEMCODE_PROJECT_ROOT" >> /tmp/gemcode-audit.log
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
49
|
+
import json
|
|
50
|
+
import logging
|
|
51
|
+
import os
|
|
52
|
+
import subprocess
|
|
53
|
+
from pathlib import Path
|
|
54
|
+
from typing import Any
|
|
55
|
+
|
|
56
|
+
log = logging.getLogger(__name__)
|
|
57
|
+
|
|
58
|
+
_HOOKS_DIR = ".gemcode/hooks"
|
|
59
|
+
_TIMEOUT_PRE = 5.0 # seconds — pre_tool_use must respond quickly
|
|
60
|
+
_TIMEOUT_POST = 10.0 # seconds — post_tool_use has more budget
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _find_hook(project_root: Path, name: str) -> Path | None:
|
|
64
|
+
"""Return the hook file path if it exists and is executable, else None."""
|
|
65
|
+
hooks_dir = project_root / _HOOKS_DIR
|
|
66
|
+
for ext in ("", ".sh", ".py", ".bash"):
|
|
67
|
+
candidate = hooks_dir / f"{name}{ext}"
|
|
68
|
+
if candidate.is_file() and os.access(candidate, os.X_OK):
|
|
69
|
+
return candidate
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _run_hook(
|
|
74
|
+
hook_path: Path,
|
|
75
|
+
project_root: Path,
|
|
76
|
+
model: str,
|
|
77
|
+
hook_type: str,
|
|
78
|
+
stdin_data: dict | None = None,
|
|
79
|
+
timeout: float = 5.0,
|
|
80
|
+
) -> tuple[int, str, str]:
|
|
81
|
+
"""
|
|
82
|
+
Run a hook script.
|
|
83
|
+
|
|
84
|
+
Returns (exit_code, stdout, stderr).
|
|
85
|
+
Never raises — all exceptions are caught and logged.
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
env = {
|
|
89
|
+
**os.environ,
|
|
90
|
+
"GEMCODE_HOOK_TYPE": hook_type,
|
|
91
|
+
"GEMCODE_PROJECT_ROOT": str(project_root),
|
|
92
|
+
"GEMCODE_MODEL": model or "",
|
|
93
|
+
}
|
|
94
|
+
stdin_bytes = (
|
|
95
|
+
json.dumps(stdin_data, ensure_ascii=False).encode() if stdin_data else b""
|
|
96
|
+
)
|
|
97
|
+
result = subprocess.run(
|
|
98
|
+
[str(hook_path)],
|
|
99
|
+
input=stdin_bytes,
|
|
100
|
+
capture_output=True,
|
|
101
|
+
timeout=timeout,
|
|
102
|
+
env=env,
|
|
103
|
+
cwd=str(project_root),
|
|
104
|
+
)
|
|
105
|
+
return result.returncode, result.stdout.decode(errors="replace"), result.stderr.decode(errors="replace")
|
|
106
|
+
except subprocess.TimeoutExpired:
|
|
107
|
+
log.warning("[hooks] %s timed out after %.1fs", hook_path.name, timeout)
|
|
108
|
+
return 1, "", f"hook timed out after {timeout}s"
|
|
109
|
+
except Exception as exc:
|
|
110
|
+
log.debug("[hooks] failed to run %s: %s", hook_path.name, exc)
|
|
111
|
+
return 1, "", str(exc)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def run_pre_tool_use_hook(
|
|
115
|
+
project_root: Path,
|
|
116
|
+
model: str,
|
|
117
|
+
tool_name: str,
|
|
118
|
+
args: dict[str, Any],
|
|
119
|
+
) -> dict | None:
|
|
120
|
+
"""
|
|
121
|
+
Run the pre_tool_use hook (if it exists).
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
None → tool is allowed (default)
|
|
125
|
+
dict → {"error": "...", "error_kind": "hook_denied"} to deny the tool
|
|
126
|
+
"""
|
|
127
|
+
hook = _find_hook(project_root, "pre_tool_use")
|
|
128
|
+
if hook is None:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
payload = {"tool": tool_name, "args": args, "type": "pre_tool_use"}
|
|
132
|
+
rc, stdout, stderr = _run_hook(
|
|
133
|
+
hook, project_root, model, "pre_tool_use", payload, timeout=_TIMEOUT_PRE
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if rc != 0:
|
|
137
|
+
reason = stderr.strip() or stdout.strip() or f"hook exited {rc}"
|
|
138
|
+
log.info("[hooks] pre_tool_use denied %s: %s", tool_name, reason[:200])
|
|
139
|
+
return {
|
|
140
|
+
"error": f"Tool denied by pre_tool_use hook: {reason[:400]}",
|
|
141
|
+
"error_kind": "hook_denied",
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# Check if stdout contains an explicit deny decision.
|
|
145
|
+
if stdout.strip().startswith("{"):
|
|
146
|
+
try:
|
|
147
|
+
decision = json.loads(stdout.strip())
|
|
148
|
+
if str(decision.get("decision", "")).lower() == "deny":
|
|
149
|
+
reason = decision.get("reason", "hook denied")
|
|
150
|
+
log.info("[hooks] pre_tool_use JSON-denied %s: %s", tool_name, reason)
|
|
151
|
+
return {
|
|
152
|
+
"error": f"Tool denied by hook: {reason}",
|
|
153
|
+
"error_kind": "hook_denied",
|
|
154
|
+
}
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
return None # allowed
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def run_post_tool_use_hook(
|
|
162
|
+
project_root: Path,
|
|
163
|
+
model: str,
|
|
164
|
+
tool_name: str,
|
|
165
|
+
args: dict[str, Any],
|
|
166
|
+
result: dict[str, Any],
|
|
167
|
+
) -> None:
|
|
168
|
+
"""Run the post_tool_use hook if it exists. Return value is ignored."""
|
|
169
|
+
hook = _find_hook(project_root, "post_tool_use")
|
|
170
|
+
if hook is None:
|
|
171
|
+
return
|
|
172
|
+
payload = {
|
|
173
|
+
"tool": tool_name,
|
|
174
|
+
"args": args,
|
|
175
|
+
"result": result,
|
|
176
|
+
"type": "post_tool_use",
|
|
177
|
+
}
|
|
178
|
+
_run_hook(hook, project_root, model, "post_tool_use", payload, timeout=_TIMEOUT_POST)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def run_session_start_hook(project_root: Path, model: str) -> None:
|
|
182
|
+
"""Run the session_start hook if it exists."""
|
|
183
|
+
hook = _find_hook(project_root, "session_start")
|
|
184
|
+
if hook is None:
|
|
185
|
+
return
|
|
186
|
+
_run_hook(hook, project_root, model, "session_start", timeout=10.0)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def run_session_stop_hook(project_root: Path, model: str) -> None:
|
|
190
|
+
"""Run the session_stop hook if it exists."""
|
|
191
|
+
hook = _find_hook(project_root, "session_stop")
|
|
192
|
+
if hook is None:
|
|
193
|
+
return
|
|
194
|
+
_run_hook(hook, project_root, model, "session_stop", timeout=10.0)
|
gemcode/modality_tools.py
CHANGED
|
@@ -172,9 +172,23 @@ def build_extra_tools(cfg: GemCodeConfig) -> list[Any]:
|
|
|
172
172
|
"""Return ADK tool unions to expose for enabled modalities."""
|
|
173
173
|
extra: list[Any] = []
|
|
174
174
|
|
|
175
|
+
# ── Web search (standalone, no full deep_research needed) ────────────────
|
|
176
|
+
# enable_web_search=True adds google_search alone.
|
|
177
|
+
# enable_deep_research=True adds google_search + url_context + optional maps.
|
|
178
|
+
# If both are on, avoid adding google_search twice.
|
|
179
|
+
web_search_added = False
|
|
180
|
+
if getattr(cfg, "enable_web_search", False) and not getattr(cfg, "enable_deep_research", False):
|
|
181
|
+
try:
|
|
182
|
+
from google.adk.tools import google_search
|
|
183
|
+
extra.append(google_search)
|
|
184
|
+
web_search_added = True
|
|
185
|
+
except Exception:
|
|
186
|
+
pass
|
|
187
|
+
|
|
175
188
|
if getattr(cfg, "enable_deep_research", False):
|
|
176
189
|
from google.adk.tools import google_search, url_context
|
|
177
|
-
|
|
190
|
+
if not web_search_added:
|
|
191
|
+
extra.append(google_search)
|
|
178
192
|
extra.append(url_context)
|
|
179
193
|
# Google Maps grounding can be incompatible with other built-in tools
|
|
180
194
|
# (e.g., google_search) depending on the request/model tooling layer.
|
gemcode/pricing.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Per-model token pricing for cost estimation.
|
|
3
|
+
|
|
4
|
+
All prices are USD per 1 million tokens (input / output separately).
|
|
5
|
+
Sources: Google AI Studio / Vertex AI pricing pages as of 2026.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from gemcode.pricing import estimate_cost
|
|
9
|
+
usd = estimate_cost("gemini-2.5-flash", input_tokens=5000, output_tokens=1200)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
# Format: model_prefix → (input_$/M, output_$/M)
|
|
15
|
+
# Matches are done by checking if the model id STARTS WITH any key (longest first).
|
|
16
|
+
_PRICING_TABLE: dict[str, tuple[float, float]] = {
|
|
17
|
+
# ── Gemini 2.5 ──────────────────────────────────────────────────────────
|
|
18
|
+
"gemini-2.5-pro": (3.50, 10.50), # standard context ≤200k
|
|
19
|
+
"gemini-2.5-flash": (0.15, 0.60), # standard context
|
|
20
|
+
"gemini-2.5-flash-8b": (0.037, 0.15), # 8B lite variant
|
|
21
|
+
# ── Gemini 2.0 ──────────────────────────────────────────────────────────
|
|
22
|
+
"gemini-2.0-flash": (0.10, 0.40),
|
|
23
|
+
"gemini-2.0-flash-lite": (0.075, 0.30),
|
|
24
|
+
"gemini-2.0-pro": (3.50, 10.50),
|
|
25
|
+
# ── Gemini 1.5 ──────────────────────────────────────────────────────────
|
|
26
|
+
"gemini-1.5-flash": (0.075, 0.30),
|
|
27
|
+
"gemini-1.5-pro": (1.25, 5.00),
|
|
28
|
+
# ── Gemini experimental / preview ───────────────────────────────────────
|
|
29
|
+
"gemini-exp": (0.00, 0.00), # free during preview
|
|
30
|
+
"gemini-3": (0.30, 1.20), # approximate future pricing
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def estimate_cost(
|
|
35
|
+
model: str,
|
|
36
|
+
*,
|
|
37
|
+
input_tokens: int = 0,
|
|
38
|
+
output_tokens: int = 0,
|
|
39
|
+
) -> float | None:
|
|
40
|
+
"""
|
|
41
|
+
Return estimated USD cost for a model turn, or None if pricing is unknown.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
model: Model id string (e.g. "gemini-2.5-flash").
|
|
45
|
+
input_tokens: Prompt/input token count.
|
|
46
|
+
output_tokens: Candidates/output token count.
|
|
47
|
+
"""
|
|
48
|
+
if not model:
|
|
49
|
+
return None
|
|
50
|
+
model_lower = model.lower().strip()
|
|
51
|
+
# Match longest prefix first for specificity.
|
|
52
|
+
for prefix in sorted(_PRICING_TABLE, key=len, reverse=True):
|
|
53
|
+
if model_lower.startswith(prefix):
|
|
54
|
+
in_rate, out_rate = _PRICING_TABLE[prefix]
|
|
55
|
+
return (input_tokens * in_rate + output_tokens * out_rate) / 1_000_000
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def format_cost(usd: float | None) -> str:
|
|
60
|
+
"""Human-readable cost string, e.g. '$0.0012' or '$1.23'."""
|
|
61
|
+
if usd is None:
|
|
62
|
+
return ""
|
|
63
|
+
if usd < 0.0001:
|
|
64
|
+
return f"<$0.0001"
|
|
65
|
+
if usd < 0.01:
|
|
66
|
+
return f"${usd:.4f}"
|
|
67
|
+
return f"${usd:.3f}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def format_tokens(n: int) -> str:
|
|
71
|
+
"""Format a token count compactly: 1234 → '1.2k', 12345678 → '12.3M'."""
|
|
72
|
+
if n >= 1_000_000:
|
|
73
|
+
return f"{n / 1_000_000:.1f}M"
|
|
74
|
+
if n >= 1_000:
|
|
75
|
+
return f"{n / 1_000:.1f}k"
|
|
76
|
+
return str(n)
|
gemcode/refine.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Iterative refinement pipelines using ADK LoopAgent + SequentialAgent.
|
|
3
|
+
|
|
4
|
+
This module exposes factory functions for building multi-agent orchestration
|
|
5
|
+
patterns that GemCode can invoke for complex, multi-step tasks:
|
|
6
|
+
|
|
7
|
+
- build_refine_loop(): LoopAgent that loops write→test→fix until tests pass
|
|
8
|
+
or max_iterations is reached (escalates via exit_loop tool).
|
|
9
|
+
|
|
10
|
+
- build_sequential_pipeline(): SequentialAgent that runs N specialist agents
|
|
11
|
+
in order, passing results through session state via output_key.
|
|
12
|
+
|
|
13
|
+
- build_parallel_research(): ParallelAgent that fans out N research sub-agents
|
|
14
|
+
then collects their outputs.
|
|
15
|
+
|
|
16
|
+
These are "heavy" patterns; they are NOT wired by default — callers instantiate
|
|
17
|
+
them on demand (e.g. via /refine slash command or run_subtask).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
log = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# Guard: check what's available in the installed ADK version
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
try:
|
|
31
|
+
from google.adk.agents import LoopAgent, SequentialAgent, ParallelAgent, LlmAgent # type: ignore
|
|
32
|
+
_ADK_WORKFLOW_AGENTS_OK = True
|
|
33
|
+
except ImportError:
|
|
34
|
+
_ADK_WORKFLOW_AGENTS_OK = False
|
|
35
|
+
log.warning("google.adk workflow agents (LoopAgent/SequentialAgent/ParallelAgent) "
|
|
36
|
+
"not available — refine pipelines disabled.")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _require_workflow() -> None:
|
|
40
|
+
if not _ADK_WORKFLOW_AGENTS_OK:
|
|
41
|
+
raise RuntimeError(
|
|
42
|
+
"ADK workflow agents (LoopAgent / SequentialAgent / ParallelAgent) are not "
|
|
43
|
+
"available in the installed google-adk version. "
|
|
44
|
+
"Try: pip install -U google-adk"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Write → Test → Fix Loop
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
def build_refine_loop(
|
|
53
|
+
*,
|
|
54
|
+
model: str,
|
|
55
|
+
task_description: str,
|
|
56
|
+
test_command: str,
|
|
57
|
+
max_iterations: int = 8,
|
|
58
|
+
extra_tools: list | None = None,
|
|
59
|
+
) -> Any:
|
|
60
|
+
"""
|
|
61
|
+
Build a LoopAgent that iteratively writes code, runs tests, and fixes
|
|
62
|
+
failures until all tests pass or ``max_iterations`` is exhausted.
|
|
63
|
+
|
|
64
|
+
Architecture:
|
|
65
|
+
LoopAgent(max_iterations=N)
|
|
66
|
+
├─ writer_agent — writes / edits code based on current task + test output
|
|
67
|
+
├─ tester_agent — runs the test command via bash, stores result in state
|
|
68
|
+
└─ checker_agent — inspects test result; calls exit_loop if tests pass
|
|
69
|
+
|
|
70
|
+
State keys used:
|
|
71
|
+
"task" — original task description (seeded by caller)
|
|
72
|
+
"test_command" — shell command to run tests
|
|
73
|
+
"test_output" — stdout/stderr from last test run
|
|
74
|
+
"test_passed" — "yes" if tests passed
|
|
75
|
+
"iteration" — current loop count (auto-incremented by writer)
|
|
76
|
+
|
|
77
|
+
Returns the LoopAgent instance, ready to run via a Runner.
|
|
78
|
+
|
|
79
|
+
Example usage:
|
|
80
|
+
loop = build_refine_loop(model="gemini-2.5-flash",
|
|
81
|
+
task_description="add unit tests for parser.py",
|
|
82
|
+
test_command="pytest tests/test_parser.py -x")
|
|
83
|
+
runner = Runner(agent=loop, app_name="refine", session_service=...)
|
|
84
|
+
async for event in runner.run_async(...):
|
|
85
|
+
...
|
|
86
|
+
"""
|
|
87
|
+
_require_workflow()
|
|
88
|
+
from google.adk.tools import exit_loop # type: ignore
|
|
89
|
+
|
|
90
|
+
from gemcode.tools import build_function_tools # type: ignore
|
|
91
|
+
|
|
92
|
+
# Shared tools that the sub-agents may use (no run_subtask to avoid recursion)
|
|
93
|
+
tools = extra_tools or []
|
|
94
|
+
|
|
95
|
+
writer = LlmAgent(
|
|
96
|
+
model=model,
|
|
97
|
+
name="writer_agent",
|
|
98
|
+
description="Writes or edits source code to fulfil the task, using test failure output as feedback.",
|
|
99
|
+
instruction=(
|
|
100
|
+
"You are a coding agent. Your goal: {task}.\n\n"
|
|
101
|
+
"Test command: {test_command}\n"
|
|
102
|
+
"Last test output:\n{test_output?}\n\n"
|
|
103
|
+
"Write or fix code to make the tests pass. "
|
|
104
|
+
"After making your edits, update state key 'iteration' by incrementing it."
|
|
105
|
+
),
|
|
106
|
+
tools=tools,
|
|
107
|
+
output_key="writer_notes",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
tester = LlmAgent(
|
|
111
|
+
model=model,
|
|
112
|
+
name="tester_agent",
|
|
113
|
+
description="Runs the test suite and stores the output in session state.",
|
|
114
|
+
instruction=(
|
|
115
|
+
"Run the test command stored in state: {test_command}.\n"
|
|
116
|
+
"Use the bash tool to execute it. Store ALL stdout+stderr verbatim in "
|
|
117
|
+
"state key 'test_output'. "
|
|
118
|
+
"If the command exits with code 0 (success), set state key 'test_passed' to 'yes'. "
|
|
119
|
+
"If it fails, set 'test_passed' to 'no'."
|
|
120
|
+
),
|
|
121
|
+
tools=tools,
|
|
122
|
+
output_key="test_output",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
checker = LlmAgent(
|
|
126
|
+
model=model,
|
|
127
|
+
name="checker_agent",
|
|
128
|
+
description=(
|
|
129
|
+
"Checks whether tests passed. If yes, calls exit_loop to terminate the "
|
|
130
|
+
"refinement cycle. If no, does nothing (loop continues)."
|
|
131
|
+
),
|
|
132
|
+
instruction=(
|
|
133
|
+
"Check the value of state key 'test_passed'.\n"
|
|
134
|
+
"If 'test_passed' is 'yes': call the exit_loop tool to stop the loop — "
|
|
135
|
+
"the task is complete.\n"
|
|
136
|
+
"If 'test_passed' is 'no': output a brief summary of the failures so the "
|
|
137
|
+
"writer knows what to fix next. Do NOT call exit_loop."
|
|
138
|
+
),
|
|
139
|
+
tools=[exit_loop, *tools],
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return LoopAgent(
|
|
143
|
+
name="refine_loop",
|
|
144
|
+
description=(
|
|
145
|
+
f"Iterative write→test→fix loop for: {task_description[:120]}"
|
|
146
|
+
),
|
|
147
|
+
sub_agents=[writer, tester, checker],
|
|
148
|
+
max_iterations=max_iterations,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
# Sequential Pipeline
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
def build_sequential_pipeline(
|
|
157
|
+
agents: list[Any],
|
|
158
|
+
*,
|
|
159
|
+
name: str = "sequential_pipeline",
|
|
160
|
+
description: str = "Runs specialist agents sequentially, passing results via state.",
|
|
161
|
+
) -> Any:
|
|
162
|
+
"""
|
|
163
|
+
Wrap a list of LlmAgents in a SequentialAgent.
|
|
164
|
+
|
|
165
|
+
Each agent should set ``output_key`` so subsequent agents can read its output
|
|
166
|
+
via {key} template substitution in their instructions.
|
|
167
|
+
|
|
168
|
+
Example:
|
|
169
|
+
pipeline = build_sequential_pipeline([
|
|
170
|
+
planner_agent, # output_key="plan"
|
|
171
|
+
coder_agent, # reads {plan}, output_key="code"
|
|
172
|
+
reviewer_agent, # reads {code}, output_key="review"
|
|
173
|
+
])
|
|
174
|
+
"""
|
|
175
|
+
_require_workflow()
|
|
176
|
+
return SequentialAgent(name=name, description=description, sub_agents=agents)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
# Parallel Fan-Out
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
def build_parallel_research(
|
|
184
|
+
agents: list[Any],
|
|
185
|
+
*,
|
|
186
|
+
name: str = "parallel_research",
|
|
187
|
+
description: str = "Runs research agents in parallel, collecting results via state.",
|
|
188
|
+
) -> Any:
|
|
189
|
+
"""
|
|
190
|
+
Wrap a list of LlmAgents in a ParallelAgent.
|
|
191
|
+
|
|
192
|
+
Each agent MUST use a distinct ``output_key`` to avoid state key collisions.
|
|
193
|
+
|
|
194
|
+
Example:
|
|
195
|
+
research = build_parallel_research([
|
|
196
|
+
web_researcher, # output_key="web_results"
|
|
197
|
+
code_searcher, # output_key="code_results"
|
|
198
|
+
doc_reader, # output_key="doc_results"
|
|
199
|
+
])
|
|
200
|
+
# Wrap in a SequentialAgent to collect after:
|
|
201
|
+
pipeline = build_sequential_pipeline([research, synthesis_agent])
|
|
202
|
+
"""
|
|
203
|
+
_require_workflow()
|
|
204
|
+
return ParallelAgent(name=name, description=description, sub_agents=agents)
|
gemcode/repl_commands.py
CHANGED
|
@@ -101,7 +101,8 @@ def format_memory_lines(cfg: GemCodeConfig) -> list[str]:
|
|
|
101
101
|
|
|
102
102
|
def format_hooks_lines(cfg: GemCodeConfig) -> list[str]:
|
|
103
103
|
env_hook = os.environ.get("GEMCODE_POST_TURN_HOOK")
|
|
104
|
-
|
|
104
|
+
hooks_dir = cfg.project_root / ".gemcode" / "hooks"
|
|
105
|
+
default_hook = hooks_dir / "post_turn"
|
|
105
106
|
active: str | None = env_hook
|
|
106
107
|
if not active and _is_executable(default_hook):
|
|
107
108
|
active = str(default_hook)
|
|
@@ -115,6 +116,31 @@ def format_hooks_lines(cfg: GemCodeConfig) -> list[str]:
|
|
|
115
116
|
lines.append(f" active_hook: {active}")
|
|
116
117
|
else:
|
|
117
118
|
lines.append(" active_hook: (none — set env or chmod +x .gemcode/hooks/post_turn)")
|
|
119
|
+
|
|
120
|
+
# ── Shell lifecycle hooks (like OpenClaude PreToolUse / PostToolUse) ──────
|
|
121
|
+
lines.append("")
|
|
122
|
+
lines.append("Shell lifecycle hooks (.gemcode/hooks/):")
|
|
123
|
+
lines.append(" Scripts run at tool/session lifecycle points.")
|
|
124
|
+
lifecycle_hooks = [
|
|
125
|
+
("pre_tool_use", "Run BEFORE each tool call. Non-zero exit → tool denied."),
|
|
126
|
+
("post_tool_use", "Run AFTER each tool call. Informational; return ignored."),
|
|
127
|
+
("session_start", "Run when a GemCode session starts."),
|
|
128
|
+
("session_stop", "Run when a GemCode session ends."),
|
|
129
|
+
]
|
|
130
|
+
for hook_name, description in lifecycle_hooks:
|
|
131
|
+
found = None
|
|
132
|
+
for ext in ("", ".sh", ".py", ".bash"):
|
|
133
|
+
p = hooks_dir / f"{hook_name}{ext}"
|
|
134
|
+
if p.is_file() and _is_executable(p):
|
|
135
|
+
found = p
|
|
136
|
+
break
|
|
137
|
+
status = f"✓ {found}" if found else "✗ not found (create and chmod +x to enable)"
|
|
138
|
+
lines.append(f" {hook_name:20s} {status}")
|
|
139
|
+
lines.append(f" {'':20s} {description}")
|
|
140
|
+
lines.append("")
|
|
141
|
+
lines.append(f" Hook directory: {hooks_dir}")
|
|
142
|
+
lines.append(" Hooks receive JSON on stdin with tool name, args, and result.")
|
|
143
|
+
lines.append(" Set GEMCODE_HOOK_TYPE env var is set for each hook.")
|
|
118
144
|
return lines
|
|
119
145
|
|
|
120
146
|
|
|
@@ -216,4 +242,6 @@ def slash_help_lines() -> list[str]:
|
|
|
216
242
|
" /permissions Show permission / HITL settings",
|
|
217
243
|
" /hooks Show post-turn hook configuration",
|
|
218
244
|
" /kairos How to launch the background parallel job scheduler",
|
|
245
|
+
" /code [on|off] Toggle sandboxed Python executor (ADK BuiltInCodeExecutor)",
|
|
246
|
+
" /plan [on|off] Toggle plan mode — agent plans before executing tools",
|
|
219
247
|
]
|
gemcode/repl_slash.py
CHANGED
|
@@ -357,6 +357,75 @@ async def process_repl_slash(
|
|
|
357
357
|
out()
|
|
358
358
|
return ReplSlashResult(skip_model_turn=True)
|
|
359
359
|
|
|
360
|
+
# ── /plan ─────────────────────────────────────────────────────────────────
|
|
361
|
+
if name == "plan":
|
|
362
|
+
args_s = (sc.args or "").strip().lower()
|
|
363
|
+
if args_s in ("on", "enable", "1", "true"):
|
|
364
|
+
cfg.plan_mode = True
|
|
365
|
+
out("Plan mode: ON")
|
|
366
|
+
out("The agent will now write an explicit numbered plan BEFORE executing")
|
|
367
|
+
out("any tools. It will pause for your confirmation before proceeding.")
|
|
368
|
+
out()
|
|
369
|
+
out("Type /plan off to disable.")
|
|
370
|
+
return ReplSlashResult(skip_model_turn=True, force_rebuild_runner=True)
|
|
371
|
+
elif args_s in ("off", "disable", "0", "false"):
|
|
372
|
+
cfg.plan_mode = False
|
|
373
|
+
out("Plan mode: OFF")
|
|
374
|
+
return ReplSlashResult(skip_model_turn=True, force_rebuild_runner=True)
|
|
375
|
+
else:
|
|
376
|
+
status = "ON" if getattr(cfg, "plan_mode", False) else "OFF"
|
|
377
|
+
out(f"Plan mode: {status}")
|
|
378
|
+
out()
|
|
379
|
+
out("When ON, the agent writes a numbered plan and waits for your")
|
|
380
|
+
out("confirmation before executing any file or shell operations.")
|
|
381
|
+
out()
|
|
382
|
+
out("Best for: complex multi-file refactors, migrations, risky changes.")
|
|
383
|
+
out("Toggle: /plan on /plan off")
|
|
384
|
+
return ReplSlashResult(skip_model_turn=True)
|
|
385
|
+
|
|
386
|
+
# ── /code ─────────────────────────────────────────────────────────────────
|
|
387
|
+
if name == "code":
|
|
388
|
+
args_s = (sc.args or "").strip().lower()
|
|
389
|
+
if args_s in ("on", "enable", "1", "true"):
|
|
390
|
+
cfg.enable_code_executor = True
|
|
391
|
+
out("Code executor: ON (sandboxed Python via Gemini built-in executor)")
|
|
392
|
+
out("The agent can now write Python code blocks and execute them safely,")
|
|
393
|
+
out("without requiring bash/shell permission. Best for: math, data, quick tests.")
|
|
394
|
+
out()
|
|
395
|
+
out("Note: the sandbox has no internet/filesystem access — use bash for I/O.")
|
|
396
|
+
return ReplSlashResult(skip_model_turn=True, force_rebuild_runner=True)
|
|
397
|
+
elif args_s in ("off", "disable", "0", "false"):
|
|
398
|
+
cfg.enable_code_executor = False
|
|
399
|
+
out("Code executor: OFF")
|
|
400
|
+
return ReplSlashResult(skip_model_turn=True, force_rebuild_runner=True)
|
|
401
|
+
else:
|
|
402
|
+
# Status
|
|
403
|
+
status = "ON" if getattr(cfg, "enable_code_executor", False) else "OFF"
|
|
404
|
+
out(f"Code executor: {status}")
|
|
405
|
+
out()
|
|
406
|
+
out("ADK BuiltInCodeExecutor — safe sandboxed Python execution via Gemini API.")
|
|
407
|
+
out()
|
|
408
|
+
out("What it does:")
|
|
409
|
+
out(" When ON, the agent can write Python code blocks directly in its response")
|
|
410
|
+
out(" and the Gemini API executes them in a sandboxed environment. The output")
|
|
411
|
+
out(" (stdout, result) is sent back so the agent can use it for further reasoning.")
|
|
412
|
+
out()
|
|
413
|
+
out("Best for:")
|
|
414
|
+
out(" - Math and numerical calculations")
|
|
415
|
+
out(" - Data processing and transformations")
|
|
416
|
+
out(" - Quick tests of logic or algorithms")
|
|
417
|
+
out(" - Anything that doesn't need filesystem or network access")
|
|
418
|
+
out()
|
|
419
|
+
out("Not for:")
|
|
420
|
+
out(" - Shell commands (use bash)")
|
|
421
|
+
out(" - Reading/writing files (use read_file / write_file)")
|
|
422
|
+
out(" - Internet requests (use web_fetch)")
|
|
423
|
+
out()
|
|
424
|
+
out("Toggle: /code on /code off")
|
|
425
|
+
out()
|
|
426
|
+
out("Supported models: gemini-2.5-flash, gemini-2.5-pro, gemini-3.x and newer.")
|
|
427
|
+
return ReplSlashResult(skip_model_turn=True)
|
|
428
|
+
|
|
360
429
|
# ── /computer ────────────────────────────────────────────────────────────
|
|
361
430
|
if name in ("computer", "browser"):
|
|
362
431
|
args_s = (sc.args or "").strip().lower()
|
|
@@ -560,19 +629,23 @@ async def process_repl_slash(
|
|
|
560
629
|
# ── /caps ─────────────────────────────────────────────────────────────────
|
|
561
630
|
if name in ("caps", "capabilities", "capability"):
|
|
562
631
|
args_s = (sc.args or "").strip().lower()
|
|
563
|
-
valid_caps = ("auto", "research", "embeddings", "computer", "all", "none", "reset")
|
|
632
|
+
valid_caps = ("auto", "research", "embeddings", "computer", "search", "all", "none", "reset")
|
|
564
633
|
out("Active capabilities:")
|
|
634
|
+
out(f" web_search: {'on' if getattr(cfg, 'enable_web_search', False) else 'off'}")
|
|
565
635
|
out(f" deep_research: {'on' if cfg.enable_deep_research else 'off'}")
|
|
566
636
|
out(f" embeddings: {'on' if cfg.enable_embeddings else 'off'}")
|
|
567
637
|
out(f" memory: {'on' if cfg.enable_memory else 'off'}")
|
|
568
638
|
out(f" computer_use: {'on' if cfg.enable_computer_use else 'off'}")
|
|
569
639
|
out(f" maps_grounding: {'on' if cfg.enable_maps_grounding else 'off'}")
|
|
640
|
+
out(f" code_executor: {'on' if getattr(cfg, 'enable_code_executor', False) else 'off'}")
|
|
641
|
+
out(f" plan_mode: {'on' if getattr(cfg, 'plan_mode', False) else 'off'}")
|
|
570
642
|
out(f" capability_mode (auto-routing): {cfg.capability_mode}")
|
|
571
643
|
out()
|
|
572
644
|
if not args_s:
|
|
573
645
|
out("Commands:")
|
|
574
646
|
out(" /caps none — turn all off, capability_mode=auto")
|
|
575
|
-
out(" /caps
|
|
647
|
+
out(" /caps search — enable_web_search on (standalone google_search)")
|
|
648
|
+
out(" /caps research — enable_deep_research on (search + url_context)")
|
|
576
649
|
out(" /caps embeddings — enable_embeddings on")
|
|
577
650
|
out(" /caps all — all modalities on")
|
|
578
651
|
out(" /caps reset — reset to startup defaults (all off, auto mode)")
|
|
@@ -583,10 +656,18 @@ async def process_repl_slash(
|
|
|
583
656
|
cfg.enable_embeddings = False
|
|
584
657
|
cfg.enable_computer_use = False
|
|
585
658
|
cfg.enable_maps_grounding = False
|
|
659
|
+
if hasattr(cfg, "enable_web_search"):
|
|
660
|
+
cfg.enable_web_search = False
|
|
586
661
|
cfg.capability_mode = "auto"
|
|
587
662
|
out("capabilities: reset to defaults (all off, auto mode)")
|
|
588
663
|
out()
|
|
589
664
|
return ReplSlashResult(skip_model_turn=True, force_rebuild_runner=True)
|
|
665
|
+
if args_s == "search":
|
|
666
|
+
if hasattr(cfg, "enable_web_search"):
|
|
667
|
+
cfg.enable_web_search = True
|
|
668
|
+
out("enable_web_search: on (google_search available without full deep_research)")
|
|
669
|
+
out()
|
|
670
|
+
return ReplSlashResult(skip_model_turn=True, force_rebuild_runner=True)
|
|
590
671
|
if args_s == "research":
|
|
591
672
|
cfg.enable_deep_research = True
|
|
592
673
|
out("enable_deep_research: on (runner rebuilding…)")
|
|
@@ -607,6 +688,8 @@ async def process_repl_slash(
|
|
|
607
688
|
cfg.enable_deep_research = True
|
|
608
689
|
cfg.enable_embeddings = True
|
|
609
690
|
cfg.enable_computer_use = True
|
|
691
|
+
if hasattr(cfg, "enable_web_search"):
|
|
692
|
+
cfg.enable_web_search = True
|
|
610
693
|
out("capabilities: all on (runner rebuilding…)")
|
|
611
694
|
out()
|
|
612
695
|
return ReplSlashResult(skip_model_turn=True, force_rebuild_runner=True)
|
gemcode/session_runtime.py
CHANGED
|
@@ -30,6 +30,23 @@ def session_db_path(cfg: GemCodeConfig) -> Path:
|
|
|
30
30
|
return cfg.project_root / ".gemcode" / "sessions.sqlite"
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
def _build_artifact_service(cfg: GemCodeConfig):
|
|
34
|
+
"""
|
|
35
|
+
Return an ADK ArtifactService for this session, or None if disabled.
|
|
36
|
+
|
|
37
|
+
Uses InMemoryArtifactService so artifacts are available within the session
|
|
38
|
+
without requiring GCS credentials. The agent can save screenshots, generated
|
|
39
|
+
files, large reports, etc. as artifacts to avoid bloating session history.
|
|
40
|
+
"""
|
|
41
|
+
if not getattr(cfg, "enable_artifacts", True):
|
|
42
|
+
return None
|
|
43
|
+
try:
|
|
44
|
+
from google.adk.artifacts import InMemoryArtifactService
|
|
45
|
+
return InMemoryArtifactService()
|
|
46
|
+
except Exception:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
33
50
|
def create_runner(cfg: GemCodeConfig, extra_tools: list | None = None) -> Runner:
|
|
34
51
|
"""Construct Runner + SQLite session service + root LlmAgent."""
|
|
35
52
|
modality_tools = build_modality_extra_tools(cfg)
|
|
@@ -86,7 +103,9 @@ def create_runner(cfg: GemCodeConfig, extra_tools: list | None = None) -> Runner
|
|
|
86
103
|
else:
|
|
87
104
|
memory_service = FileMemoryService(mem_path)
|
|
88
105
|
|
|
89
|
-
|
|
106
|
+
artifact_service = _build_artifact_service(cfg)
|
|
107
|
+
|
|
108
|
+
runner_kwargs: dict = dict(
|
|
90
109
|
app_name="gemcode",
|
|
91
110
|
agent=agent,
|
|
92
111
|
session_service=session_service,
|
|
@@ -94,3 +113,7 @@ def create_runner(cfg: GemCodeConfig, extra_tools: list | None = None) -> Runner
|
|
|
94
113
|
memory_service=memory_service,
|
|
95
114
|
auto_create_session=True,
|
|
96
115
|
)
|
|
116
|
+
if artifact_service is not None:
|
|
117
|
+
runner_kwargs["artifact_service"] = artifact_service
|
|
118
|
+
|
|
119
|
+
return Runner(**runner_kwargs)
|
gemcode/tool_prompt_manifest.py
CHANGED
|
@@ -126,6 +126,9 @@ def build_tool_manifest(cfg: GemCodeConfig) -> str | None:
|
|
|
126
126
|
- Returns the sub-agent's final text as `result`.
|
|
127
127
|
- Use for: context-heavy exploration (reading 50+ files), parallel investigation of independent subsystems, verification passes after you finish work.
|
|
128
128
|
- Always give the sub-agent enough context to work independently; end the task with "Summarise your findings clearly."
|
|
129
|
+
- **`get_user_choice(question, choices)`** — present the user with a structured list of options (2–6 items). Returns the user's selected option. Use instead of open-ended questions when the set of valid responses is known and bounded (e.g. framework choice, migration strategy, naming convention).
|
|
130
|
+
- **`load_artifacts(filenames)`** — load one or more named artifacts (binary/large files) saved in this or a previous session. Returns the content of each. Use `user:filename` prefix for cross-session artifacts.
|
|
131
|
+
- **`exit_loop()`** — signal the surrounding LoopAgent to stop iterating and emit the final result. Only relevant when running inside a LoopAgent pipeline (e.g. the `/refine` write→test→fix loop).
|
|
129
132
|
|
|
130
133
|
### Read-only tools (no permission needed): {_fmt_list(read_only)}
|
|
131
134
|
Use proactively. Never use bash/run_command just to list or read files — these are instant and require zero approval.
|
gemcode/tui/input_handler.py
CHANGED
|
@@ -56,6 +56,8 @@ SLASH_COMMANDS: list[tuple[str, str]] = [
|
|
|
56
56
|
("budget", "Set per-turn token budget · /budget <N> · /budget off"),
|
|
57
57
|
("limits", "Show/set execution limits (max_llm_calls, context, etc.)"),
|
|
58
58
|
("kairos", "Background parallel job scheduler — how to run gemcode kairos"),
|
|
59
|
+
("code", "Toggle sandboxed Python code executor (ADK BuiltInCodeExecutor)"),
|
|
60
|
+
("plan", "Toggle plan mode — agent writes explicit plan before executing tools"),
|
|
59
61
|
("tools", "List all tools and their permission categories"),
|
|
60
62
|
("config", "Show full active configuration (all fields)"),
|
|
61
63
|
("permissions", "Show current permission mode (default / strict / yes)"),
|
|
@@ -113,10 +115,12 @@ class GemCodeInputHandler:
|
|
|
113
115
|
ansi_enabled: bool = True,
|
|
114
116
|
get_model: Callable[[], str] | None = None,
|
|
115
117
|
get_session_id: Callable[[], str] | None = None,
|
|
118
|
+
get_cfg: Callable[[], object | None] | None = None,
|
|
116
119
|
) -> None:
|
|
117
120
|
self._ansi = ansi_enabled
|
|
118
121
|
self._get_model = get_model or (lambda: "gemini")
|
|
119
122
|
self._get_session_id = get_session_id or (lambda: "")
|
|
123
|
+
self._get_cfg = get_cfg or (lambda: None)
|
|
120
124
|
self._session: "PromptSession | None" = None
|
|
121
125
|
|
|
122
126
|
if _PT_AVAILABLE and sys.stdin.isatty() and sys.stdout.isatty():
|
|
@@ -141,15 +145,30 @@ class GemCodeInputHandler:
|
|
|
141
145
|
|
|
142
146
|
get_model = self._get_model
|
|
143
147
|
get_session_id = self._get_session_id
|
|
148
|
+
get_cfg = self._get_cfg
|
|
144
149
|
|
|
145
150
|
def bottom_toolbar() -> HTML:
|
|
146
151
|
model = get_model() or "gemini"
|
|
147
152
|
sid = get_session_id()
|
|
148
153
|
sid_short = sid[:8] if len(sid) >= 8 else sid
|
|
154
|
+
cfg = get_cfg()
|
|
155
|
+
extras = ""
|
|
156
|
+
if cfg is not None:
|
|
157
|
+
flags = []
|
|
158
|
+
if getattr(cfg, "plan_mode", False):
|
|
159
|
+
flags.append("PLAN")
|
|
160
|
+
if getattr(cfg, "enable_code_executor", False):
|
|
161
|
+
flags.append("CODE")
|
|
162
|
+
if getattr(cfg, "enable_deep_research", False):
|
|
163
|
+
flags.append("RESEARCH")
|
|
164
|
+
if getattr(cfg, "enable_computer_use", False):
|
|
165
|
+
flags.append("BROWSER")
|
|
166
|
+
if flags:
|
|
167
|
+
extras = " · " + " ".join(f"[{f}]" for f in flags)
|
|
149
168
|
return HTML(
|
|
150
169
|
f'<style bg="#0d1f2d" fg="#5fafd7"><b> ◆ {model}</b></style>'
|
|
151
170
|
f'<style bg="#0d1f2d" fg="#3d6080"> · session {sid_short}'
|
|
152
|
-
f' · / for commands</style>'
|
|
171
|
+
f'{extras} · / for commands</style>'
|
|
153
172
|
)
|
|
154
173
|
|
|
155
174
|
kb = KeyBindings()
|
gemcode/tui/scrollback.py
CHANGED
|
@@ -204,6 +204,13 @@ async def run_gemcode_scrollback_tui(
|
|
|
204
204
|
load_cli_environment()
|
|
205
205
|
os.environ["GEMCODE_TUI_ACTIVE"] = "1"
|
|
206
206
|
|
|
207
|
+
# ── Session-start hook ──────────────────────────────────────────────────
|
|
208
|
+
try:
|
|
209
|
+
from gemcode.hooks import run_session_start_hook
|
|
210
|
+
run_session_start_hook(cfg.project_root, model=getattr(cfg, "model", "") or "")
|
|
211
|
+
except Exception:
|
|
212
|
+
pass
|
|
213
|
+
|
|
207
214
|
ansi = _Ansi(
|
|
208
215
|
enabled=(
|
|
209
216
|
sys.stdout.isatty()
|
|
@@ -234,6 +241,7 @@ async def run_gemcode_scrollback_tui(
|
|
|
234
241
|
ansi_enabled=ansi.enabled,
|
|
235
242
|
get_model=lambda: getattr(cfg, "model", "gemini") or "gemini",
|
|
236
243
|
get_session_id=lambda: _current_session_id_holder[0],
|
|
244
|
+
get_cfg=lambda: cfg,
|
|
237
245
|
)
|
|
238
246
|
|
|
239
247
|
async def typewrite(text: str) -> None:
|
|
@@ -437,10 +445,20 @@ async def run_gemcode_scrollback_tui(
|
|
|
437
445
|
prompt = await input_handler.prompt_async()
|
|
438
446
|
except EOFError:
|
|
439
447
|
print("")
|
|
448
|
+
try:
|
|
449
|
+
from gemcode.hooks import run_session_stop_hook
|
|
450
|
+
run_session_stop_hook(cfg.project_root, model=getattr(cfg, "model", "") or "")
|
|
451
|
+
except Exception:
|
|
452
|
+
pass
|
|
440
453
|
return
|
|
441
454
|
if not prompt:
|
|
442
455
|
continue
|
|
443
456
|
if prompt in (":q", "quit", "exit", "/exit"):
|
|
457
|
+
try:
|
|
458
|
+
from gemcode.hooks import run_session_stop_hook
|
|
459
|
+
run_session_stop_hook(cfg.project_root, model=getattr(cfg, "model", "") or "")
|
|
460
|
+
except Exception:
|
|
461
|
+
pass
|
|
444
462
|
return
|
|
445
463
|
|
|
446
464
|
old_model = getattr(cfg, "model", "")
|
|
@@ -707,7 +725,32 @@ async def run_gemcode_scrollback_tui(
|
|
|
707
725
|
else current_session_id
|
|
708
726
|
)
|
|
709
727
|
model = getattr(cfg, "model", "") or ""
|
|
710
|
-
|
|
728
|
+
# ── Token / cost stats from last turn ──────────────────────────────────
|
|
729
|
+
stats = getattr(cfg, "_last_turn_stats", None)
|
|
730
|
+
token_part = ""
|
|
731
|
+
if stats:
|
|
732
|
+
try:
|
|
733
|
+
from gemcode.pricing import format_cost, format_tokens
|
|
734
|
+
in_tok = stats.get("in", 0) or 0
|
|
735
|
+
out_tok = stats.get("out", 0) or 0
|
|
736
|
+
think_tok = stats.get("think", 0) or 0
|
|
737
|
+
session_total = stats.get("session_total", 0) or 0
|
|
738
|
+
turn_cost = stats.get("turn_cost")
|
|
739
|
+
session_cost = stats.get("session_cost")
|
|
740
|
+
parts_t: list[str] = []
|
|
741
|
+
parts_t.append(f"↑{format_tokens(in_tok)} ↓{format_tokens(out_tok)}")
|
|
742
|
+
if think_tok:
|
|
743
|
+
parts_t.append(f"✦{format_tokens(think_tok)}")
|
|
744
|
+
if turn_cost is not None:
|
|
745
|
+
parts_t.append(format_cost(turn_cost))
|
|
746
|
+
if session_total:
|
|
747
|
+
parts_t.append(f"session {format_tokens(session_total)}")
|
|
748
|
+
if session_cost and session_cost > 0.0001:
|
|
749
|
+
parts_t.append(f"total {format_cost(session_cost)}")
|
|
750
|
+
token_part = " · " + " · ".join(parts_t)
|
|
751
|
+
except Exception:
|
|
752
|
+
pass
|
|
753
|
+
print(f"{ansi.dim} · {model} · session {sid}{token_part}{ansi.reset}")
|
|
711
754
|
if os.environ.get("GEMCODE_TUI_TURN_RULE", "1").lower() in (
|
|
712
755
|
"1",
|
|
713
756
|
"true",
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
gemcode/__init__.py,sha256=l0DCRYqK7KM7Fb7u49fqh-5_SlpeIL7r3LjMeJWMgSg,112
|
|
2
2
|
gemcode/__main__.py,sha256=EX2s1hxq2Yvli_-tnBN3w5Qv4bOjsBBbjyISF0pDIQw,37
|
|
3
|
-
gemcode/agent.py,sha256=
|
|
3
|
+
gemcode/agent.py,sha256=DED-H1lYEYb3NpIiwVbcefmzfkl06XM1edM1qhXyUw8,35713
|
|
4
4
|
gemcode/audit.py,sha256=bh9uhXaeh8wqxqoZtz3ZAowd8Ndk1ss-mw9993Vlrgo,469
|
|
5
5
|
gemcode/autocompact.py,sha256=77h5tgFzJ2rjrhlCL2oIc28IHwLbP4Pqlo7cSNgDwiA,6727
|
|
6
|
-
gemcode/callbacks.py,sha256=
|
|
6
|
+
gemcode/callbacks.py,sha256=g_1HgHzD7NRB1DN7eVc4MN5sT9g7ZB2dnu3nreXAz7I,23182
|
|
7
7
|
gemcode/capability_routing.py,sha256=D8tvawmf_MSL94GVXgG9QhDvNaQVGqzA8tUFQ8XlftY,2894
|
|
8
8
|
gemcode/cli.py,sha256=0b9hO86EILJ5xRg8fb1Sfmwh6vYi9Bp6tkOvsxOSIEU,25447
|
|
9
9
|
gemcode/compaction.py,sha256=9YtA_qa23_8dHWVHx7AJwUduuI7jJQtq-m6sT8jgPWI,1186
|
|
10
|
-
gemcode/config.py,sha256=
|
|
10
|
+
gemcode/config.py,sha256=GKbze1nDf1U5O-i6AmKd9FYr92TklJ2Vhi3qdKrhqZw,14081
|
|
11
11
|
gemcode/context_budget.py,sha256=Nhox9vFBtLbb7jtO7cyGW1MxtN7SVjlIeQ7d-cgGyKM,10544
|
|
12
12
|
gemcode/context_warning.py,sha256=Q8mg5Vojj7EglPhsGAVL7vb8ROLuHVPgdzw25yw-Q2c,4263
|
|
13
13
|
gemcode/credentials.py,sha256=04v-rLD8_Ams69FQdof2FwcL3ZgsroGUnMcHNQFuBZo,1296
|
|
14
14
|
gemcode/hitl_session.py,sha256=oNiI7odFJGUcmqPavjKLJOEumZKrgklLvwjjrIG9GPg,281
|
|
15
|
+
gemcode/hooks.py,sha256=FHz175d_18j-4ByZZVdEIagmdOvLHcjDjo7HD2Cikf4,6339
|
|
15
16
|
gemcode/interactions.py,sha256=B0b3QNE_I2i5_HtiebX4ehhjlc4Nbqjf_XbvcTLyJT0,641
|
|
16
17
|
gemcode/invoke.py,sha256=TakGUR8RweW0nKFCQeYFKwge24rVYHhwkO1erMuoMf8,7514
|
|
17
18
|
gemcode/kairos_daemon.py,sha256=giINipslAIhBtdbqA0o4RYwt4fBsZjtkiKDjp4zSEvw,6980
|
|
@@ -19,18 +20,20 @@ gemcode/limits.py,sha256=1u7w3X13Uqw34ZOVYKa62pxfvlyLTY2Dne1kN8SKJlA,2524
|
|
|
19
20
|
gemcode/live_audio_engine.py,sha256=Re1zS-9lcaK2qfU7ydPVQlI6tXxv384O6Bs1dhZ8Puo,3423
|
|
20
21
|
gemcode/logging_config.py,sha256=nw_FyJbXM3StzaAx6b6BLLNvzuxsGg-sdQ3QRw7BWgs,1080
|
|
21
22
|
gemcode/mcp_loader.py,sha256=0QRgYvI71YNAyehO0N9SgK-9jhiocXHVo0J5HQ8MZHo,1344
|
|
22
|
-
gemcode/modality_tools.py,sha256=
|
|
23
|
+
gemcode/modality_tools.py,sha256=Gx-hDXr5Y364jl2oiUvBNWOZj7Z6bUXxmwVHUpc1L44,6875
|
|
23
24
|
gemcode/model_errors.py,sha256=6I2Gl2HbxINMmJOjSdM5rH6rCUDLA4ellGZsQrzy47Q,2948
|
|
24
25
|
gemcode/model_routing.py,sha256=Q42HZtXQa6rao2O2vYMHxohrTgD-wq4t7qGxU4_38Jg,4881
|
|
25
26
|
gemcode/paths.py,sha256=U6cEH9jfIcSc4NO8Ke0jniZSiJTfCIJPvSMue3hR0ZU,768
|
|
26
27
|
gemcode/permissions.py,sha256=9FmwTP_LLflahxS9KXvY3VPSra4e-h3OariOqnWIKDI,177
|
|
28
|
+
gemcode/pricing.py,sha256=pedyJg4i18NeZ9gvw3JObhmuEBaHJMKwQ5a17Q6Y-6s,3176
|
|
27
29
|
gemcode/prompt_suggestions.py,sha256=h-W_9LlfagS91PyoMEjEjsCqoG4XmIh3QBypA59HyGw,2553
|
|
28
|
-
gemcode/
|
|
29
|
-
gemcode/
|
|
30
|
-
gemcode/
|
|
30
|
+
gemcode/refine.py,sha256=BijEZ4Z32wGa9aK_WottyAhZF-j0xEqRg5UpjedNv2A,7653
|
|
31
|
+
gemcode/repl_commands.py,sha256=VMqJVf9L-H3Qschia_VkqS-Sq7s4HNNnKIg7nfRSzC4,9676
|
|
32
|
+
gemcode/repl_slash.py,sha256=2XT01vCx9rUaSI0_zJy-djZodlLBodEzqUh091pqXuc,35853
|
|
33
|
+
gemcode/session_runtime.py,sha256=gXuBU-3kJlZ-_aBTCaCVnEVxz6RD-J7CRTMC1Z7JVSg,4845
|
|
31
34
|
gemcode/slash_commands.py,sha256=Qylzsj1notk0xN_hvd3CR4HD8g-l99UENDMcg1pKeBA,794
|
|
32
35
|
gemcode/thinking.py,sha256=RanBf_x9fKv1o4DNyNXPLfOdn2xT0KybJb65nYgmMEE,4885
|
|
33
|
-
gemcode/tool_prompt_manifest.py,sha256=
|
|
36
|
+
gemcode/tool_prompt_manifest.py,sha256=MS_eSJg2BTp6yv1Ih2p93okPmnK3B2dYAMjnG6yaEVY,8695
|
|
34
37
|
gemcode/tool_registry.py,sha256=ifqxtr2uLwEUwnJLKYLza_tIz-paaZediJa75y9MiyA,1795
|
|
35
38
|
gemcode/tools_inspector.py,sha256=okmu4PDYAQQ7nthDvuzSHmy2zArFTG4ftIPRadzLnxA,4100
|
|
36
39
|
gemcode/trust.py,sha256=fxe57Xg6aL_KU24bQDUtD-rXjsNpaq7g-eQTInZnudE,1336
|
|
@@ -64,17 +67,17 @@ gemcode/tools/subtask.py,sha256=ur_6jnwnNzHSBtW96mB2N2A6zmALpydSV24Nkx0T6UQ,5683
|
|
|
64
67
|
gemcode/tools/think.py,sha256=WrNATR-bi97aLkbSsOFOYYAGxbzihe9AnPDZfw3z5-Q,1704
|
|
65
68
|
gemcode/tools/todo.py,sha256=d9aXiyT04r1RFZIk6qdVif17-_Oc3oi4ymDnsPBRg68,3143
|
|
66
69
|
gemcode/tools/web.py,sha256=ULg1e3inG4FjPSUCYI8dVBzTrcCHINNRo76SIU9qw-A,4489
|
|
67
|
-
gemcode/tui/input_handler.py,sha256=
|
|
68
|
-
gemcode/tui/scrollback.py,sha256=
|
|
70
|
+
gemcode/tui/input_handler.py,sha256=M3TNo7EjcKVHvzIw_WbBPr6VNYVxbsGwwX_5vXWtXOs,9195
|
|
71
|
+
gemcode/tui/scrollback.py,sha256=LrYjmu9g9oatFB22ZzN5CcVEzn2NQfTF3sV3_AAHpiw,26517
|
|
69
72
|
gemcode/tui/spinner.py,sha256=AJrApG5od-Sh40-5uWcNM9RHb5ax7gr-NbgAZmTbIYY,4848
|
|
70
73
|
gemcode/tui/welcome_banner.py,sha256=aocl1lnoyLIM6RN4f65g3i0wRA71RqUlgPrGsXeVLW4,4387
|
|
71
74
|
gemcode/tui/welcome_rich.py,sha256=8FEZzLXrzqly5JWiDgV9ooRV1LNXDk-CXV1a7K6ua-U,4048
|
|
72
75
|
gemcode/web/__init__.py,sha256=EysmUAWs6g-lmMk4VFljKfaHVrEgb_FiIzwQmBdORJc,40
|
|
73
76
|
gemcode/web/claude_sse_adapter.py,sha256=HcNp0Lh4DdBZBLOpstsqa-VzfqAUrRngZ6FSuJ-mIMg,8609
|
|
74
77
|
gemcode/web/terminal_repl.py,sha256=k2irvFGbCY8gDm_pbirR7b_cakaeafcctoTIvnJkVXk,3902
|
|
75
|
-
gemcode-0.3.
|
|
76
|
-
gemcode-0.3.
|
|
77
|
-
gemcode-0.3.
|
|
78
|
-
gemcode-0.3.
|
|
79
|
-
gemcode-0.3.
|
|
80
|
-
gemcode-0.3.
|
|
78
|
+
gemcode-0.3.30.dist-info/licenses/LICENSE,sha256=TD4524qn-W8Z07GTDnag-9jJPFutFZNB0a1WbMHPC54,8388
|
|
79
|
+
gemcode-0.3.30.dist-info/METADATA,sha256=rzb9o3zpt1S4CNHZF3YjcbG-1EazIWiPL7Xi5A5T-wI,23695
|
|
80
|
+
gemcode-0.3.30.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
81
|
+
gemcode-0.3.30.dist-info/entry_points.txt,sha256=cZdLTLDiHbks7OSUCuxCh66dCWeQdpLR8BozoqfEjV4,45
|
|
82
|
+
gemcode-0.3.30.dist-info/top_level.txt,sha256=UYrjULLBY2bcgK6KI6flomJWmsbDXu7n0rvW2SWFrbo,8
|
|
83
|
+
gemcode-0.3.30.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|