From 1eb0cd0769596bfbddee69239adf652651169136 Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Thu, 16 Apr 2026 04:27:22 -0700 Subject: [PATCH] feat: add adapter code + Dockerfile for standalone deployment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adapters extracted from molecule-monorepo/workspace-template. Uses molecule-ai-workspace-runtime PyPI package for shared infrastructure. - adapter.py — runtime-specific adapter class - requirements.txt — runtime-specific deps + molecule-ai-workspace-runtime - Dockerfile — FROM python:3.11-slim, pip install, COPY adapter, molecule-runtime entrypoint - ADAPTER_MODULE=adapter tells the runtime to load this repo's Adapter class Co-Authored-By: Claude Sonnet 4.6 --- Dockerfile | 21 +++++++ __init__.py | 3 + adapter.py | 141 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 5 ++ 4 files changed, 170 insertions(+) create mode 100644 Dockerfile create mode 100644 __init__.py create mode 100644 adapter.py create mode 100644 requirements.txt diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f03543c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.11-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl gosu ca-certificates nodejs npm \ + && rm -rf /var/lib/apt/lists/* + +# Install Gemini CLI +RUN npm install -g @google/gemini-cli 2>/dev/null || true + +RUN useradd -u 1000 -m -s /bin/bash agent +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY adapter.py . +COPY __init__.py . + +ENV ADAPTER_MODULE=adapter + +ENTRYPOINT ["molecule-runtime"] diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..3e6ad4c --- /dev/null +++ b/__init__.py @@ -0,0 +1,3 @@ +from .adapter import GeminiCLIAdapter as Adapter + +__all__ = ["Adapter"] diff --git a/adapter.py b/adapter.py new file mode 100644 index 0000000..e0f6c78 --- /dev/null +++ b/adapter.py @@ -0,0 +1,141 @@ +"""Gemini CLI adapter — wraps Google's Gemini CLI as an agent runtime. + +Gemini CLI (github.com/google-gemini/gemini-cli, ~101k stars, Apache 2.0) +is structurally identical to the Claude Code adapter: a single-agent agentic +CLI with file/shell tools, MCP support, and a ReAct loop — backed by Gemini +instead of Claude. + +Key differences from claude-code: +- Auth: GEMINI_API_KEY env var (no OAuth token needed) +- Memory file: GEMINI.md (equivalent of Claude Code's CLAUDE.md) +- MCP config: ~/.gemini/settings.json (not via --mcp-config flag) +- Executor: CLIAgentExecutor (no Python SDK; uses gemini CLI subprocess) +""" + +import json +import logging +import os +import sys +from pathlib import Path + +from a2a.server.agent_execution import AgentExecutor + +from molecule_runtime.adapters.base import BaseAdapter, AdapterConfig + +logger = logging.getLogger(__name__) + + +class GeminiCLIAdapter(BaseAdapter): + + @staticmethod + def name() -> str: + return "gemini-cli" + + @staticmethod + def display_name() -> str: + return "Gemini CLI" + + @staticmethod + def description() -> str: + return ( + "Google Gemini CLI — agentic coding with file/shell tools, " + "MCP support, and a ReAct loop backed by Gemini models" + ) + + @staticmethod + def get_config_schema() -> dict: + return { + "model": { + "type": "string", + "description": "Gemini model (e.g. gemini-2.5-pro, gemini-2.5-flash)", + "default": "gemini-2.5-pro", + }, + "required_env": { + "type": "array", + "description": "Required env vars", + "default": ["GEMINI_API_KEY"], + }, + "timeout": { + "type": "integer", + "description": "Timeout in seconds (0 = no timeout)", + "default": 0, + }, + } + + def memory_filename(self) -> str: + """Gemini CLI reads GEMINI.md as its persistent context file.""" + return "GEMINI.md" + + async def setup(self, config: AdapterConfig) -> None: + """Wire MCP server into ~/.gemini/settings.json and seed GEMINI.md. + + Gemini CLI does not accept an --mcp-config flag; instead, MCP servers + are declared in ~/.gemini/settings.json under the "mcpServers" key. + This method merges the A2A MCP server into that file, preserving any + existing keys (e.g. user's own MCP tools). + + Also seeds GEMINI.md from system-prompt.md if GEMINI.md is absent, + so the agent has role context on first boot. + """ + from executor_helpers import get_mcp_server_path + + # -- MCP wiring -------------------------------------------------- + gemini_dir = Path.home() / ".gemini" + gemini_dir.mkdir(parents=True, exist_ok=True) + settings_path = gemini_dir / "settings.json" + + settings: dict = {} + if settings_path.exists(): + try: + settings = json.loads(settings_path.read_text()) + except Exception as exc: + logger.warning("gemini-cli: could not parse %s: %s", settings_path, exc) + settings = {} + + settings.setdefault("mcpServers", {}) + settings["mcpServers"]["a2a"] = { + "command": sys.executable, + "args": [get_mcp_server_path()], + } + + try: + settings_path.write_text(json.dumps(settings, indent=2)) + logger.info("gemini-cli: wrote MCP config to %s", settings_path) + except OSError as exc: + logger.warning("gemini-cli: could not write %s: %s", settings_path, exc) + + # -- GEMINI.md seed ---------------------------------------------- + gemini_md = Path(config.config_path) / "GEMINI.md" + system_prompt_file = Path(config.config_path) / "system-prompt.md" + if not gemini_md.exists() and system_prompt_file.exists(): + try: + gemini_md.write_text(system_prompt_file.read_text()) + logger.info("gemini-cli: seeded GEMINI.md from system-prompt.md") + except OSError as exc: + logger.warning("gemini-cli: could not seed GEMINI.md: %s", exc) + + async def create_executor(self, config: AdapterConfig) -> AgentExecutor: + from cli_executor import CLIAgentExecutor + from config import RuntimeConfig + + rc = config.runtime_config + if isinstance(rc, dict): + model = rc.get("model") or "gemini-2.5-pro" + timeout = int(rc.get("timeout") or 0) + else: + model = getattr(rc, "model", None) or "gemini-2.5-pro" + timeout = int(getattr(rc, "timeout", None) or 0) + + runtime_config = RuntimeConfig( + model=model, + timeout=timeout, + required_env=["GEMINI_API_KEY"], + ) + + return CLIAgentExecutor( + runtime="gemini-cli", + runtime_config=runtime_config, + system_prompt=config.system_prompt, + config_path=config.config_path, + heartbeat=config.heartbeat, + ) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7e05e0a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +# Molecule AI workspace runtime — shared infrastructure +molecule-ai-workspace-runtime>=0.1.0 + +# gemini-cli adapter has no extra Python dependencies. +# The CLIAgentExecutor (base) handles all subprocess management.