CodeAssist

Agentic coding

An in-IDE AI coding assistant: a chat surface backed by a tool-using agent that reads, searches, and edits the open project. Built entirely on the existing plugin substrate (plugin-api / BuiltInPlugins), the scoped-service container, and the IdeBackend port, so it adds no privileged host wiring and can be enabled or disabled like any other built-in plugin.

The agent is CodeAssist’s own agent. It is grounded as this IDE (on-device, interpreter-based runs, no hosted Gradle, ART constraints), never as another product. Users bring their own API key for one of several providers; nothing is bundled or proxied.

Goals and non-goals

Goals (first increment):

Deferred (tracked, not built yet):

Module layout

Two new pure-Kotlin/JVM modules, engine-agnostic so they compile and test under CI_CORE_ONLY (no Android SDK), plus additive surfaces on the existing UI port and host.

agent-api   (dev.ide.agent)        no engine deps; coroutines only
  ^
agent-impl  (dev.ide.agent.impl)   OkHttp + okhttp-sse + kotlinx-serialization-json + platform-core
  ^
ide-core    (dev.ide.core)         implements AgentWorkspace, hosts AgentBackend + AgentPlugin
ide-ui-api  (dev.ide.ui.backend)   adds AgentService port + Ui* DTOs
ide-ui      (dev.ide.ui)           the ChatDrawer + composables

agent-api and agent-impl join the unconditional framework list in settings.gradle.kts; ide-ui-api / ide-core / ide-ui remain in the Android-shell-gated block.

agent-api

Provider-neutral contracts, extensible by third-party plugins:

agent-impl

Provider abstraction

LlmClient.chat(request): Flow<LlmStreamEvent> is the one streaming entry point. LlmRequest carries the model id, system prompt, message history, tool specs, and generation controls (the loop sets streaming and, for capable models, adaptive thinking). Each provider owns:

LlmRequest also carries an optional thinkingBudget (a cap on provider reasoning tokens; null leaves the model default) — only providers that expose a reasoning budget honor it.

Default model per provider is the strongest current model; the model is user-selectable in Settings. Adding a provider is implementing LlmProvider; the client resolves providers through a registry, so a plugin can contribute its own.

Antigravity gateway (experimental, opt-in)

AntigravityProvider reaches Google’s Code Assist backend (cloudcode-pa.googleapis.com/v1internal:streamGenerateContent) with an OAuth Bearer token instead of an API key, giving access to Gemini 3 / Claude / GPT-OSS at Antigravity’s rate limits. It speaks the same Gemini contents/parts dialect as GeminiProvider — both now share the request builders in GeminiWire and the GeminiStreamDecoder — but wraps the call in a {project, model, request, userAgent, requestId} envelope, nests each streamed candidate under response, and sends the Antigravity IDE’s identity headers (User-Agent / X-Goog-Api-Client / Client-Metadata). Reasoning level is carried in the model id (…-high / …-low / …-thinking), so no thinkingConfig is sent.

