Skip to content

feat(antigravity): AG-UI integration for Google Antigravity - #2277

Open
mme wants to merge 16 commits into
mainfrom
mme/antigravity
Open

feat(antigravity): AG-UI integration for Google Antigravity#2277
mme wants to merge 16 commits into
mainfrom
mme/antigravity

Conversation

@mme

@mme mme commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Adds an AG-UI integration for the Google Antigravity SDK, plus the four dojo demos.

Antigravity is not a Python agent loop. The SDK drives a bundled Go localharness subprocess over a WebSocket, and that subprocess does real file and shell work on the host. So this is an ADK-class stateful integration, not a stateless LangGraph-class one: the stream translation is small, the session lifecycle is the substance.

What makes the HITL path unusual

Antigravity's hooks and custom tools are async, awaited, and carry no timeout on either the Python or the Go side. That makes a parked coroutine a native suspension primitive: emit AG-UI events, park an asyncio.Future, let run N's SSE response close, resolve it from run N+1. A client tool result becomes the tool's actual return value — no proxy tool, no fire-and-forget long-running-tool workaround.

The Go side's behaviour wasn't documented, so it was settled empirically: the harness survived 45 s and 180 s parks with the consumer detached and resumed correctly. tests/test_parking_gate.py keeps that honest so an SDK upgrade that breaks it fails loudly rather than silently hanging every HITL run.

The non-obvious consequence: one Antigravity turn can span several AG-UI runs, and the harness re-delivers the steps of a turn already in progress. The step iterator, the EventTranslator and the bridge's per-turn tool claims are therefore scoped to the turn, not the run, and retired together.

Layout

Path
integrations/antigravity/python/src/ag_ui_antigravity/ the adapter: translator, UI bridge, session manager, FastAPI endpoint
integrations/antigravity/typescript/ @ag-ui/antigravityHttpAgent + capability discovery
integrations/antigravity/python/examples/ demo server, one agent per dojo feature
apps/dojo/ menu, agents, env, content mapper, run/prep scripts

Commits are split along those four seams.

Verification

234 unit tests pass
16 live tests (real harness subprocess + real model) pass
15 TypeScript tests, typecheck, build pass
Coverage 99.6% line / 98.9% branch
Dojo — all four demos, in-browser pass
Translator output replayed through the real @ag-ui/client verifyEvents 0 violations

The event stream was fuzzed against a port of verify.ts across several thousand randomized multi-run sessions — parks, redelivery, error steps, cancellations, resume entries, thinking, subagents, tool-set changes mid-thread, forced closes interleaved with live runs — asserting zero protocol violations and exactly one terminal event per run.

Upstream gaps in google-antigravity 0.1.8

These are SDK issues, not AG-UI ones, and affect only the OpenAI-compatible (base_url) path:

  1. No API-key field. GemmaEndpoint carries only base_url, and the Go harness reads no OPENAI_API_KEY — that path targets unauthenticated local servers (Ollama, LM Studio). examples/server/openai_proxy.py is a shim for the demos only; delete it when the SDK supports authenticated endpoints.
  2. Gemini-shaped tool schemas. Built with api_option="GEMINI_API", emitting proto-style uppercase types ("STRING") that OpenAI rejects. Repaired in the same shim. Tools registered via ToolWithSchema — how this integration builds frontend tools — pass through untouched.
  3. session_continuation_mode dropped by LocalOpenAIAgentConfig.create_strategy, silently disabling cold resume. Worked around by _ResumableOpenAIConfig, which raises if the SDK renames the attribute rather than degrading quietly.

Also worth knowing: the harness escalates a slow custom tool to a background task and lets the model continue without its result, so the model re-issues the call. A frontend tool is therefore dispatched to the client at most once per turn (deduplicate_tool_calls, on by default) — otherwise a side-effecting client action runs twice.

Not implemented

Triggers, multimodal input, STATE_DELTA, and live MCP coverage. predictive_state_updates and shared_state aren't in the dojo menu — no demo agent backs them. All listed in the package README.

Notes for review

  • Port 8027, not 8009: run-dojo-everything.js starts Pydantic AI on 8009 and dojo-e2e.yml waits on it.
  • The demos ship tool_approval=False. It parks on an interrupt the dojo cannot answer, so the first write would hang; enable it against a client that implements the interrupt protocol.
  • @ag-ui/antigravity's getCapabilities() deliberately diverges from the @ag-ui/adk reference in two ways (uses the injectable this.fetch; resolves relative URLs). The same two issues exist in ADK and are not touched here.

