Skip to content

Latest commit

 

History

52 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Antigravity CLI REST Bridge

REST API server that wraps Google's Antigravity CLI (agy) interactive mode, giving you HTTP endpoints for named multi-session chat with full conversation continuity. Each session gets its own live, warm CLI process — run as many parallel conversations as you need, with no per-request startup cost.

Quick Start (Docker Compose)

# 1. Start the server
docker compose up -d --build

# 2. First time only — authenticate inside the container
docker exec -it gemini-cli-rest-agy-rest-1 agy
# Complete auth in browser, then Ctrl+C to exit

# 3. Restart so the server picks up the auth
docker compose restart

# 4. Chat! (session "default" is created automatically)
curl -s -X POST http://localhost:8000/chat/default \
  -H "Content-Type: application/json" \
  -d '{"prompt": "What is 2+2?"}'

Auth persists across restarts (stored in a Docker named volume).

Endpoints

POST /chat/{name} — Send a message

Creates the session on first use. Follow-up messages retain full conversation context.

curl -s -X POST http://localhost:8000/chat/research \
  -H "Content-Type: application/json" \
  -d '{"prompt": "Explain the CAP theorem in 2 sentences"}'

Response:

{
  "response": "The CAP theorem states that a distributed system can only simultaneously provide two out of three guarantees: Consistency, Availability, and Partition Tolerance.",
  "session": "research",
  "turn": 1,
  "elapsed_ms": 9573,
  "via": "bell"
}

Follow-up in the same session:

curl -s -X POST http://localhost:8000/chat/research \
  -H "Content-Type: application/json" \
  -d '{"prompt": "Now compare it to the PACELC theorem"}'