The credential (ProviderConfig.apiKey) is an OAuth refresh token (starts with 1//, exchanged for short-lived access tokens here via postForm to oauth2.googleapis.com/token, cached until near expiry) or a raw access token, optionally suffixed with an explicit project id: <token> or <token>|<projectId>. With no project id, the free-tier project is discovered via loadCodeAssist (falling back to onboardUser). AntigravitySession holds this token + project state per client session behind a mutex.

Sign-in flow (AntigravityOAuth, mobile + desktop): the provider card’s “Sign in with Google” button runs an OAuth authorization-code + PKCE flow that mints the refresh token. Because the reproduced client only registers a loopback redirect (http://localhost:36742/oauth-callback), the flow stands up a one-shot local HTTP listener on that fixed port (java.net sockets — identical on ART and the JVM), surfaces the consent URL through AgentService.antigravitySignIn for the UI to open in the platform browser (a Custom Tab on Android via LocalUriHandler; the default browser on desktop), waits for the browser to redirect back to the listener, validates the CSRF state, exchanges the code, and stores the refresh token as the antigravity key. Cancellation tears the listener down; a five-minute timeout guards an abandoned consent screen. Pasting a token directly into the card still works as a fallback.

This is off by default and warned in the provider sheet: it talks to an undocumented internal endpoint by impersonating the Antigravity IDE’s OAuth client, violates Google’s Terms of Service, and has led to account bans. It is a best-effort, fragile integration (the endpoint and client identity can change without notice), not a supported path.

Request cost and error categorization

An agentic turn re-sends the growing history on every step, so an agent is unusually hard on token- and request-metered tiers (a single free-tier Gemini task can exhaust the per-minute or per-day cap). Four mechanisms keep the cost down and the failures legible:

LlmErrors categorizes an HTTP or in-stream error into an LlmErrorKind so the transport knows whether a retry helps and the chat shows something actionable. A subtlety worth preserving: a Gemini free-tier rate limit is an HTTP 429 RESOURCE_EXHAUSTED that reuses the same “you exceeded your current quota / billing details” wording a truly-exhausted paid quota uses, but it is transient and carries a short RetryInfo delay. The classifier therefore treats a 429 / resource_exhausted as a retryable RATE_LIMIT and reserves the non-retryable QUOTA verdict for narrow true-billing signals (insufficient_quota, spent credit balance) — so a per-minute limit auto-retries instead of surfacing a dead-end “billing exhausted” message.

Quotas, free tiers and pacing

Why a fresh Gemini key used to fail on its first message: the default model was gemini-2.5-pro, and Google took 2.5 Pro off the free tier entirely (its free limit is 0) and later closed the 2.5 family to new projects. Three changes follow from that.

Request correctness on Gemini 3: it takes thinkingConfig.thinkingLevel and rejects thinkingBudget (2.5 is the reverse), so the builder sends whichever the model’s generation takes. maxOutputTokens caps thinking and answer together, so the reasoning allowance is added on top of maxTokens. Search grounding next to function declarations is a Gemini 3 capability; on 2.x it is only sent on a request with no client tools.

Tools and the engine seam

Tool implementations call the project only through AgentWorkspace, which ide-core implements over EngineContext / IdeServices, running every call on the engine dispatcher lanes so the index, analyzers, and synthetic classes stay consistent.

Read tools: read_file (overlay-preferred, optional line range), list_dir, search_text (findInFiles), find_symbol (searchSymbols / searchMembers), get_diagnostics (per-file, the merged compiler + analyzer stream, used for self-checking), project_overview (modules, source sets, dependencies, facets).

Write tools (permission-gated): create_file, edit_file, create_dir, rename_path, move_path, delete_path, add_dependency, edit_module_config.

Build/run tool (permission-gated): run_program compiles a module and runs its main on the in-process VM via IdeServices.runAndCapture (headless — it does not touch the interactive run console), feeding optional stdin then EOF and returning output + exit code + compile errors. This closes the agent’s edit → run → read-failure → fix loop; it is time-limited so a blocked or long run can’t stall a turn.

Two engine additions fill the one gap (there was no public disk-persisting multi-file edit):

Permission model

A per-project AgentPermissionPolicy mirrors the run sandbox’s PermissionPolicy (.platform/agent-permissions.properties). Modes:

Read tools never prompt. The mode is set from the chat drawer header and persists per project.

Settings, keys, and secrets

An “AI” SettingsPage (SETTINGS_PAGE_EP, APPLICATION scope) exposes provider selection, per-provider API key and optional base URL, model selection, and a “Test connection” action. Keys are read and written through a SecretStore seam. The default implementation persists to the existing prefs.properties store (plaintext, matching the current keystore-password posture); an encrypted-at-rest implementation (Android EncryptedSharedPreferences, desktop keychain) is a later drop-in behind the same interface. The key field renders masked.

Backup keys

Each provider, the custom gateway included, takes any number of backup keys after its main key (settings.ai.<keyField>.backups, one per line; edited in the key manager). AgentBackend wraps the provider’s client in a KeyFailoverClient over an ApiKeyPool (agent-impl KeyFailover.kt):

Chat UI

AgentService (added to IdeBackend) exposes chatState: StateFlow<AgentChatState>, send, stop, newSession, the permission-request flow, and provider/model configuration reads. The UI collects the StateFlow and recomposes, exactly like the build console.

ChatDrawer is a right-edge sliding drawer (the previously-unused RIGHT tool-window anchor on desktop; a from-end PushDrawer on mobile), toggled from the editor top bar. It is surface-agnostic (no background of its own), mirroring BuildConsole. Rendering reuses parseMarkdown / highlight / CodeSample for messages and code, a StepRow-style status row per tool call, and the RunScreen InputBar pattern for the composer. The visual language is a futuristic, Gemini-style treatment over the design tokens: glass message surfaces, a gradient sparkle accent, a shimmer thinking indicator, a glowing pill composer, and a token-by-token streaming reveal. All user-facing strings are chat_* keys in strings.xml.

Attachments, mentions, plans, sessions and undo

System prompt

SystemPrompt grounds the agent as CodeAssist and states the platform’s real shape and limits: an on-device Android/Java IDE, program runs execute by interpreting bytecode on the in-process VM (not a forked JVM), the build system is native (no hosted Gradle), and the runtime is ART (single-threaded VM for user code, no invokedynamic bootstrap, minSdk floor). It lists the available tools and the active permission mode, then appends live project context (modules, the active file, current diagnostics), kept after the stable prefix so the grounding stays cache-friendly.

Extensibility

Testing

agent-impl runs offline in CI_CORE_ONLY: provider adapters are tested against recorded SSE fixtures through a fake LlmTransport, and the agent loop and tools against a fake AgentWorkspace. The chat UI is verified on desktop with a headless Compose snapshot.