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.
Files changed (58) hide show
  1. gemcode/__init__.py +3 -0
  2. gemcode/__main__.py +3 -0
  3. gemcode/agent.py +146 -0
  4. gemcode/audit.py +16 -0
  5. gemcode/callbacks.py +473 -0
  6. gemcode/capability_routing.py +137 -0
  7. gemcode/cli.py +658 -0
  8. gemcode/compaction.py +35 -0
  9. gemcode/computer_use/__init__.py +0 -0
  10. gemcode/computer_use/browser_computer.py +275 -0
  11. gemcode/config.py +247 -0
  12. gemcode/interactions.py +15 -0
  13. gemcode/invoke.py +151 -0
  14. gemcode/kairos_daemon.py +221 -0
  15. gemcode/limits.py +83 -0
  16. gemcode/live_audio_engine.py +124 -0
  17. gemcode/mcp_loader.py +57 -0
  18. gemcode/memory/__init__.py +0 -0
  19. gemcode/memory/embedding_memory_service.py +292 -0
  20. gemcode/memory/file_memory_service.py +176 -0
  21. gemcode/modality_tools.py +216 -0
  22. gemcode/model_routing.py +179 -0
  23. gemcode/paths.py +29 -0
  24. gemcode/permissions.py +5 -0
  25. gemcode/plugins/__init__.py +0 -0
  26. gemcode/plugins/terminal_hooks_plugin.py +168 -0
  27. gemcode/plugins/tool_recovery_plugin.py +135 -0
  28. gemcode/prompt_suggestions.py +80 -0
  29. gemcode/query/__init__.py +36 -0
  30. gemcode/query/config.py +35 -0
  31. gemcode/query/deps.py +20 -0
  32. gemcode/query/engine.py +55 -0
  33. gemcode/query/stop_hooks.py +63 -0
  34. gemcode/query/token_budget.py +109 -0
  35. gemcode/query/transitions.py +41 -0
  36. gemcode/session_runtime.py +81 -0
  37. gemcode/thinking.py +136 -0
  38. gemcode/tool_prompt_manifest.py +118 -0
  39. gemcode/tool_registry.py +50 -0
  40. gemcode/tools/__init__.py +25 -0
  41. gemcode/tools/edit.py +53 -0
  42. gemcode/tools/filesystem.py +73 -0
  43. gemcode/tools/search.py +85 -0
  44. gemcode/tools/shell.py +73 -0
  45. gemcode/tools_inspector.py +132 -0
  46. gemcode/trust.py +54 -0
  47. gemcode/tui/app.py +697 -0
  48. gemcode/tui/scrollback.py +312 -0
  49. gemcode/vertex.py +22 -0
  50. gemcode/web/__init__.py +2 -0
  51. gemcode/web/claude_sse_adapter.py +282 -0
  52. gemcode/web/terminal_repl.py +147 -0
  53. gemcode-0.2.2.dist-info/METADATA +440 -0
  54. gemcode-0.2.2.dist-info/RECORD +58 -0
  55. gemcode-0.2.2.dist-info/WHEEL +5 -0
  56. gemcode-0.2.2.dist-info/entry_points.txt +2 -0
  57. gemcode-0.2.2.dist-info/licenses/LICENSE +151 -0
  58. 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