via says how the end of the turn was detected: "bell" (agy's terminal-bell notification — the fast path; the codex bridge reports "notify"), or one of the polling-fallback exit reasons ("transcript_done" / "rollout_done", "stalled", "hard_timeout"). A missed notification only means a slower answer, never a hung one — and the field makes the fallback rate observable.

Meanwhile, a completely separate conversation:

curl -s -X POST http://localhost:8000/chat/coding \
  -H "Content-Type: application/json" \
  -d '{"prompt": "Write a Python fibonacci generator"}'

GET /last/{name} — Re-read the last answer (recover a lost response)

Returns a session's most recent completed answer without re-asking — for when a POST /chat response never reached you (the 3-min hard cap, or a connectivity blip after the CLI had already answered). The answer is durable in the CLI's own transcript/rollout regardless of whether the HTTP response arrived, so /last simply reads it back.

It is binary by design: you get the answer only once the turn is done, never a partial and never a previous turn's answer in its place.

# instant snapshot
curl -s http://localhost:8000/last/research

# or wait up to 30s for an in-flight turn to finish, then return it
curl -s "http://localhost:8000/last/research?wait=30"

Response when the turn has finished:

{ "done": true, "response": "…the answer…", "turn": 2, "session": "research", "elapsed_ms": 12, "status": "done" }

While the turn is still running — poll again:

{ "done": false, "response": null, "turn": 2, "session": "research", "elapsed_ms": 30001, "status": "pending" }

status tells the two not-done cases apart: "pending" = still working, keep polling; "never_started" = the CLI dropped the prompt and no answer is coming — re-send it. (The codex bridge adds "usage_limit" / "model_drift" / "error": done, but with an empty response and an error message — see CODEX.md.)

wait is capped at LAST_MAX_WAIT (default 180s) so /last never blocks longer than a /chat would. Recovery works while the server is up (the warm process holds the session); it does not survive a full server restart.

POST /clear/{name} — Clear conversation context

Wipes the conversation by respawning agy in a fresh project directory (~5s). A respawn is required because agy keeps cross-conversation memory per project: its own /clear starts a new conversation that still receives summaries of the previous ones — and the agent can read their transcripts to recover "cleared" context.

curl -s -X POST http://localhost:8000/clear/research

POST /reset/{name} — Full process restart

Kills the session's agy process and spawns a new one. Use when the CLI is stuck or misbehaving (~25s).

curl -s -X POST http://localhost:8000/reset/research

DELETE /chat/{name} — Delete a session

Kills and permanently removes a session and its process.

curl -s -X DELETE http://localhost:8000/chat/research

POST /stop — Stop all sessions

Kills every active session. Clean slate.

curl -s -X POST http://localhost:8000/stop

GET /health — Health check

Lists all active sessions and their status. docker-compose.yml also uses it as the container healthcheck (both bridges must answer, so docker ps shows healthy/unhealthy for the pair).

curl -s http://localhost:8000/health   # agy
curl -s http://localhost:8001/health   # codex
{
  "status": "ok",
  "active_sessions": 2,
  "sessions": [
    {"name": "research", "alive": true, "turn_count": 3},
    {"name": "coding", "alive": true, "turn_count": 1}
  ]
}

Use Cases

  • Parallel research — Run multiple topic-specific sessions simultaneously (/chat/ml-papers, /chat/api-docs, /chat/competitor-analysis)
  • Automation scripts — Each script gets its own named session with isolated context, no cross-contamination
  • Agent contexts — Give each autonomous agent a dedicated session (/chat/agent-planner, /chat/agent-coder, /chat/agent-reviewer)
  • Interactive + batch — Keep a long-running exploratory session open while firing off one-shot queries in disposable sessions

Docker Compose Commands

docker compose up -d --build   # Build and start
docker compose down             # Stop
docker compose restart          # Restart (keeps auth)
docker compose logs -f          # View live logs (stdout)

Logs & diagnostics

Both bridges write persistent log files in addition to stdout, plus a per-incident dump for every slow or failed turn — so you can find out why a turn was slow or timed out without docker exec'ing into a live container.

Everything lands under LOG_DIR (/app/logs), which docker-compose mounts to ./logs on the host:

./logs/
├── agy-rest.log              # rolling agy bridge log (10MB x 5)
├── codex-rest.log            # rolling codex bridge log
└── timeouts/                 # one file per slow/failed turn — START HERE
    ├── <session>-turn<N>-<conversation-id>.log         # agy
    └── codex-<session>-turn<N>-<rollout>.log           # codex

Per-request access log. Every line is tagged with its bridge ([gemini] or [codex], worktree lines included) so the shared docker logs stream stays attributable; the rolling *-rest.log files use the same format with a full date. Each request logs one line when it arrives and one when it finishes, sharing a short id; /chat also records who asked what:

09:34:25 [gemini] INFO    --> POST /chat/foo@dev [a1b2c3d4] from 172.20.0.1
09:34:25 [gemini] INFO    [a1b2c3d4] /chat session 'foo@dev' ua='curl/8.18.0' prompt (33 chars): 'Reply with…'
09:34:38 [gemini] INFO    <-- POST /chat/foo@dev [a1b2c3d4] 200 in 12.8s   # finished: status + duration
09:34:38 [gemini] INFO    [b5c6d7e8] DELETE /chat/nope@dev failed 404: Session 'nope@dev' not found.
09:34:38 [gemini] INFO    <-- DELETE /chat/nope@dev [b5c6d7e8] 404 in 0.0s

This is how you investigate the three failure modes from the file alone: a stuck request shows a --> with no matching <-- (still hung in its handler); a break logs a full !!! traceback and returns 500; a slow turn is the one whose <-- carries a large in …s. Any request the bridge fails on purpose logs its reason on a failed NNN: line just before the <-- (WARNING for 5xx, INFO for 4xx and malformed bodies). /last additionally logs whether it actually recovered a completed answer. Health checks log at DEBUG so polling never floods the file, and uvicorn's own access log is off — the bridge lines above replace it.

When does a dump get written? Whenever a turn hits the hard cap, stalls, or simply runs slower than *_SLOW_DUMP_SECS (90s) — even if it succeeded. A turn faster than that leaves no dump.

What's in each dump (both bridges follow the same shape now):

  • agy — the resolved conversation id + transcript path, the timestamped tail of the transcript (the step-by-step record — where the time actually went, e.g. a long RUN_COMMAND or a node: command not found retry loop), and the rendered screen at the moment the bridge stopped waiting. That screen tells you whether a missed answer was already on-screen (transcript flush lag) or agy was still Generating....
  • codex — the resolved session id + rollout path, the tail of the rollout events (task_started / response_item / task_complete with their turn_ids — the turn-by-turn record), and the rendered screen at give-up time. Full detail lives in codex's own rollout at ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl.

Turn timeouts. A single request is hard-capped at 3 minutes (RESPONSE_HARD_TIMEOUT / CODEX_RESPONSE_HARD_TIMEOUT = 180); each bridge also gives up early if its CLI makes no progress for *_STALL_TIMEOUT (90s). Work that genuinely needs longer should be handled by the client re-polling, not by raising these caps. That re-poll is GET /last/{name}: the turn keeps running in the warm process after a request gives up, its answer is written to the CLI's transcript/rollout, and /last reads it back once it is done — so a capped or dropped /chat never loses the reply.

Running Without Docker

Requires Python 3.11+, tmux, and the Antigravity CLI.

# Install Antigravity CLI (to ~/.local/bin/agy)
curl -fsSL https://antigravity.google/cli/install.sh | bash
agy  # authenticate once, then /quit or Ctrl+C

# Install Python deps (tmux from your package manager if missing)
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

# Run
uvicorn server:app --host 0.0.0.0 --port 8000

Configuration

All config via environment variables (set in docker-compose.yml or shell):

Variable Default Description
AGY_CMD agy Path to the agy CLI binary
AGY_SKIP_PERMISSIONS false Pass --dangerously-skip-permissions to agy. Prefer "toolPermission": "always-proceed" in agy's settings.json (the Docker image seeds this)
AGY_EXTRA_ARGS Additional CLI args (space-separated)
AGY_MODEL Gemini 3.8 Flash (High) Passed as --model to every session. Reasoning effort is part of the model name in agy (agy models lists Low and High as separate entries), so this is how effort is set. Its own variable, not AGY_EXTRA_ARGS: the name contains spaces. Wins over the model in settings.json, which agy rewrites itself
AGY_STATE_DIR ~/.gemini/antigravity-cli agy's state directory (conversation transcripts live here)
SESSIONS_ROOT /tmp/agy-rest-sessions Per-session working directories
TMUX_SOCKET agy-rest Dedicated tmux server socket name
RESPONSE_POLL_INTERVAL 0.5 Seconds between completion-detection polls (fallback path)
AGY_BELL 1 Use agy's end-of-turn terminal bell as the fast done-signal; set to 0 to disable and revert to pure polling
AGY_BELL_DIR /tmp/agy-rest-bells Directory where the tmux alert-bell hook records bells (one file per tmux session)
RESPONSE_FAST_POLL 0.3 Seconds between checks of the bell file while a turn is in flight
RESPONSE_FULL_CHECK_EVERY 10 Run the full fallback poll (transcript + screen) every Nth bell-check wake (~3s)
RESPONSE_STALL_TIMEOUT 90 Give up if agy makes no progress (idle, transcript not growing) for this long
RESPONSE_HARD_TIMEOUT 180 Absolute hard cap on a turn (3 min), regardless of progress
RESPONSE_SLOW_DUMP_SECS 90 Write a diagnostic dump for any turn slower than this (even successful ones)
STARTUP_TIMEOUT 60 Max seconds to wait for CLI startup
AGY_STARTUP_RETRIES 1 Respawns after a startup that never shows the idle prompt (screen + agy's own process log are dumped to LOG_DIR/timeouts/<session>-startup-*.log first, the stuck process is killed and confirmed gone, then the same worktree/tmux session is relaunched). Only after every attempt fails does /chat get the 503 "agy startup timed out … (after N attempts)". Startups are serialized bridge-wide: agy shares a per-second process log, sqlite stores and presence locks under AGY_STATE_DIR, and two instances initializing at once (or one starting while another is still shutting down) is what stalled it
AGY_EXIT_WAIT 10 Seconds a stop/clear/reset waits for the agy process to actually exit after tmux kill-session (its shutdown takes 0.1–5s: it waits for store migrations first) before SIGKILLing its process group. A spawn never overlaps a shutting-down instance
VERIFY_RESUBMIT_MAX 3 How many times a session's first prompt is re-pasted when agy's per-launch account-verification gate eats it (⚠ Verifying your account...); set to 0 to disable the recovery
VERIFY_RESUBMIT_DELAY 3.0 Seconds to let the screen settle before a re-paste — and the grace a fresh submit gets before the (permanently displayed) notice may count as another drop
CONVERSATION_DETECT_TIMEOUT 20 Expected time for a session's first prompt to produce its conversation transcript (how the bridge learns the conversation id). An idle screen with no transcript at the end of it means the prompt was dropped → /chat 502
CONVERSATION_DETECT_MAX RESPONSE_STALL_TIMEOUT (90) Hard bound on that wait when agy is visibly still working past the window — process alive and the screen showing Generating... or the auth/backend Signing in... spinner (agy 1.1.27 can sit 20s+ on a post-login loadCodeAssist call before forwarding the prompt). Checked ~1/s; a give-up writes LOG_DIR/timeouts/<session>-detect-*.log (screen + tail of agy's own log). Response collection then gets only what is left of RESPONSE_HARD_TIMEOUT
SUBMIT_REPASTE_MAX 2 How many times a submit may be re-pasted when agy consumed the paste itself (input box empty, transcript frozen — Enter re-press can't help). Re-checked against the transcript at the last instant so an accepted turn can never duplicate; 0 disables
SUBMIT_REPASTE_DELAY 3.0 Seconds to let the screen settle before each re-paste
LOG_DIR /app/logs Rolling logs + per-incident dumps written here (mounted to ./logs)
LOG_LEVEL INFO Logging level
WORKTREE_GIT_TIMEOUT 60 Seconds before any git call made for a session worktree (above all the spawn-time git fetch) is killed. A timed-out or failed fetch is logged as a warning and the spawn continues on the origin/* refs already in the clone; 0 disables the cap. Pair with a GIT_SSH_COMMAND carrying -o ConnectTimeout / -o BatchMode=yes (compose does) so ssh itself cannot prompt or hang
WORKTREE_FETCH_MIN_INTERVAL 60 At most one git fetch per this many seconds per bridge process; concurrent spawns share the in-flight fetch, later ones inside the window skip it (agy and codex are separate processes, so the pair may fetch twice per window)

The codex bridge (port 8001) shares this tmux architecture and has matching, CODEX_-prefixed knobs — CODEX_RESPONSE_HARD_TIMEOUT (180), CODEX_RESPONSE_STALL_TIMEOUT (90), CODEX_STARTUP_TIMEOUT (60), CODEX_SLOW_DUMP_SECS (90), CODEX_TMUX_SOCKET (codex-rest) — and shares LOG_DIR / LOG_LEVEL. Its completion-push equivalent is codex's notify hook rather than a bell: CODEX_NOTIFY (1, set 0 to revert to pure polling), CODEX_NOTIFY_DIR (/tmp/codex-rest-notify), CODEX_RESPONSE_FAST_POLL (0.3), CODEX_RESPONSE_FULL_CHECK_EVERY (10). See CODEX.md and Logs & diagnostics.

The model is not selected per-request — set "model" in agy's settings.json (~/.gemini/antigravity-cli/settings.json).

Architecture

Client (curl/app)          Server (FastAPI)                 tmux              agy
─────────────────     ──────────────────────────     ───────────────    ─────────────
                         ChatManager
                      ┌──────────────────────┐
POST /chat/foo ──────→│  session "foo"       │       tmux session
                      │  AgySession ─────────────→  "agy-foo"  ─────→  live agy TUI
                      │                      │       paste-buffer -p → typed input
                      │   response text ←──────── transcript.jsonl ←── model output
  ← JSON response ←──│                      │       capture-pane → busy/idle check
                      ├──────────────────────┤
POST /chat/bar ──────→│  session "bar"       │       tmux session
                      │  AgySession ─────────────→  "agy-bar"   ─────→  live agy TUI
                      └──────────────────────┘
GET   /last/{name} ─→ re-read the last COMPLETED answer (recover a lost response)
POST  /clear/{name} → respawn in a fresh project dir (true context wipe)
POST  /reset/{name} → kill + respawn session's process
DELETE /chat/{name} → stop + remove session entirely
POST  /stop ────────→ stop all sessions
GET   /health ──────→ list all sessions + status

Each named session owns a live agy process hosted in a detached tmux session (dedicated socket, so it never touches your own tmux). tmux acts as a terminal-emulator mediator with four clean channels:

  1. Input — prompts are injected with tmux load-buffer + paste-buffer -p (bracketed paste), so newlines and TUI shortcut characters (!, @, /, backticks) always arrive as literal text.
  2. Response content — read from agy's structured per-conversation transcript (brain/<conversation>/.system_generated/logs/transcript.jsonl), not scraped off the screen. The bridge takes the completed (DONE) model steps that appeared after the prompt was sent.
  3. Completion push (bell) — the primary done-signal. With "notifications": true in agy's settings.json (the entrypoint seeds it; the server also ensures it at startup), agy rings a terminal bell at end of turn, 0–43ms after the answer is flushed to the transcript. A server-global tmux alert-bell hook appends a timestamped line to AGY_BELL_DIR/<tmux-session>; the bridge checks that file every RESPONSE_FAST_POLL (0.3s) and, on a new bell past the turn's baseline, reads the transcript immediately, with a single busy-glance at the pane confirming the ring wasn't a stray mid-turn bell — no idle-marker match, no debounce. /last also accepts bell-evidence in place of the idle-screen check.
  4. Busy/idle detection (fallback)tmux capture-pane returns the rendered screen (no ANSI escapes); if the bell is missed (or AGY_BELL=0), a turn is complete when a new model step exists in the transcript and the screen shows the idle status bar (? for shortcuts) with no Generating... indicator. A missed bell degrades to this slower path, never to a hung request.

You can watch any session live while the bridge drives it:

tmux -L agy-rest attach -t agy-research   # Ctrl+B then D to detach

Limitations

  1. No streaming — Responses return only after fully collected.
  2. Prompts starting with / may be interpreted as agy slash commands.
  3. Model selection is global (agy settings.json), not per-session.
  4. agy's brain is shared — sessions are isolated per project directory, but the agent has filesystem access; a prompt that explicitly asks it to read another conversation's transcript under ~/.gemini/antigravity-cli/brain/ could cross session boundaries.

About

REST API bridge for Google's Gemini CLI — named multi-session chat with conversation continuity

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages