From c754dc4afcc2df324fef4a06c8ce9ff1bcf1537f Mon Sep 17 00:00:00 2001 From: Michael Xu Date: Thu, 10 Sep 2026 12:14:23 -0500 Subject: [PATCH] docs: add Gemini CLI agents guide Companion to the scale-agentex-python change that adds a Gemini CLI harness (convert_gemini_cli_to_agentex_events, GeminiCliTurn, and sync / async / temporal agentex init templates). Documents the CLI invocation, the stream-json to canonical-event mapping, usage normalisation, and the multi-turn and tool-approval caveats; adds the page to the Framework Agents nav and lists Gemini CLI among the coding-CLI options. Claude-Session: https://claude.ai/code/session_01HCVKnA7LeJZ44nxZz1uzF3 --- .../development_guides/gemini_cli_agents.md | 90 +++++++++++++++++++ .../getting_started/choose_your_agent_type.md | 8 +- agentex/docs/mkdocs.yml | 1 + 3 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 agentex/docs/docs/development_guides/gemini_cli_agents.md diff --git a/agentex/docs/docs/development_guides/gemini_cli_agents.md b/agentex/docs/docs/development_guides/gemini_cli_agents.md new file mode 100644 index 00000000..e2eabf31 --- /dev/null +++ b/agentex/docs/docs/development_guides/gemini_cli_agents.md @@ -0,0 +1,90 @@ +# Gemini CLI Agents + +A Gemini CLI agent wraps the `gemini` CLI as a local subprocess and streams its output through the [unified harness](streaming_patterns.md#unified-harness-surface-framework-agents). You spawn the CLI in headless mode with `--output-format stream-json`, pass the prompt with `-p`, and hand its newline-delimited JSON stream to a `GeminiCliTurn`. The `UnifiedEmitter` then delivers the canonical `StreamTaskMessage*` events and derives tracing spans automatically, exactly like the Claude Code and Codex harnesses. + +Scaffold one with `agentex init` by picking the **Gemini CLI** framework option (available for Sync, Async-base, and Temporal). + +## Prerequisites + +- The `gemini` CLI installed and on your `PATH` (`npm install -g @google/gemini-cli`). +- A `GEMINI_API_KEY` in the environment (the CLI's other login methods also work in a shell where you have signed in). Optionally `GEMINI_MODEL` to pin a model; the CLI defaults to `auto`. + +## How it works + +The template spawns the CLI in streaming-JSON mode with the prompt on the command line and stdin closed: + +```python +cmd = ["gemini", "-p", prompt, "--output-format", "stream-json"] +if model := os.environ.get("GEMINI_MODEL"): + cmd.extend(["-m", model]) +proc = await asyncio.create_subprocess_exec( + *cmd, + stdin=asyncio.subprocess.DEVNULL, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, +) +``` + +Stdin is closed on purpose: in non-interactive mode the CLI reads stdin to EOF and appends it to the prompt, so an open pipe would make it wait forever. + +`GeminiCliTurn(lines)` wraps the iterator of stdout lines (raw JSON strings or pre-parsed dicts). Under the hood it runs the `convert_gemini_cli_to_agentex_events` tap, which maps the CLI's events onto the canonical stream: + +| Gemini CLI event | Canonical events | +|---|---| +| `init` | none (session id and model are captured on the turn) | +| `message` (`role: user`) | none (the CLI echoes the prompt) | +| `message` (`role: assistant`, `delta: true`) | `Start(TextContent)` once, then a `Delta(TextDelta)` per chunk; the slot is closed on the next tool event or the `result` | +| `tool_use` | `Start(ToolRequestContent)` + `Done`, keyed by `tool_id` | +| `tool_result` | `Full(ToolResponseContent)` with the output (or the error message and `is_error`) | +| `error` | logged, nothing emitted | +| `result` | closes any open text slot; its `stats` become the turn's `TurnUsage` | + +The turn exposes `session_id` and `model` from the `init` event, and `usage()` maps `stats` (`input_tokens`, `output_tokens`, `cached`, `total_tokens`, `duration_ms`, `tool_calls`) onto `TurnUsage`. The CLI does not report cost, so `cost_usd` stays `None`. + +## Sync delivery (HTTP yield) + +```python +import agentex.lib.adk as adk +from agentex.lib.adk import UnifiedEmitter, GeminiCliTurn + +@acp.on_message_send +async def handle_message_send(params: SendMessageParams): + task_id = params.task.id + async with adk.tracing.span( + trace_id=task_id, task_id=task_id, name="message", + input={"message": prompt}, + data={"__span_type__": "AGENT_WORKFLOW"}, + ) as turn_span: + emitter = UnifiedEmitter( + task_id=task_id, trace_id=task_id, + parent_span_id=turn_span.id if turn_span else None, + ) + turn = GeminiCliTurn(_spawn_gemini(prompt)) # iterator of CLI stdout lines + async for event in emitter.yield_turn(turn): + yield event +``` + +## Async and Temporal delivery + +For Async-base and Temporal agents the body is the same, except you call `auto_send_turn` (which pushes to Redis and returns a `TurnResult`) instead of `yield_turn`. Under Temporal, run the subprocess inside an activity and pass `created_at=workflow.now()`: + +```python +result = await emitter.auto_send_turn(turn, created_at=workflow.now()) +# result.final_text, result.usage +``` + +Always tear the subprocess down in a `finally` block so a cancelled or failed turn does not leak a `gemini` process. + +## Multi-turn conversations + +The Gemini CLI's `--resume` flag takes `latest` or a session index rather than a session id, which is not safe when one worker serves several tasks. The templates therefore run each turn as an independent prompt and keep the reported `session_id` for observability only. If you need conversational memory, carry the relevant history into the prompt yourself. + +## Tool approval + +The CLI's `--approval-mode` (`default`, `auto_edit`, `yolo`, `plan`) governs what its built-in tools may do without confirmation. The templates do not pass it, so the CLI's default applies; add `--approval-mode yolo` to the spawn only for agents that are meant to run tools unattended, and prefer the CLI's policy engine for anything shared. + +## See also + +- [Unified Harness Surface](streaming_patterns.md#unified-harness-surface-framework-agents) +- [Observability & Tracing](observability_and_tracing.md) +- [Claude Code Agents](claude_code_agents.md) and [Codex Agents](codex_agents.md), the other CLI harnesses diff --git a/agentex/docs/docs/getting_started/choose_your_agent_type.md b/agentex/docs/docs/getting_started/choose_your_agent_type.md index e6d8d4b7..4bbc0572 100644 --- a/agentex/docs/docs/getting_started/choose_your_agent_type.md +++ b/agentex/docs/docs/getting_started/choose_your_agent_type.md @@ -27,9 +27,9 @@ Choosing an agent type is only the **first** prompt in `agentex init`. After you | `agentex init` prompt | Framework options | |---|---| -| **Sync ACP** | Basic · OpenAI Agents SDK (Recommended) · OpenAI Agents SDK + Local Sandbox · LangGraph · Pydantic AI · Claude Code · Codex | -| **Async - ACP Only** | Basic · OpenAI Agents SDK · LangGraph · Pydantic AI · Claude Code · Codex | -| **Async - Temporal** | Basic · OpenAI Agents SDK (Recommended) · Pydantic AI · LangGraph · Claude Code · Codex | +| **Sync ACP** | Basic · OpenAI Agents SDK (Recommended) · OpenAI Agents SDK + Local Sandbox · LangGraph · Pydantic AI · Claude Code · Codex · Gemini CLI | +| **Async - ACP Only** | Basic · OpenAI Agents SDK · LangGraph · Pydantic AI · Claude Code · Codex · Gemini CLI | +| **Async - Temporal** | Basic · OpenAI Agents SDK (Recommended) · Pydantic AI · LangGraph · Claude Code · Codex · Gemini CLI | !!! note "OpenAI Agents SDK + Local Sandbox" The **Local Sandbox** variant is the OpenAI Agents SDK starter wired to run tools inside a local sandbox. It shares the same harness wiring and tutorial base as the plain [OpenAI Agents SDK](../development_guides/tutorials.md#sync-acp-simple-agents) starter — there is no separate tutorial for it. @@ -42,7 +42,7 @@ How to choose: - **Writing the loop yourself / LiteLLM only** → Basic. - **Already standardized on a framework** → pick it directly (LangGraph, Pydantic AI, OpenAI Agents SDK). -- **Wrapping a coding CLI** → Claude Code (spawns the `claude` CLI) or Codex (spawns the `codex` CLI) as a local subprocess, streamed through the harness. +- **Wrapping a coding CLI** → Claude Code (spawns the `claude` CLI), Codex (spawns the `codex` CLI), or Gemini CLI (spawns the `gemini` CLI) as a local subprocess, streamed through the harness. - **Want tools + good defaults and unsure** → OpenAI Agents SDK (the Recommended option for Sync and Temporal). To add a framework that isn't in the list, see the `agentex-add-agent-framework` workflow. diff --git a/agentex/docs/mkdocs.yml b/agentex/docs/mkdocs.yml index 566a98ba..ad9c91db 100644 --- a/agentex/docs/mkdocs.yml +++ b/agentex/docs/mkdocs.yml @@ -73,6 +73,7 @@ nav: - Framework Agents: - Claude Code: development_guides/claude_code_agents.md - Codex: development_guides/codex_agents.md + - Gemini CLI: development_guides/gemini_cli_agents.md - OpenAI Local Sandbox: development_guides/local_sandbox.md - Race Conditions: development_guides/race_conditions.md - Message Handling: development_guides/message_handling.md