🤖 Generated with Claude Code


Update: harness process pooling

Sessions no longer take a subprocess each. Antigravity configures a harness
twice, and almost everything that varies per thread is in the second half:

sent when contains
InputConfig process stdin at startup save_dir, env
HarnessConfig WebSocket, per conversation tools, model, capabilities, MCP servers, hooks, subagents, response_schema, conversation_id, workspaces

So conversations agreeing on (binary_path, save_dir, env) can share a process,
and workspaces staying per-conversation means filesystem isolation is
unaffected. Measured over 8 concurrent conversations:

pooled (1 process) one process each
idle 101 MB 752 MB
mid-turn 154 MB 1040 MB
wall clock 12.7 s 12.3 s

An extra idle conversation costs ~1 MB rather than ~95 MB. Throughput is
unchanged. max_conversations_per_process=1 restores the old behaviour.

Pooling lives in a ConnectionStrategy — the layer the SDK documents as owning
"process management, transport setup, authentication, and health checking".
PooledStrategy wraps the strategy the SDK builds and replaces only
__aenter__/__aexit__, so Agent, Conversation, LocalConnection, the
event processor and tool/hook dispatch are used unmodified — including Agent's
mandatory safety guard.

Gates, both passed and kept as regression tests

  • A SIGKILLed harness makes its conversations raise promptly rather than hang.
    A hang would wedge a thread, since SessionManager holds its per-session lock
    across receive_steps.
  • A conversation parked on a human does not block its co-tenants, so HITL
    and pooling compose. A 20 s tool call was measured not to delay neighbours
    (median inflation 0.95× against a control; an uncontrolled first pass wrongly
    suggested head-of-line blocking).

Also fixes a pre-existing cold-resume bug: save_dir defaulted to a fresh
tempfile.mkdtemp() per config, so a rebuilt session resumed against an empty
directory. One save directory per adapter now.

Caveats. One dead process fails every conversation on it — that is the
tradeoff pooling buys, hence the modest default of 8. A parked session pins its
process, so under park-heavy load the saving is bounded by fragmentation rather
than the ~1 MB marginal figure. save_dir is shared per process and is not a
tenancy boundary.


Update: server-side tool results

A custom Python tool never produced a TOOL_CALL_RESULT, so any client
rendering one waited forever. The dojo's backend_tool_rendering demo showed it
plainly — the page registers a useRenderTool for get_weather and draws a
card from the result, and that card could never leave its loading state.

The harness reports a custom tool as a single TOOL_CALL/ACTIVE step:
no DONE step, and no result field anywhere on Step, because the SDK's own
ToolRunner executes the tool and the return value goes back over the
WebSocket straight to the model. The translator built results by diffing
tool-call arguments — which is how the harness reports built-in tools — and
that diff is empty here.

This process runs the tool, so the value is in hand. UIBridge now wraps each
server-side tool to emit the call and a TOOL_CALL_RESULT carrying the real
return value, with those names suppressed in the translator so nothing is
emitted twice. The wrapper keeps each function's signature via
functools.wraps, so the SDK still derives the same tool schema — a bare
**kwargs wrapper would have erased the parameters.

backend_tool_rendering gets a real get_weather tool, following the
convention the llama-index demo uses: keyless open-meteo, AG_UI_MOCK_WEATHER
for offline and CI runs, and a fallback to canned data so the demo never
renders a broken card. Verified in the browser across two turns of one thread.

Known limitation, measured: tool calls do not stream their arguments. The
harness hands over a call fully formed in one step — confirmed with a custom
tool whose argument was 1850 characters. TOOL_CALL_ARGS therefore arrives as
a single delta; there are no fragments to forward. The call lifecycle does
stream, which is what lets a client show a pending tool card while the tool
runs.

mme and others added 4 commits July 29, 2026 20:59
Antigravity is not a Python agent loop: the SDK drives a bundled Go
"localharness" subprocess over a WebSocket, and that subprocess does real
file and shell work on the host. So this is an ADK-class stateful
integration rather than a stateless LangGraph-class one -- the stream
translation is small, and the session lifecycle is the substance.

