gemcode 0.3.28__tar.gz → 0.3.29__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. {gemcode-0.3.28/src/gemcode.egg-info → gemcode-0.3.29}/PKG-INFO +1 -1
  2. {gemcode-0.3.28 → gemcode-0.3.29}/pyproject.toml +1 -1
  3. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/agent.py +77 -4
  4. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/config.py +16 -0
  5. gemcode-0.3.29/src/gemcode/refine.py +204 -0
  6. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/repl_commands.py +1 -0
  7. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/repl_slash.py +43 -0
  8. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/session_runtime.py +24 -1
  9. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/tool_prompt_manifest.py +3 -0
  10. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/tui/input_handler.py +1 -0
  11. {gemcode-0.3.28 → gemcode-0.3.29/src/gemcode.egg-info}/PKG-INFO +1 -1
  12. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode.egg-info/SOURCES.txt +1 -0
  13. {gemcode-0.3.28 → gemcode-0.3.29}/LICENSE +0 -0
  14. {gemcode-0.3.28 → gemcode-0.3.29}/MANIFEST.in +0 -0
  15. {gemcode-0.3.28 → gemcode-0.3.29}/README.md +0 -0
  16. {gemcode-0.3.28 → gemcode-0.3.29}/setup.cfg +0 -0
  17. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/__init__.py +0 -0
  18. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/__main__.py +0 -0
  19. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/audit.py +0 -0
  20. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/autocompact.py +0 -0
  21. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/callbacks.py +0 -0
  22. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/capability_routing.py +0 -0
  23. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/cli.py +0 -0
  24. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/compaction.py +0 -0
  25. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/computer_use/__init__.py +0 -0
  26. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/computer_use/browser_computer.py +0 -0
  27. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/context_budget.py +0 -0
  28. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/context_warning.py +0 -0
  29. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/credentials.py +0 -0
  30. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/hitl_session.py +0 -0
  31. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/interactions.py +0 -0
  32. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/invoke.py +0 -0
  33. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/kairos_daemon.py +0 -0
  34. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/limits.py +0 -0
  35. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/live_audio_engine.py +0 -0
  36. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/logging_config.py +0 -0
  37. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/mcp_loader.py +0 -0
  38. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/memory/__init__.py +0 -0
  39. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/memory/embedding_memory_service.py +0 -0
  40. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/memory/file_memory_service.py +0 -0
  41. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/modality_tools.py +0 -0
  42. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/model_errors.py +0 -0
  43. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/model_routing.py +0 -0
  44. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/paths.py +0 -0
  45. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/permissions.py +0 -0
  46. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/plugins/__init__.py +0 -0
  47. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  48. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  49. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/prompt_suggestions.py +0 -0
  50. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/query/__init__.py +0 -0
  51. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/query/config.py +0 -0
  52. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/query/deps.py +0 -0
  53. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/query/engine.py +0 -0
  54. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/query/stop_hooks.py +0 -0
  55. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/query/token_budget.py +0 -0
  56. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/query/transitions.py +0 -0
  57. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/slash_commands.py +0 -0
  58. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/thinking.py +0 -0
  59. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/tool_registry.py +0 -0
  60. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/tools/__init__.py +0 -0
  61. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/tools/bash.py +0 -0
  62. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/tools/browser.py +0 -0
  63. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/tools/edit.py +0 -0
  64. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/tools/filesystem.py +0 -0
  65. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/tools/search.py +0 -0
  66. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/tools/shell.py +0 -0
  67. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/tools/shell_gate.py +0 -0
  68. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/tools/subtask.py +0 -0
  69. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/tools/think.py +0 -0
  70. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/tools/todo.py +0 -0
  71. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/tools/web.py +0 -0
  72. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/tools_inspector.py +0 -0
  73. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/trust.py +0 -0
  74. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/tui/scrollback.py +0 -0
  75. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/tui/spinner.py +0 -0
  76. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/tui/welcome_banner.py +0 -0
  77. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/tui/welcome_rich.py +0 -0
  78. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/version.py +0 -0
  79. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/vertex.py +0 -0
  80. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/web/__init__.py +0 -0
  81. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/web/claude_sse_adapter.py +0 -0
  82. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/web/terminal_repl.py +0 -0
  83. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode/workspace_hints.py +0 -0
  84. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode.egg-info/dependency_links.txt +0 -0
  85. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode.egg-info/entry_points.txt +0 -0
  86. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode.egg-info/requires.txt +0 -0
  87. {gemcode-0.3.28 → gemcode-0.3.29}/src/gemcode.egg-info/top_level.txt +0 -0
  88. {gemcode-0.3.28 → gemcode-0.3.29}/tests/test_agent_instruction.py +0 -0
  89. {gemcode-0.3.28 → gemcode-0.3.29}/tests/test_autocompact.py +0 -0
  90. {gemcode-0.3.28 → gemcode-0.3.29}/tests/test_capability_routing.py +0 -0
  91. {gemcode-0.3.28 → gemcode-0.3.29}/tests/test_claude_web_adapter_sse.py +0 -0
  92. {gemcode-0.3.28 → gemcode-0.3.29}/tests/test_cli_init.py +0 -0
  93. {gemcode-0.3.28 → gemcode-0.3.29}/tests/test_computer_use_permissions.py +0 -0
  94. {gemcode-0.3.28 → gemcode-0.3.29}/tests/test_context_budget.py +0 -0
  95. {gemcode-0.3.28 → gemcode-0.3.29}/tests/test_context_warning.py +0 -0
  96. {gemcode-0.3.28 → gemcode-0.3.29}/tests/test_credentials.py +0 -0
  97. {gemcode-0.3.28 → gemcode-0.3.29}/tests/test_interactive_permission_ask.py +0 -0
  98. {gemcode-0.3.28 → gemcode-0.3.29}/tests/test_kairos_scheduler.py +0 -0
  99. {gemcode-0.3.28 → gemcode-0.3.29}/tests/test_modality_tools.py +0 -0
  100. {gemcode-0.3.28 → gemcode-0.3.29}/tests/test_model_error_retry.py +0 -0
  101. {gemcode-0.3.28 → gemcode-0.3.29}/tests/test_model_errors.py +0 -0
  102. {gemcode-0.3.28 → gemcode-0.3.29}/tests/test_model_routing.py +0 -0
  103. {gemcode-0.3.28 → gemcode-0.3.29}/tests/test_paths.py +0 -0
  104. {gemcode-0.3.28 → gemcode-0.3.29}/tests/test_permissions.py +0 -0
  105. {gemcode-0.3.28 → gemcode-0.3.29}/tests/test_prompt_suggestions.py +0 -0
  106. {gemcode-0.3.28 → gemcode-0.3.29}/tests/test_repl_commands.py +0 -0
  107. {gemcode-0.3.28 → gemcode-0.3.29}/tests/test_repl_slash.py +0 -0
  108. {gemcode-0.3.28 → gemcode-0.3.29}/tests/test_slash_commands.py +0 -0
  109. {gemcode-0.3.28 → gemcode-0.3.29}/tests/test_thinking_config.py +0 -0
  110. {gemcode-0.3.28 → gemcode-0.3.29}/tests/test_token_budget.py +0 -0
  111. {gemcode-0.3.28 → gemcode-0.3.29}/tests/test_tool_context_circulation.py +0 -0
  112. {gemcode-0.3.28 → gemcode-0.3.29}/tests/test_tools.py +0 -0
  113. {gemcode-0.3.28 → gemcode-0.3.29}/tests/test_tools_inspector.py +0 -0
  114. {gemcode-0.3.28 → gemcode-0.3.29}/tests/test_workspace_hints.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.28
3
+ Version: 0.3.29
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gemcode"
7
- version = "0.3.28"
7
+ version = "0.3.29"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -77,8 +77,12 @@ def _build_runtime_facts(cfg: GemCodeConfig) -> str:
77
77
  caps.append(f"memory ON ({mem_kind}, stored at {mem_path}; ADK preload_memory injects relevant memories before each turn)")
78
78
  if getattr(cfg, "enable_computer_use", False):
79
79
  caps.append("computer_use ON (tools: navigate, click_at, type_text_at, browser_screenshot, browser_find_element, etc.)")
80
+ if getattr(cfg, "enable_code_executor", False):
81
+ 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.")
82
+ if getattr(cfg, "enable_artifacts", True):
83
+ 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
84
  if not caps:
81
- caps.append("none enabled (use /research on, /embeddings on, /memory on, /computer on to enable)")
85
+ caps.append("none enabled (use /research on, /embeddings on, /memory on, /computer on, /code on to enable)")
82
86
  caps_text = "\n".join(f" - {c}" for c in caps)
83
87
 
84
88
  # ── Limits ───────────────────────────────────────────────────────────────
@@ -426,6 +430,41 @@ Use `run_subtask` when the work is better done in an isolated context:
426
430
 
427
431
  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
432
 
433
+ ## ADK Special Tools (always available when ADK supports them)
434
+
435
+ ### `get_user_choice`
436
+ Present the user with a structured multi-option prompt rather than open-ended questions.
437
+ 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").
438
+ This provides a better UX than asking them to type a free-form answer.
439
+
440
+ ### `load_artifacts`
441
+ Load binary/large artifacts that were saved in a previous turn or by a sub-agent.
442
+ Artifacts are keyed by filename (e.g. "report.pdf", "screenshot.png", "output.json").
443
+ Use `user:filename` prefix for user-scoped artifacts that persist across sessions.
444
+ After loading, the artifact bytes are available for further processing (display, analysis, transformation).
445
+
446
+ ### `exit_loop`
447
+ Signal the surrounding LoopAgent to stop iterating and return the final result.
448
+ Only meaningful when this agent is running inside an ADK LoopAgent pipeline.
449
+ Call this when the task is complete and no further iterations are needed.
450
+
451
+ ## Artifacts — storing large outputs
452
+ When `artifacts ON` (see Runtime facts above):
453
+ - **Save** large generated content as artifacts instead of printing them inline:
454
+ - Screenshots from computer_use: save as "screenshot.png" artifact
455
+ - Generated reports/PDFs: save as "report.pdf" artifact
456
+ - Large JSON data: save as "data.json" artifact
457
+ - **Reference** artifacts in instructions via `{artifact.filename?}` template syntax
458
+ - Artifacts are keyed by filename; `user:` prefix = cross-session persistence
459
+
460
+ ## Code Executor (sandboxed Python)
461
+ When `code_executor ON` (see Runtime facts above):
462
+ - You can write Python code blocks in your response and the Gemini API executes them safely
463
+ - The result appears as a `code_execution_result` event with stdout and the outcome
464
+ - Best for: math calculations, data transformation, unit testing logic, quick experiments
465
+ - The sandbox does NOT have internet access or filesystem access — use for pure computation
466
+ - For file I/O or shell commands, use the standard tools (`bash`, `write_file`, etc.)
467
+
429
468
  ## Evaluator-optimizer loop
430
469
  For tasks where quality matters:
431
470
  1. Complete the task (execute tools, write code, run commands)
@@ -470,6 +509,17 @@ All file tools use paths **relative to the project root** (where GemCode was sta
470
509
  return base
471
510
 
472
511
 
512
+ def _build_code_executor(cfg: GemCodeConfig):
513
+ """Return an ADK BuiltInCodeExecutor when enable_code_executor=True, else None."""
514
+ if not getattr(cfg, "enable_code_executor", False):
515
+ return None
516
+ try:
517
+ from google.adk.code_executors import BuiltInCodeExecutor
518
+ return BuiltInCodeExecutor()
519
+ except Exception:
520
+ return None
521
+
522
+
473
523
  def build_root_agent(
474
524
  cfg: GemCodeConfig,
475
525
  extra_tools: list | None = None,
@@ -488,12 +538,19 @@ def build_root_agent(
488
538
  if _tools is not None:
489
539
  tools = list(_tools)
490
540
  else:
491
- tools = build_function_tools(cfg)
541
+ tools = build_function_tools(cfg)
492
542
  if getattr(cfg, "enable_memory", False):
493
543
  # ADK preload_memory injects retrieved memories into the next llm_request.
494
544
  from google.adk.tools import preload_memory
495
-
496
545
  tools = [preload_memory, *tools]
546
+
547
+ # ADK built-in interactive + artifact tools — always available when ADK supports them.
548
+ try:
549
+ from google.adk.tools import get_user_choice, load_artifacts, exit_loop
550
+ tools = [*tools, get_user_choice, load_artifacts, exit_loop]
551
+ except Exception:
552
+ pass
553
+
497
554
  if extra_tools:
498
555
  tools = [*tools, *extra_tools]
499
556
 
@@ -548,15 +605,31 @@ def build_root_agent(
548
605
  tool_config=tool_cfg,
549
606
  )
550
607
 
551
- return LlmAgent(
608
+ # global_instruction applies to the entire agent tree (including sub-agents
609
+ # spawned via run_subtask or multi-agent delegation). Keep it short — it's
610
+ # prepended to every agent's effective instruction.
611
+ global_instr = (
612
+ "You are GemCode, an expert software engineering agent powered by Google Gemini. "
613
+ "Act, don't advise. Complete tasks fully and autonomously. "
614
+ "Think before destructive actions. Use read-only tools before shell/write tools."
615
+ )
616
+
617
+ agent_kwargs: dict = dict(
552
618
  model=cfg.model,
553
619
  name="gemcode",
554
620
  instruction=build_instruction(cfg),
621
+ global_instruction=global_instr,
555
622
  tools=tools,
556
623
  generate_content_config=gen_cfg,
557
624
  **cb_kwargs,
558
625
  )
559
626
 
627
+ code_executor = _build_code_executor(cfg)
628
+ if code_executor is not None:
629
+ agent_kwargs["code_executor"] = code_executor
630
+
631
+ return LlmAgent(**agent_kwargs)
632
+
560
633
 
561
634
  def create_runner(cfg: GemCodeConfig, extra_tools: list | None = None):
562
635
  """Backward-compatible: prefer `gemcode.session_runtime.create_runner`."""
@@ -251,6 +251,22 @@ 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
+
254
270
  def __post_init__(self) -> None:
255
271
  self.project_root = self.project_root.resolve()
256
272
  # Default agentic depth when env omits GEMCODE_MAX_LLM_CALLS (was: None → SDK default).
@@ -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)
@@ -216,4 +216,5 @@ def slash_help_lines() -> list[str]:
216
216
  " /permissions Show permission / HITL settings",
217
217
  " /hooks Show post-turn hook configuration",
218
218
  " /kairos How to launch the background parallel job scheduler",
219
+ " /code [on|off] Toggle sandboxed Python executor (ADK BuiltInCodeExecutor)",
219
220
  ]
@@ -357,6 +357,49 @@ async def process_repl_slash(
357
357
  out()
358
358
  return ReplSlashResult(skip_model_turn=True)
359
359
 
360
+ # ── /code ─────────────────────────────────────────────────────────────────
361
+ if name == "code":
362
+ args_s = (sc.args or "").strip().lower()
363
+ if args_s in ("on", "enable", "1", "true"):
364
+ cfg.enable_code_executor = True
365
+ out("Code executor: ON (sandboxed Python via Gemini built-in executor)")
366
+ out("The agent can now write Python code blocks and execute them safely,")
367
+ out("without requiring bash/shell permission. Best for: math, data, quick tests.")
368
+ out()
369
+ out("Note: the sandbox has no internet/filesystem access — use bash for I/O.")
370
+ return ReplSlashResult(skip_model_turn=True, force_rebuild_runner=True)
371
+ elif args_s in ("off", "disable", "0", "false"):
372
+ cfg.enable_code_executor = False
373
+ out("Code executor: OFF")
374
+ return ReplSlashResult(skip_model_turn=True, force_rebuild_runner=True)
375
+ else:
376
+ # Status
377
+ status = "ON" if getattr(cfg, "enable_code_executor", False) else "OFF"
378
+ out(f"Code executor: {status}")
379
+ out()
380
+ out("ADK BuiltInCodeExecutor — safe sandboxed Python execution via Gemini API.")
381
+ out()
382
+ out("What it does:")
383
+ out(" When ON, the agent can write Python code blocks directly in its response")
384
+ out(" and the Gemini API executes them in a sandboxed environment. The output")
385
+ out(" (stdout, result) is sent back so the agent can use it for further reasoning.")
386
+ out()
387
+ out("Best for:")
388
+ out(" - Math and numerical calculations")
389
+ out(" - Data processing and transformations")
390
+ out(" - Quick tests of logic or algorithms")
391
+ out(" - Anything that doesn't need filesystem or network access")
392
+ out()
393
+ out("Not for:")
394
+ out(" - Shell commands (use bash)")
395
+ out(" - Reading/writing files (use read_file / write_file)")
396
+ out(" - Internet requests (use web_fetch)")
397
+ out()
398
+ out("Toggle: /code on /code off")
399
+ out()
400
+ out("Supported models: gemini-2.5-flash, gemini-2.5-pro, gemini-3.x and newer.")
401
+ return ReplSlashResult(skip_model_turn=True)
402
+
360
403
  # ── /computer ────────────────────────────────────────────────────────────
361
404
  if name in ("computer", "browser"):
362
405
  args_s = (sc.args or "").strip().lower()
@@ -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
- return Runner(
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)
@@ -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.
@@ -56,6 +56,7 @@ 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)"),
59
60
  ("tools", "List all tools and their permission categories"),
60
61
  ("config", "Show full active configuration (all fields)"),
61
62
  ("permissions", "Show current permission mode (default / strict / yes)"),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.28
3
+ Version: 0.3.29
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -29,6 +29,7 @@ src/gemcode/model_routing.py
29
29
  src/gemcode/paths.py
30
30
  src/gemcode/permissions.py
31
31
  src/gemcode/prompt_suggestions.py
32
+ src/gemcode/refine.py
32
33
  src/gemcode/repl_commands.py
33
34
  src/gemcode/repl_slash.py
34
35
  src/gemcode/session_runtime.py
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes