gemcode 0.2.2__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/__init__.py +3 -0
- gemcode/__main__.py +3 -0
- gemcode/agent.py +146 -0
- gemcode/audit.py +16 -0
- gemcode/callbacks.py +473 -0
- gemcode/capability_routing.py +137 -0
- gemcode/cli.py +658 -0
- gemcode/compaction.py +35 -0
- gemcode/computer_use/__init__.py +0 -0
- gemcode/computer_use/browser_computer.py +275 -0
- gemcode/config.py +247 -0
- gemcode/interactions.py +15 -0
- gemcode/invoke.py +151 -0
- gemcode/kairos_daemon.py +221 -0
- gemcode/limits.py +83 -0
- gemcode/live_audio_engine.py +124 -0
- gemcode/mcp_loader.py +57 -0
- gemcode/memory/__init__.py +0 -0
- gemcode/memory/embedding_memory_service.py +292 -0
- gemcode/memory/file_memory_service.py +176 -0
- gemcode/modality_tools.py +216 -0
- gemcode/model_routing.py +179 -0
- gemcode/paths.py +29 -0
- gemcode/permissions.py +5 -0
- gemcode/plugins/__init__.py +0 -0
- gemcode/plugins/terminal_hooks_plugin.py +168 -0
- gemcode/plugins/tool_recovery_plugin.py +135 -0
- gemcode/prompt_suggestions.py +80 -0
- gemcode/query/__init__.py +36 -0
- gemcode/query/config.py +35 -0
- gemcode/query/deps.py +20 -0
- gemcode/query/engine.py +55 -0
- gemcode/query/stop_hooks.py +63 -0
- gemcode/query/token_budget.py +109 -0
- gemcode/query/transitions.py +41 -0
- gemcode/session_runtime.py +81 -0
- gemcode/thinking.py +136 -0
- gemcode/tool_prompt_manifest.py +118 -0
- gemcode/tool_registry.py +50 -0
- gemcode/tools/__init__.py +25 -0
- gemcode/tools/edit.py +53 -0
- gemcode/tools/filesystem.py +73 -0
- gemcode/tools/search.py +85 -0
- gemcode/tools/shell.py +73 -0
- gemcode/tools_inspector.py +132 -0
- gemcode/trust.py +54 -0
- gemcode/tui/app.py +697 -0
- gemcode/tui/scrollback.py +312 -0
- gemcode/vertex.py +22 -0
- gemcode/web/__init__.py +2 -0
- gemcode/web/claude_sse_adapter.py +282 -0
- gemcode/web/terminal_repl.py +147 -0
- gemcode-0.2.2.dist-info/METADATA +440 -0
- gemcode-0.2.2.dist-info/RECORD +58 -0
- gemcode-0.2.2.dist-info/WHEEL +5 -0
- gemcode-0.2.2.dist-info/entry_points.txt +2 -0
- gemcode-0.2.2.dist-info/licenses/LICENSE +151 -0
- gemcode-0.2.2.dist-info/top_level.txt +1 -0
gemcode/cli.py
ADDED
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
"""CLI entry: `gemcode "prompt"`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import uuid
|
|
10
|
+
import warnings
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from gemcode.config import GemCodeConfig, load_dotenv_optional
|
|
14
|
+
from gemcode.tools_inspector import inspect_tools, smoke_tools
|
|
15
|
+
from gemcode.invoke import run_turn
|
|
16
|
+
from gemcode.model_routing import pick_effective_model
|
|
17
|
+
from gemcode.capability_routing import apply_capability_routing
|
|
18
|
+
from gemcode.session_runtime import create_runner
|
|
19
|
+
from gemcode.trust import is_trusted_root, trust_root
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _events_to_text(events) -> str:
|
|
23
|
+
parts: list[str] = []
|
|
24
|
+
for event in events:
|
|
25
|
+
if not event.content or not event.content.parts:
|
|
26
|
+
continue
|
|
27
|
+
for part in event.content.parts:
|
|
28
|
+
if part.text and event.author and event.author != "user":
|
|
29
|
+
parts.append(part.text)
|
|
30
|
+
return "".join(parts)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _maybe_prompt_trust(cfg: GemCodeConfig) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Claude Code-style folder trust prompt.
|
|
36
|
+
|
|
37
|
+
On first run in a new project root, ask the user to trust the folder.
|
|
38
|
+
If not trusted, we exit early so tools/filesystem/shell access never happens.
|
|
39
|
+
"""
|
|
40
|
+
# Non-interactive sessions can't answer prompts.
|
|
41
|
+
if not (hasattr(sys.stdin, "isatty") and sys.stdin.isatty()):
|
|
42
|
+
return
|
|
43
|
+
if os.environ.get("GEMCODE_TRUST_PROMPT", "1").lower() not in ("1", "true", "yes", "on"):
|
|
44
|
+
return
|
|
45
|
+
root = cfg.project_root
|
|
46
|
+
if is_trusted_root(root):
|
|
47
|
+
return
|
|
48
|
+
try:
|
|
49
|
+
print(f"Trust this folder for GemCode access?\n {root}\n[y/N] ", file=sys.stderr, end="")
|
|
50
|
+
ans = input().strip().lower()
|
|
51
|
+
except EOFError:
|
|
52
|
+
raise SystemExit("Folder is not trusted; aborting.")
|
|
53
|
+
if ans in ("y", "yes"):
|
|
54
|
+
trust_root(root, trusted=True)
|
|
55
|
+
return
|
|
56
|
+
raise SystemExit("Folder is not trusted; aborting.")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def _run_prompt(
|
|
60
|
+
cfg: GemCodeConfig, prompt: str, session_id: str, *, use_mcp: bool
|
|
61
|
+
) -> str:
|
|
62
|
+
load_dotenv_optional()
|
|
63
|
+
_maybe_prompt_trust(cfg)
|
|
64
|
+
extra: list = []
|
|
65
|
+
if use_mcp:
|
|
66
|
+
from gemcode.mcp_loader import load_mcp_toolsets
|
|
67
|
+
|
|
68
|
+
extra = load_mcp_toolsets(cfg)
|
|
69
|
+
|
|
70
|
+
runner = create_runner(cfg, extra_tools=extra or None)
|
|
71
|
+
try:
|
|
72
|
+
collected = await run_turn(
|
|
73
|
+
runner,
|
|
74
|
+
user_id="local",
|
|
75
|
+
session_id=session_id,
|
|
76
|
+
prompt=prompt,
|
|
77
|
+
max_llm_calls=cfg.max_llm_calls,
|
|
78
|
+
cfg=cfg,
|
|
79
|
+
)
|
|
80
|
+
return _events_to_text(collected)
|
|
81
|
+
finally:
|
|
82
|
+
# Ensure toolsets with external resources (e.g. Playwright browser) are
|
|
83
|
+
# cleaned up after each CLI invocation.
|
|
84
|
+
await runner.close()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def _run_repl(cfg: GemCodeConfig, session_id: str, *, use_mcp: bool) -> None:
|
|
88
|
+
"""
|
|
89
|
+
Interactive REPL mode (Claude Code-like): keep the session open for multiple turns.
|
|
90
|
+
"""
|
|
91
|
+
load_dotenv_optional()
|
|
92
|
+
_maybe_prompt_trust(cfg)
|
|
93
|
+
extra: list = []
|
|
94
|
+
if use_mcp:
|
|
95
|
+
from gemcode.mcp_loader import load_mcp_toolsets
|
|
96
|
+
|
|
97
|
+
extra = load_mcp_toolsets(cfg)
|
|
98
|
+
|
|
99
|
+
runner = create_runner(cfg, extra_tools=extra or None)
|
|
100
|
+
try:
|
|
101
|
+
# For CLI UX, show concise tool summaries (helps users see what ran).
|
|
102
|
+
if os.environ.get("GEMCODE_EMIT_TOOL_USE_SUMMARIES") is None:
|
|
103
|
+
os.environ["GEMCODE_EMIT_TOOL_USE_SUMMARIES"] = "1"
|
|
104
|
+
|
|
105
|
+
# One-time permission prompt (interactive UX).
|
|
106
|
+
# This maps to the existing flags:
|
|
107
|
+
# - "auto" => --yes (auto-approve mutating tools)
|
|
108
|
+
# - "ask" => --interactive-ask (HITL prompts during runs)
|
|
109
|
+
# - "ro" => read-only (default)
|
|
110
|
+
if os.environ.get("GEMCODE_CLI_PERMISSION_PROMPT", "1").lower() in (
|
|
111
|
+
"1",
|
|
112
|
+
"true",
|
|
113
|
+
"yes",
|
|
114
|
+
"on",
|
|
115
|
+
):
|
|
116
|
+
try:
|
|
117
|
+
if (
|
|
118
|
+
hasattr(sys.stdin, "isatty")
|
|
119
|
+
and sys.stdin.isatty()
|
|
120
|
+
and not cfg.yes_to_all
|
|
121
|
+
and not cfg.interactive_permission_ask
|
|
122
|
+
):
|
|
123
|
+
print(
|
|
124
|
+
"Permission mode: [Enter]=read-only, (a)sk each time, (y)es auto-approve (writes + shell)",
|
|
125
|
+
file=sys.stderr,
|
|
126
|
+
)
|
|
127
|
+
choice = input("perm> ").strip().lower()
|
|
128
|
+
if choice in ("y", "yes"):
|
|
129
|
+
cfg.yes_to_all = True
|
|
130
|
+
elif choice in ("a", "ask"):
|
|
131
|
+
cfg.interactive_permission_ask = True
|
|
132
|
+
except EOFError:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
# Optional TUI. Claude-like default is "scrollback" (no internal scrolling).
|
|
136
|
+
tui_enabled = os.environ.get("GEMCODE_TUI", "1").lower() in ("1", "true", "yes", "on")
|
|
137
|
+
if tui_enabled:
|
|
138
|
+
term = (os.environ.get("TERM") or "").strip().lower()
|
|
139
|
+
# Guardrails: Prompt Toolkit needs a real interactive terminal.
|
|
140
|
+
if not sys.stdin.isatty() or not sys.stdout.isatty() or term in ("", "dumb", "unknown"):
|
|
141
|
+
print(
|
|
142
|
+
f"[tui] disabled (stdin/stdout isatty={sys.stdin.isatty()}/{sys.stdout.isatty()}, TERM={term or '<unset>'}); using plain REPL",
|
|
143
|
+
file=sys.stderr,
|
|
144
|
+
)
|
|
145
|
+
else:
|
|
146
|
+
try:
|
|
147
|
+
style = os.environ.get("GEMCODE_TUI_STYLE", "scrollback").strip().lower()
|
|
148
|
+
if style in ("scrollback", "claude", "claude-like"):
|
|
149
|
+
from gemcode.tui.scrollback import run_gemcode_scrollback_tui
|
|
150
|
+
|
|
151
|
+
await run_gemcode_scrollback_tui(cfg=cfg, runner=runner, session_id=session_id)
|
|
152
|
+
else:
|
|
153
|
+
from gemcode.tui.app import run_gemcode_tui
|
|
154
|
+
|
|
155
|
+
await run_gemcode_tui(cfg=cfg, runner=runner, session_id=session_id)
|
|
156
|
+
return
|
|
157
|
+
except Exception as e:
|
|
158
|
+
# Dependency missing or terminal doesn't support full-screen.
|
|
159
|
+
# Print one line so users know how to fix it.
|
|
160
|
+
print(
|
|
161
|
+
f"[tui] failed to start: {type(e).__name__}: {e} (falling back to plain REPL). "
|
|
162
|
+
"Install extras with: pip install 'gemcode[tui]'",
|
|
163
|
+
file=sys.stderr,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
print(
|
|
167
|
+
"GemCode CLI is running. Type your prompt and press Enter. (Ctrl+D to exit)",
|
|
168
|
+
file=sys.stderr,
|
|
169
|
+
)
|
|
170
|
+
while True:
|
|
171
|
+
try:
|
|
172
|
+
raw = input("> ")
|
|
173
|
+
except EOFError:
|
|
174
|
+
break
|
|
175
|
+
|
|
176
|
+
prompt_text = (raw or "").strip()
|
|
177
|
+
if not prompt_text:
|
|
178
|
+
continue
|
|
179
|
+
if prompt_text in (":q", "quit", "exit", "/exit"):
|
|
180
|
+
break
|
|
181
|
+
|
|
182
|
+
apply_capability_routing(cfg, prompt_text, context="prompt")
|
|
183
|
+
cfg.model = pick_effective_model(cfg, prompt_text)
|
|
184
|
+
collected = await run_turn(
|
|
185
|
+
runner,
|
|
186
|
+
user_id="local",
|
|
187
|
+
session_id=session_id,
|
|
188
|
+
prompt=prompt_text,
|
|
189
|
+
max_llm_calls=cfg.max_llm_calls,
|
|
190
|
+
cfg=cfg,
|
|
191
|
+
)
|
|
192
|
+
out = _events_to_text(collected)
|
|
193
|
+
if out:
|
|
194
|
+
print(out)
|
|
195
|
+
print()
|
|
196
|
+
finally:
|
|
197
|
+
await runner.close()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def main() -> None:
|
|
201
|
+
# Reduce startup noise: hide the experimental ReflectAndRetryToolPlugin warning
|
|
202
|
+
# unless explicitly enabled.
|
|
203
|
+
if os.environ.get("GEMCODE_SHOW_EXPERIMENTAL_WARNINGS", "").lower() not in (
|
|
204
|
+
"1",
|
|
205
|
+
"true",
|
|
206
|
+
"yes",
|
|
207
|
+
"on",
|
|
208
|
+
):
|
|
209
|
+
warnings.filterwarnings(
|
|
210
|
+
"ignore",
|
|
211
|
+
message=r"^\[EXPERIMENTAL\] ReflectAndRetryToolPlugin: .*",
|
|
212
|
+
category=UserWarning,
|
|
213
|
+
)
|
|
214
|
+
# Google SDK warnings are useful for library authors but noisy for CLI users.
|
|
215
|
+
warnings.filterwarnings(
|
|
216
|
+
"ignore",
|
|
217
|
+
message=r"^Interactions usage is experimental.*",
|
|
218
|
+
category=UserWarning,
|
|
219
|
+
)
|
|
220
|
+
warnings.filterwarnings(
|
|
221
|
+
"ignore",
|
|
222
|
+
message=r"^Async interactions client cannot use aiohttp.*",
|
|
223
|
+
category=UserWarning,
|
|
224
|
+
)
|
|
225
|
+
warnings.filterwarnings(
|
|
226
|
+
"ignore",
|
|
227
|
+
message=r"^Warning: there are non-text parts in the response: .*",
|
|
228
|
+
category=UserWarning,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# macOS privacy can block Desktop/Documents access for Terminal.app.
|
|
232
|
+
# Provide a clear error if the current directory is not accessible.
|
|
233
|
+
try:
|
|
234
|
+
Path.cwd().resolve()
|
|
235
|
+
except PermissionError:
|
|
236
|
+
raise SystemExit(
|
|
237
|
+
"PermissionError: terminal cannot access this folder. "
|
|
238
|
+
"On macOS: System Settings → Privacy & Security → Files and Folders, "
|
|
239
|
+
"enable Terminal for Desktop Folder (or grant Full Disk Access)."
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Quick command bypass (no prompt parsing): list available Gemini models.
|
|
243
|
+
if (
|
|
244
|
+
len(sys.argv) > 1
|
|
245
|
+
and sys.argv[1] in ("models", "list-models", "list_models")
|
|
246
|
+
):
|
|
247
|
+
load_dotenv_optional()
|
|
248
|
+
from google.genai import Client
|
|
249
|
+
|
|
250
|
+
api_key = os.environ.get("GOOGLE_API_KEY")
|
|
251
|
+
if not api_key:
|
|
252
|
+
raise SystemExit("GOOGLE_API_KEY is not set. Copy .env.example -> .env and retry.")
|
|
253
|
+
|
|
254
|
+
client = Client(api_key=api_key)
|
|
255
|
+
models = client.models.list()
|
|
256
|
+
show_all = "--show-all" in sys.argv[2:]
|
|
257
|
+
# `models.list()` returns objects; print best-effort fields.
|
|
258
|
+
for m in models:
|
|
259
|
+
name = getattr(m, "name", None)
|
|
260
|
+
actions = getattr(m, "supported_actions", None)
|
|
261
|
+
if not name:
|
|
262
|
+
continue
|
|
263
|
+
if not show_all and actions and isinstance(actions, list):
|
|
264
|
+
# GemCode uses an ADK LlmAgent; it relies on `generateContent`-style models.
|
|
265
|
+
if "generateContent" not in actions:
|
|
266
|
+
continue
|
|
267
|
+
if actions and isinstance(actions, list):
|
|
268
|
+
print(f"{name}\t{','.join(actions)}")
|
|
269
|
+
else:
|
|
270
|
+
print(name)
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
# Tool inventory / smoke test.
|
|
274
|
+
if len(sys.argv) > 1 and sys.argv[1] == "tools":
|
|
275
|
+
tools_parser = argparse.ArgumentParser(prog="gemcode tools")
|
|
276
|
+
tools_parser.add_argument(
|
|
277
|
+
"subcommand",
|
|
278
|
+
choices=("list", "smoke"),
|
|
279
|
+
help="Tool inventory operation",
|
|
280
|
+
)
|
|
281
|
+
tools_parser.add_argument(
|
|
282
|
+
"-C",
|
|
283
|
+
"--directory",
|
|
284
|
+
type=Path,
|
|
285
|
+
default=Path.cwd(),
|
|
286
|
+
help="Project root",
|
|
287
|
+
)
|
|
288
|
+
tools_parser.add_argument(
|
|
289
|
+
"--deep-research",
|
|
290
|
+
action="store_true",
|
|
291
|
+
help="Enable deep research built-in tools for inspection",
|
|
292
|
+
)
|
|
293
|
+
tools_parser.add_argument(
|
|
294
|
+
"--maps-grounding",
|
|
295
|
+
action="store_true",
|
|
296
|
+
help="Opt-in to Google Maps grounding during deep-research inspection",
|
|
297
|
+
)
|
|
298
|
+
tools_parser.add_argument(
|
|
299
|
+
"--embeddings",
|
|
300
|
+
action="store_true",
|
|
301
|
+
help="Enable embeddings semantic retrieval tool for inspection",
|
|
302
|
+
)
|
|
303
|
+
tools_parser.add_argument(
|
|
304
|
+
"--memory",
|
|
305
|
+
action="store_true",
|
|
306
|
+
help="Enable persistent memory ingestion tool preload for inspection",
|
|
307
|
+
)
|
|
308
|
+
args = tools_parser.parse_args(sys.argv[2:])
|
|
309
|
+
|
|
310
|
+
load_dotenv_optional()
|
|
311
|
+
cfg = GemCodeConfig(project_root=args.directory)
|
|
312
|
+
cfg.enable_deep_research = bool(args.deep_research)
|
|
313
|
+
cfg.enable_maps_grounding = bool(args.maps_grounding)
|
|
314
|
+
cfg.enable_embeddings = bool(args.embeddings)
|
|
315
|
+
cfg.enable_memory = bool(args.memory)
|
|
316
|
+
|
|
317
|
+
inspections = inspect_tools(cfg)
|
|
318
|
+
failures = smoke_tools(inspections)
|
|
319
|
+
|
|
320
|
+
if args.subcommand == "list":
|
|
321
|
+
for i in inspections:
|
|
322
|
+
decl = "decl_ok" if i.declaration_present else "no_decl"
|
|
323
|
+
if i.declaration_error:
|
|
324
|
+
decl = "decl_err"
|
|
325
|
+
print(f"{i.name}\t{i.category}\t{i.tool_type}\t{decl}")
|
|
326
|
+
if i.declaration_error:
|
|
327
|
+
print(f" error: {i.declaration_error}")
|
|
328
|
+
return
|
|
329
|
+
|
|
330
|
+
# smoke
|
|
331
|
+
if failures:
|
|
332
|
+
for i in failures:
|
|
333
|
+
print(f"{i.name}\t{ i.category }\tdecl_err")
|
|
334
|
+
if i.declaration_error:
|
|
335
|
+
print(f" error: {i.declaration_error}")
|
|
336
|
+
raise SystemExit(1)
|
|
337
|
+
|
|
338
|
+
print(f"smoke ok: {len(inspections)} tools validated")
|
|
339
|
+
return
|
|
340
|
+
|
|
341
|
+
# Live audio mode (Gemini Live API via ADK run_live()).
|
|
342
|
+
if len(sys.argv) > 1 and sys.argv[1] == "live-audio":
|
|
343
|
+
audio_parser = argparse.ArgumentParser(
|
|
344
|
+
prog="gemcode live-audio",
|
|
345
|
+
description="GemCode live audio (mic -> Gemini Live)",
|
|
346
|
+
)
|
|
347
|
+
audio_parser.add_argument(
|
|
348
|
+
"-C",
|
|
349
|
+
"--directory",
|
|
350
|
+
type=Path,
|
|
351
|
+
default=Path.cwd(),
|
|
352
|
+
help="Project root",
|
|
353
|
+
)
|
|
354
|
+
audio_parser.add_argument(
|
|
355
|
+
"--session",
|
|
356
|
+
default=None,
|
|
357
|
+
help="Session id for SQLite-backed history (optional)",
|
|
358
|
+
)
|
|
359
|
+
audio_parser.add_argument(
|
|
360
|
+
"--seconds",
|
|
361
|
+
type=int,
|
|
362
|
+
default=10,
|
|
363
|
+
help="Record mic for N seconds before sending audio",
|
|
364
|
+
)
|
|
365
|
+
audio_parser.add_argument(
|
|
366
|
+
"--rate",
|
|
367
|
+
type=int,
|
|
368
|
+
default=24000,
|
|
369
|
+
help="Input PCM sample rate (Hz)",
|
|
370
|
+
)
|
|
371
|
+
audio_parser.add_argument(
|
|
372
|
+
"--language",
|
|
373
|
+
default=None,
|
|
374
|
+
help="Optional BCP-47 language code (e.g. en-US)",
|
|
375
|
+
)
|
|
376
|
+
audio_parser.add_argument(
|
|
377
|
+
"--yes",
|
|
378
|
+
action="store_true",
|
|
379
|
+
help="Allow write_file / search_replace",
|
|
380
|
+
)
|
|
381
|
+
audio_parser.add_argument(
|
|
382
|
+
"--model",
|
|
383
|
+
default=None,
|
|
384
|
+
help="Override GEMCODE_MODEL (must support AUDIO live streaming)",
|
|
385
|
+
)
|
|
386
|
+
audio_parser.add_argument(
|
|
387
|
+
"--deep-research",
|
|
388
|
+
action="store_true",
|
|
389
|
+
help="Enable deep research tools + routing",
|
|
390
|
+
)
|
|
391
|
+
audio_parser.add_argument(
|
|
392
|
+
"--embeddings",
|
|
393
|
+
action="store_true",
|
|
394
|
+
help="Enable embeddings-based semantic retrieval",
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
args = audio_parser.parse_args(sys.argv[2:])
|
|
398
|
+
load_dotenv_optional()
|
|
399
|
+
|
|
400
|
+
cfg = GemCodeConfig(project_root=args.directory)
|
|
401
|
+
cfg.yes_to_all = args.yes
|
|
402
|
+
cfg.enable_deep_research = bool(args.deep_research)
|
|
403
|
+
cfg.enable_embeddings = bool(args.embeddings)
|
|
404
|
+
if args.model:
|
|
405
|
+
cfg.model = args.model
|
|
406
|
+
else:
|
|
407
|
+
cfg.model = cfg.model_audio_live
|
|
408
|
+
|
|
409
|
+
session_id = args.session or str(uuid.uuid4())
|
|
410
|
+
from gemcode.live_audio_engine import run_live_audio
|
|
411
|
+
|
|
412
|
+
asyncio.run(
|
|
413
|
+
run_live_audio(
|
|
414
|
+
cfg,
|
|
415
|
+
session_id=session_id,
|
|
416
|
+
seconds=args.seconds,
|
|
417
|
+
input_rate=args.rate,
|
|
418
|
+
language_code=args.language,
|
|
419
|
+
)
|
|
420
|
+
)
|
|
421
|
+
print(f"\n[gemcode live-audio] session_id={session_id}", file=sys.stderr)
|
|
422
|
+
return
|
|
423
|
+
|
|
424
|
+
# Kairos proactive scheduler daemon.
|
|
425
|
+
if len(sys.argv) > 1 and sys.argv[1] == "kairos":
|
|
426
|
+
kairos_parser = argparse.ArgumentParser(
|
|
427
|
+
prog="gemcode kairos",
|
|
428
|
+
description="Kairos-like proactive scheduler daemon (stdin -> queued jobs).",
|
|
429
|
+
)
|
|
430
|
+
kairos_parser.add_argument(
|
|
431
|
+
"-C",
|
|
432
|
+
"--directory",
|
|
433
|
+
type=Path,
|
|
434
|
+
default=Path.cwd(),
|
|
435
|
+
help="Project root",
|
|
436
|
+
)
|
|
437
|
+
kairos_parser.add_argument(
|
|
438
|
+
"--session",
|
|
439
|
+
default=None,
|
|
440
|
+
help="Session id for SQLite-backed history (optional; defaults to a new uuid).",
|
|
441
|
+
)
|
|
442
|
+
kairos_parser.add_argument(
|
|
443
|
+
"--concurrency",
|
|
444
|
+
type=int,
|
|
445
|
+
default=2,
|
|
446
|
+
help="Max number of concurrent queued jobs.",
|
|
447
|
+
)
|
|
448
|
+
kairos_parser.add_argument(
|
|
449
|
+
"--default-priority",
|
|
450
|
+
type=int,
|
|
451
|
+
default=0,
|
|
452
|
+
help="Priority used for stdin-enqueued jobs.",
|
|
453
|
+
)
|
|
454
|
+
kairos_parser.add_argument(
|
|
455
|
+
"--yes",
|
|
456
|
+
action="store_true",
|
|
457
|
+
help="Allow write_file / search_replace (disables interactive HITL prompts).",
|
|
458
|
+
)
|
|
459
|
+
kairos_parser.add_argument(
|
|
460
|
+
"--interactive-ask",
|
|
461
|
+
action="store_true",
|
|
462
|
+
help="Prompt in-run for mutating tool confirmations (HITL).",
|
|
463
|
+
)
|
|
464
|
+
kairos_parser.add_argument("--model", default=None, help="Override GEMCODE_MODEL")
|
|
465
|
+
kairos_parser.add_argument(
|
|
466
|
+
"--model-mode",
|
|
467
|
+
default=None,
|
|
468
|
+
help="Model mode: auto|fast|balanced|quality (overrides GEMCODE_MODEL_MODE).",
|
|
469
|
+
)
|
|
470
|
+
kairos_parser.add_argument(
|
|
471
|
+
"--deep-research",
|
|
472
|
+
action="store_true",
|
|
473
|
+
help="Enable deep research tools + routing.",
|
|
474
|
+
)
|
|
475
|
+
kairos_parser.add_argument(
|
|
476
|
+
"--maps-grounding",
|
|
477
|
+
action="store_true",
|
|
478
|
+
help="Opt-in to Google Maps grounding tool inside deep-research.",
|
|
479
|
+
)
|
|
480
|
+
kairos_parser.add_argument(
|
|
481
|
+
"--embeddings",
|
|
482
|
+
action="store_true",
|
|
483
|
+
help="Enable embeddings-based semantic retrieval.",
|
|
484
|
+
)
|
|
485
|
+
kairos_parser.add_argument(
|
|
486
|
+
"--capability-mode",
|
|
487
|
+
default=None,
|
|
488
|
+
help="Capability routing: auto|research|embeddings|computer|audio|all (enables tools and routes models).",
|
|
489
|
+
)
|
|
490
|
+
kairos_parser.add_argument(
|
|
491
|
+
"--tool-combination-mode",
|
|
492
|
+
default=None,
|
|
493
|
+
help="Gemini 3 tool context circulation: deep_research|always|never|auto",
|
|
494
|
+
)
|
|
495
|
+
kairos_parser.add_argument(
|
|
496
|
+
"--max-llm-calls",
|
|
497
|
+
type=int,
|
|
498
|
+
default=None,
|
|
499
|
+
metavar="N",
|
|
500
|
+
help="Cap model↔tool iterations for each job message (ADK RunConfig.max_llm_calls).",
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
args = kairos_parser.parse_args(sys.argv[2:])
|
|
504
|
+
load_dotenv_optional()
|
|
505
|
+
|
|
506
|
+
cfg = GemCodeConfig(project_root=args.directory)
|
|
507
|
+
if args.model:
|
|
508
|
+
cfg.model_overridden = True
|
|
509
|
+
cfg.model = args.model
|
|
510
|
+
cfg.model_family_mode = "primary"
|
|
511
|
+
if args.model_mode is None:
|
|
512
|
+
cfg.model_mode = "fast"
|
|
513
|
+
|
|
514
|
+
cfg.yes_to_all = bool(args.yes)
|
|
515
|
+
if args.interactive_ask:
|
|
516
|
+
cfg.interactive_permission_ask = True
|
|
517
|
+
else:
|
|
518
|
+
if "GEMCODE_INTERACTIVE_PERMISSION_ASK" not in os.environ:
|
|
519
|
+
cfg.interactive_permission_ask = bool(sys.stdin.isatty() and not cfg.yes_to_all)
|
|
520
|
+
|
|
521
|
+
cfg.enable_deep_research = bool(args.deep_research)
|
|
522
|
+
cfg.enable_maps_grounding = bool(args.maps_grounding)
|
|
523
|
+
cfg.enable_embeddings = bool(args.embeddings)
|
|
524
|
+
|
|
525
|
+
if args.capability_mode is not None:
|
|
526
|
+
cfg.capability_mode = args.capability_mode
|
|
527
|
+
if args.tool_combination_mode is not None:
|
|
528
|
+
cfg.tool_combination_mode = args.tool_combination_mode
|
|
529
|
+
if args.model_mode is not None:
|
|
530
|
+
cfg.model_mode = args.model_mode
|
|
531
|
+
if args.max_llm_calls is not None:
|
|
532
|
+
cfg.max_llm_calls = args.max_llm_calls
|
|
533
|
+
|
|
534
|
+
session_id = args.session or str(uuid.uuid4())
|
|
535
|
+
from gemcode.kairos_daemon import KairosDaemon
|
|
536
|
+
|
|
537
|
+
daemon = KairosDaemon(
|
|
538
|
+
cfg=cfg,
|
|
539
|
+
concurrency=args.concurrency,
|
|
540
|
+
default_priority=args.default_priority,
|
|
541
|
+
)
|
|
542
|
+
asyncio.run(daemon.run_forever(session_id=session_id))
|
|
543
|
+
print(f"\n[gemcode kairos] session_id={session_id}", file=sys.stderr)
|
|
544
|
+
return
|
|
545
|
+
|
|
546
|
+
parser = argparse.ArgumentParser(prog="gemcode", description="Gemini + ADK coding agent")
|
|
547
|
+
parser.add_argument(
|
|
548
|
+
"prompt",
|
|
549
|
+
nargs="?",
|
|
550
|
+
default=None,
|
|
551
|
+
help="Task or question (read from stdin if omitted)",
|
|
552
|
+
)
|
|
553
|
+
parser.add_argument("-C", "--directory", type=Path, default=Path.cwd(), help="Project root")
|
|
554
|
+
parser.add_argument("--session", default=None, help="Session id for SQLite-backed history")
|
|
555
|
+
parser.add_argument("--yes", action="store_true", help="Allow write_file / search_replace")
|
|
556
|
+
parser.add_argument(
|
|
557
|
+
"--interactive-ask",
|
|
558
|
+
action="store_true",
|
|
559
|
+
help="Prompt in-run for mutating tool confirmations (HITL) instead of requiring --yes rerun.",
|
|
560
|
+
)
|
|
561
|
+
parser.add_argument("--model", default=None, help="Override GEMCODE_MODEL")
|
|
562
|
+
parser.add_argument(
|
|
563
|
+
"--model-mode",
|
|
564
|
+
default=None,
|
|
565
|
+
help="Model mode: auto|fast|balanced|quality (overrides GEMCODE_MODEL_MODE)",
|
|
566
|
+
)
|
|
567
|
+
parser.add_argument(
|
|
568
|
+
"--deep-research",
|
|
569
|
+
action="store_true",
|
|
570
|
+
help="Enable deep research tools and route to the deep-research model",
|
|
571
|
+
)
|
|
572
|
+
parser.add_argument(
|
|
573
|
+
"--maps-grounding",
|
|
574
|
+
action="store_true",
|
|
575
|
+
help="Opt-in to Google Maps grounding tool inside deep-research (may be incompatible with other built-in tools depending on model/tooling).",
|
|
576
|
+
)
|
|
577
|
+
parser.add_argument(
|
|
578
|
+
"--embeddings",
|
|
579
|
+
action="store_true",
|
|
580
|
+
help="Enable embeddings-based semantic retrieval (and embedding memory if enabled)",
|
|
581
|
+
)
|
|
582
|
+
parser.add_argument(
|
|
583
|
+
"--capability-mode",
|
|
584
|
+
default=None,
|
|
585
|
+
help="Capability routing: auto|research|embeddings|computer|audio|all (enables tools and routes models)",
|
|
586
|
+
)
|
|
587
|
+
parser.add_argument(
|
|
588
|
+
"--tool-combination-mode",
|
|
589
|
+
default=None,
|
|
590
|
+
help="Gemini 3 tool context circulation: deep_research|always|never|auto",
|
|
591
|
+
)
|
|
592
|
+
parser.add_argument("--mcp", action="store_true", help="Load .gemcode/mcp.json toolsets")
|
|
593
|
+
parser.add_argument(
|
|
594
|
+
"--max-llm-calls",
|
|
595
|
+
type=int,
|
|
596
|
+
default=None,
|
|
597
|
+
metavar="N",
|
|
598
|
+
help="Cap model↔tool iterations for this message (maps to ADK RunConfig.max_llm_calls)",
|
|
599
|
+
)
|
|
600
|
+
args = parser.parse_args()
|
|
601
|
+
|
|
602
|
+
load_dotenv_optional()
|
|
603
|
+
prompt = args.prompt
|
|
604
|
+
interactive_tty = prompt is None and sys.stdin.isatty()
|
|
605
|
+
|
|
606
|
+
cfg = GemCodeConfig(project_root=args.directory)
|
|
607
|
+
if args.model:
|
|
608
|
+
cfg.model_overridden = True
|
|
609
|
+
if args.model:
|
|
610
|
+
cfg.model = args.model
|
|
611
|
+
# User explicitly picked a model id, so treat it as primary.
|
|
612
|
+
cfg.model_family_mode = "primary"
|
|
613
|
+
# If the user explicitly sets a model, default to fast mode unless
|
|
614
|
+
# `--model-mode` is also provided.
|
|
615
|
+
if args.model_mode is None:
|
|
616
|
+
cfg.model_mode = "fast"
|
|
617
|
+
cfg.yes_to_all = args.yes
|
|
618
|
+
if args.interactive_ask:
|
|
619
|
+
cfg.interactive_permission_ask = True
|
|
620
|
+
else:
|
|
621
|
+
# If user didn't explicitly set env, default to HITL when we're in a TTY.
|
|
622
|
+
if "GEMCODE_INTERACTIVE_PERMISSION_ASK" not in os.environ:
|
|
623
|
+
cfg.interactive_permission_ask = bool(sys.stdin.isatty() and not cfg.yes_to_all)
|
|
624
|
+
cfg.enable_deep_research = bool(args.deep_research)
|
|
625
|
+
cfg.enable_maps_grounding = bool(args.maps_grounding)
|
|
626
|
+
cfg.enable_embeddings = bool(args.embeddings)
|
|
627
|
+
if args.capability_mode is not None:
|
|
628
|
+
cfg.capability_mode = args.capability_mode
|
|
629
|
+
if args.tool_combination_mode is not None:
|
|
630
|
+
cfg.tool_combination_mode = args.tool_combination_mode
|
|
631
|
+
if args.model_mode is not None:
|
|
632
|
+
cfg.model_mode = args.model_mode
|
|
633
|
+
if args.max_llm_calls is not None:
|
|
634
|
+
cfg.max_llm_calls = args.max_llm_calls
|
|
635
|
+
|
|
636
|
+
session_id = args.session or str(uuid.uuid4())
|
|
637
|
+
|
|
638
|
+
if interactive_tty:
|
|
639
|
+
asyncio.run(_run_repl(cfg, session_id, use_mcp=args.mcp))
|
|
640
|
+
print(f"\n[gemcode] session_id={session_id}", file=sys.stderr)
|
|
641
|
+
return
|
|
642
|
+
|
|
643
|
+
if prompt is None:
|
|
644
|
+
prompt = sys.stdin.read()
|
|
645
|
+
if not prompt.strip():
|
|
646
|
+
parser.error("Empty prompt")
|
|
647
|
+
|
|
648
|
+
prompt_text = prompt.strip()
|
|
649
|
+
apply_capability_routing(cfg, prompt_text, context="prompt")
|
|
650
|
+
cfg.model = pick_effective_model(cfg, prompt_text)
|
|
651
|
+
out = asyncio.run(_run_prompt(cfg, prompt_text, session_id, use_mcp=args.mcp))
|
|
652
|
+
if out:
|
|
653
|
+
print(out)
|
|
654
|
+
print(f"\n[gemcode] session_id={session_id}", file=sys.stderr)
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
if __name__ == "__main__":
|
|
658
|
+
main()
|
gemcode/compaction.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Optional sliding-window trim before each model call (use with care)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from gemcode.config import GemCodeConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def make_before_model_callback(cfg: GemCodeConfig):
|
|
11
|
+
"""
|
|
12
|
+
Keep the first content block and the last N items.
|
|
13
|
+
|
|
14
|
+
Off by default: set GEMCODE_ENABLE_COMPACT=1. Trimming can break tool-call
|
|
15
|
+
pairing if misconfigured; for production prefer ADK/App compaction or
|
|
16
|
+
summarization.
|
|
17
|
+
"""
|
|
18
|
+
if os.environ.get("GEMCODE_ENABLE_COMPACT", "").lower() not in ("1", "true", "yes"):
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
max_items = cfg.max_content_items
|
|
22
|
+
|
|
23
|
+
async def before_model(callback_context, llm_request):
|
|
24
|
+
contents = llm_request.contents
|
|
25
|
+
if len(contents) <= max_items:
|
|
26
|
+
return None
|
|
27
|
+
keep_first = 1 if contents else 0
|
|
28
|
+
tail = contents[-(max_items - keep_first) :]
|
|
29
|
+
if keep_first:
|
|
30
|
+
llm_request.contents = [contents[0], *tail]
|
|
31
|
+
else:
|
|
32
|
+
llm_request.contents = tail
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
return before_model
|
|
File without changes
|