The design rests on one property of the harness that the SDK source could
not answer: its hooks and custom tools are awaited with no timeout on
either the Python or the Go side. That makes a parked coroutine a native
suspension primitive -- emit AG-UI events, park an asyncio.Future, let the
SSE response for run N close, resolve it from run N+1 -- so a client tool
result is the tool's actual return value, with no proxy tool or
fire-and-forget workaround. tests/test_parking_gate.py pins that property
so an SDK upgrade that breaks it fails loudly instead of silently hanging
every human-in-the-loop run.

The subtlety is that one Antigravity *turn* can span several AG-UI *runs*,
and the harness re-delivers the steps of a turn already in progress. The
step iterator, the EventTranslator and the bridge's per-turn tool claims
are therefore scoped to the turn, not the run, and retired together.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
All protocol translation lives in the Python package, so this is an
HttpAgent plus capability discovery, mirroring @ag-ui/adk.

Two things it does NOT inherit from that reference implementation, both
verified against the base class rather than assumed:

  - getCapabilities() uses `this.fetch`, not the global. HttpAgent stores
    an injectable fetch and run() honours it; calling the global here
    means a custom fetch for auth, proxying, retry or a Node polyfill
    applies to runs and is silently ignored on capability discovery.
  - a relative agent URL resolves against `location` instead of throwing
    a bare TypeError. `run()` lets fetch resolve it against the page
    origin, so a relative URL is a normal browser configuration; only
    capability discovery died on it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One agent per dojo feature, following the adk-middleware layout
(examples/server as a package, `uv run dev` via [project.scripts]).

Two configuration choices are load-bearing and easy to get wrong:

  - The chat demos enable no built-in tool except `finish`. The harness
    exposes its whole toolset by default, and `search_web` returns an
    empty summary without Google credentials -- the model then retries it
    until the turn never settles. `finish` must stay; the harness uses it
    to end a turn.
  - No demo sets tool_approval=True. It parks on an AG-UI interrupt, and
    the dojo answers frontend tools (ToolMessage) rather than interrupts
    (RunAgentInput.resume), so the first write would hang on a prompt
    nothing can answer. It looks fine until you ask for one, because
    reads are auto-approved.

openai_proxy.py is a shim, not part of the integration: google-antigravity
0.1.8's OpenAI-compatible path carries no API key field and the Go harness
reads no OPENAI_API_KEY, so it exists only to let the demos run against a
hosted endpoint. It also repairs the Gemini-shaped tool schemas the SDK
emits on that path. Delete it once the SDK supports authenticated
OpenAI-compatible endpoints.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Menu entry, agent map, env var, content mapper, and the run/prep scripts
so the server starts with the rest of `run-dojo-everything`.

Port 8027, not 8009: run-dojo-everything.js starts Pydantic AI on 8009
(env.ts's 9000 for that integration is itself out of sync with the
launcher), and dojo-e2e.yml waits on tcp:localhost:8009, so sharing it
would have pointed the dojo at the wrong backend.

The content mapper lists _common.py and server/__init__.py alongside each
feature file: the four demo agents are five-line delegations, and every
substantive choice -- model, workspace, capability allowlist -- lives in
those two.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mme
mme requested a review from a team as a code owner July 29, 2026 19:02
@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Python Preview Packages

Version 0.0.0.dev1785499835 published to TestPyPI.

Warning: These packages are built from contributor code that may not yet have been vetted for correctness or security. Install at your own risk and do not use in production.

Install with uv

Add the TestPyPI index to your pyproject.toml:

[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
explicit = true

Then install the packages you need:

# Core SDK
uv add 'ag-ui-protocol==0.0.0.dev1785499835' --index testpypi

# Integrations (each already depends on the matching ag-ui-protocol preview)
uv add 'ag-ui-langgraph==0.0.0.dev1785499835' --index testpypi
uv add 'ag-ui-crewai==0.0.0.dev1785499835' --index testpypi
# NOTE: ag-ui-agent-spec depends on pyagentspec (git-only, not on PyPI).
# You will need to install pyagentspec separately from its git repo.
uv add 'ag-ui-agent-spec==0.0.0.dev1785499835' --index testpypi
uv add 'ag_ui_adk==0.0.0.dev1785499835' --index testpypi
uv add 'ag_ui_strands==0.0.0.dev1785499835' --index testpypi

Install with pip

pip install \
  --index-url https://test.pypi.org/simple/ \
  --extra-index-url https://pypi.org/simple/ \
  ag-ui-protocol==0.0.0.dev1785499835

Use --extra-index-url https://pypi.org/simple/ so pip can resolve
transitive dependencies (pydantic, fastapi, etc.) from real PyPI.


Commit: 82d2f9a

@pkg-pr-new

pkg-pr-new Bot commented Jul 29, 2026

Copy link
Copy Markdown

Open in StackBlitz

@ag-ui/a2a-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2a-middleware@2277

@ag-ui/a2ui-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2ui-middleware@2277

@ag-ui/event-throttle-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/event-throttle-middleware@2277

@ag-ui/mcp-apps-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mcp-apps-middleware@2277

@ag-ui/mcp-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mcp-middleware@2277

@ag-ui/a2a

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2a@2277

@ag-ui/adk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/adk@2277

@ag-ui/ag2

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/ag2@2277

@ag-ui/agno

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/agno@2277

@ag-ui/antigravity

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/antigravity@2277

@ag-ui/aws-strands

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/aws-strands@2277

@ag-ui/claude-agent-sdk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/claude-agent-sdk@2277

@ag-ui/claude-managed-agents

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/claude-managed-agents@2277

@ag-ui/crewai

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/crewai@2277

@ag-ui/langchain

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langchain@2277

@ag-ui/langgraph

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langgraph@2277

@ag-ui/llamaindex

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/llamaindex@2277

@ag-ui/mastra

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mastra@2277

@ag-ui/pydantic-ai

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/pydantic-ai@2277

@ag-ui/vercel-ai-sdk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/vercel-ai-sdk@2277

@ag-ui/watsonx

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/watsonx@2277

@ag-ui/a2ui-toolkit

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2ui-toolkit@2277

create-ag-ui-app

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/create-ag-ui-app@2277

@ag-ui/client

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/client@2277

@ag-ui/core

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/core@2277

@ag-ui/encoder

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/encoder@2277

@ag-ui/proto

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/proto@2277

commit: 9eb3714

mme and others added 7 commits July 30, 2026 11:06
Two defects in the built-in tool path, both user-visible.

TOOL_CALL_ARGS deltas are concatenated by the client
(`function.arguments += delta`), so everything emitted for one call has to
join into exactly one JSON document. The args were diffed as strings on the
assumption that a growing dict is a prefix of its larger self -- it is not,
the closing brace moves -- so any change re-sent the whole dict and the
client's buffer became `{...}{...}`. For a built-in that is worse than
unparseable: the harness grows the args with the tool's *result* at DONE, so
the result was also leaking into the arguments. Args are now sent once, and
a genuine append-only extension is still sent as a suffix.

ToolCallResultEvent has no error field, so a failed built-in arrived as
content="" -- byte-identical to a tool that legitimately produced no output,
which `view_file` does. The client rendered a failure as a success. Failures
now say so, and so does "completed without returning any output".

Neither was caught by the review loop: verify.ts checks event ordering, not
whether concatenated args parse.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sessions took a Go localharness subprocess each, which was believed to cost
~93 MB per concurrent user. It does not: Antigravity configures a harness
twice, and almost everything that varies per thread is in the second half.

  InputConfig    process stdin at startup   save_dir, env
  HarnessConfig  WebSocket, per conversation  tools, model, capabilities,
                 MCP servers, hooks, subagents, response_schema,
                 conversation_id -- and workspaces

So conversations agreeing on (binary_path, save_dir, env) can share a process.
Measured over 8 concurrent conversations: 101 MB pooled against 752 MB idle,
154 MB against 1040 MB mid-turn, with wall clock unchanged. An extra idle
conversation costs ~1 MB rather than ~95 MB.

Pooling lives in a ConnectionStrategy, which the SDK documents as owning
"process management, transport setup, authentication, and health checking".
PooledStrategy wraps the strategy the SDK builds and replaces only
__aenter__/__aexit__; Agent, Conversation, LocalConnection, the event processor
and tool/hook dispatch are all used unmodified -- including Agent's mandatory
safety guard, which a reimplementation could have dropped. It wraps rather than
subclasses because create_strategy builds its strategy inline with fifteen
keyword arguments, and CPython rejects __class__ assignment between these
classes.

Also fixes a pre-existing cold-resume bug found on the way: save_dir defaulted
to a fresh tempfile.mkdtemp() per config, so a rebuilt session resumed against
a brand-new empty directory. One save directory per adapter now.

HarnessPool defines __deepcopy__/__copy__ returning self. Agent.__init__ runs
config.model_copy(deep=True), which otherwise cloned the pool and silently gave
every Agent its own process.

Gates, both passed and kept as tests: a SIGKILLed harness makes its
conversations raise rather than hang, and a conversation parked on a human does
not block its co-tenants. A 20s tool call was measured not to delay neighbours
(median inflation 0.95x against a control). max_conversations_per_process=1
restores one process per thread.

276 tests green (267 unit, 9 live), no leaked processes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"One subprocess with real filesystem and shell access per thread_id" stopped
being true with pooling. Documents the InputConfig/HarnessConfig split, the
measured memory table, and the three caveats that matter operationally: blast
radius, parked-session fragmentation, and save_dir not being a tenancy
boundary. Notes that workspaces stays per-conversation, so the sandboxing
advice is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each AntigravityAgent owns a HarnessPool by default, so the demo server was
starting one harness process per demo *agent* -- browser testing showed two
processes for two dojo tabs, which undercuts the point of pooling.

Nothing about an agent's config prevents sharing: tools, instructions, model
and workspaces all ride in HarnessConfig, per conversation. Only save_dir and
env are fixed at process start, so the demos now pin both and draw from one
pool. Four demo agents, one process.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each AntigravityAgent owns a pool, so a server hosting several gets a harness
process each -- a ~95 MB floor per agent. Harmless for correctness and it does
not affect per-user scaling, but it is silent, and the obvious fix does not
work: passing harness_pool= alone changes nothing, because each agent mints its
own tempfile.mkdtemp() save_dir and save_dir is half the partition key. Both
are needed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A custom Python tool never produced a TOOL_CALL_RESULT, so any client
rendering one waited forever. The dojo's backend_tool_rendering demo showed it
plainly: the page registers a useRenderTool for get_weather and draws a card
from the result, and the card could never leave its loading state.

The cause is that the harness reports a custom tool as a single
TOOL_CALL/ACTIVE step -- no DONE step, and no result field anywhere on Step,
because the SDK's own ToolRunner executes the tool and the return value goes
back over the WebSocket straight to the model. The translator builds results by
diffing tool-call args, which is how the harness reports BUILT-IN tools, and
that diff is empty here.

This process runs the tool, though, so the value is in hand. UIBridge now wraps
each server-side tool to emit TOOL_CALL_START/ARGS/END and a TOOL_CALL_RESULT
carrying the real return value, and those names are suppressed in the
translator so the step-driven events are not emitted twice. The wrapper keeps
the function's signature and docstring via functools.wraps, so the SDK still
derives the same tool schema -- a bare **kwargs wrapper would have erased the
parameters.

Gives the backend_tool_rendering demo a real get_weather tool, following the
convention the llama-index demo already uses: keyless open-meteo, with
AG_UI_MOCK_WEATHER for offline and CI runs, and a fallback to canned data if
the lookup fails so the demo never renders a broken card.

Verified in the browser: Tokyo and San Francisco both render full weather cards
with live data, correct icons and theming, across two turns of one thread.

283 tests green (267 unit, 16 live).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The harness hands over a tool call fully formed in a single step -- measured
with a custom tool whose argument was 1850 characters, still one TOOL_CALL
step. So TOOL_CALL_ARGS arrives as one delta rather than filling in
progressively, and there is nothing the adapter can do about it: there are no
fragments to forward. The call lifecycle does stream, which is what lets a
client show a pending tool card while the tool runs.

Also widens the known-gaps heading to 0.1.8-0.1.9, the range actually tested.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mme
mme force-pushed the mme/antigravity branch from f5a6557 to d7ec7b8 Compare July 30, 2026 15:50
description = "Example usage of the Antigravity integration with FastAPI"
license = "MIT"

readme = "README.md"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file does not appear to exist at this level, so "uv sync" will fail.

…ADME

Three review findings:

* `examples/pyproject.toml` declares `readme = "README.md"` but the file did
  not exist, so the examples package failed to build -- confirmed by building
  with and without it: hatchling.build.build_sdist exits 1. Adds the README,
  covering setup on both the Gemini and OpenAI paths, the endpoint list, and
  the pooling/workspace/open-meteo notes a reader needs.

* `check-config-files`: `integrations/antigravity/typescript/tsdown.config.ts`
  was not on the build-config allowlist. Added, alphabetically.

* `dojo / check-generated-files`: `apps/dojo/src/files.json` was stale after the
  demo-server changes. Regenerated with `pnpm generate-content-json`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mme
mme requested a review from AlemTuzlak as a code owner July 31, 2026 08:47
mme and others added 4 commits July 31, 2026 13:21
The README said a parked session "is never evicted by the idle sweeper".
_expired() does the opposite of an exemption -- its own docstring says so --
giving parked sessions max(parked_timeout, session_timeout), i.e. 2 h rather
than 30 min. Only a run in flight is truly never reclaimed, because it holds
the session lock.

Also states the thing the old wording implied backwards: a session holds memory
for its whole life, not only while parked. Parking does not allocate; it
extends how long the allocation is held.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
persistence.md documents Antigravity's answer to "come back later" as closing
the agent and reopening it with the same conversation_id and save_dir. The
adapter does exactly that whenever the client's tool set changes, but nothing
tested it against a real harness: the unit tests only prove the config carries
the right fields, and the pooled test only proves a strategy rehydrates
history. That gap is how the save_dir bug -- every session getting its own
tempfile.mkdtemp(), so resume restored nothing -- survived until it was found
by hand.

The new test drives two runs on one thread with a changed tool set, then
asserts the rebuild resumed the same conversation and the model still recalls
what it was told. Verified RED against both the unstable-save_dir bug and a
dropped conversation_id.

Also documents that AntigravitySession.conversation_id is always None -- it is
snapshotted before any message is exchanged, and _close_locked works via its
fallback to the live agent. That field misled the first version of this test.

Separately, test_builtin_tool_calls_are_reported is intermittently red: the run
sometimes ends in RUN_ERROR partway through a built-in tool call. Reproduced at
a similar rate on the pre-pooling commit b3f0402, so it is upstream, not a
regression here. Now asserted explicitly so the failure names that cause rather
than surfacing as a confusing missing-result count.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
test_builtin_tool_calls_are_reported failed roughly one run in four: the run
intermittently ends in RUN_ERROR partway through a built-in tool call, leaving
the call bookended but unresolved. Reproduced at a similar rate on the
pre-pooling commit b3f0402, so it is upstream in the harness or the model
rather than a regression here, and it is not root-caused -- twelve isolated
repro runs came back clean.

Removed rather than left red. The translation logic it covered is still tested
in test_event_translator.py: keys added at DONE becoming the result, a tool
that surfaces no result still closing, several keys merged into one JSON
result, a call flushed at a run boundary still delivering its result, and a
reported error being named in the result. What is lost is only the end-to-end
check against a real harness.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…pace paths

The intermittent failure was not a rate limit or a 5xx; every upstream call
returned 200. The model was garbling the workspace path when copying it into a
tool call. Two captured failures show it plainly:

  wanted  .../pq2_7rn97834lcsvc_qy4t8r0000gn/T/hunt-xz27wm6q/greeting.txt
  sent    .../pq2_7rn97834lcsvc_qy4kksh/greeting.txt            (truncated)
  sent    .../T/hunt-xz2_7rn97834lcsvc_qy4t8r0000gn/T/hunt-xz27wm6q/...  (chunk repeated)

macOS temp directories are ~75 characters of high-entropy text, about the worst
case for a model asked to reproduce one verbatim. The harness then treats the
bad path as a fatal AntigravityExecutionError instead of returning it for a
retry, so the run dies mid-tool-call with "The model produced an invalid tool
call" -- a message that points nowhere near path length.

Measured, same prompt and model: 0/14 runs failed with a 9-character workspace
path, 2/14 with a 75-character one.

Restores test_builtin_tool_calls_are_reported on a short workspace fixture,
stable across 8 consecutive runs where it previously failed roughly 1 in 4, and
documents the trap under "Keep workspace paths short" so users hosting the
integration do not hit it in production.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants