Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 62 additions & 4 deletions docs/PLUGIN_API_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,72 @@ Versions are bare `YY.WW` — two-digit ISO year, two-digit ISO week (`26.30` =

## Changelog

Newest first. Every change so far is **additive** — no capability has been
removed or had its signature broken since the plugin system shipped. A future
breaking change belongs here as a `breaking` row.
Newest first. Most changes are **additive**; the ones that are not carry a
`breaking` row saying what breaks and what to do about it. Read the `breaking`
rows at or below your `min_ide_version` before you bump it.

Legend: `added` = new capability, safe to adopt · `tooling` = API-stability
Legend: `added` = new capability, safe to adopt · `breaking` = existing plugins
need a source change, a recompile, or both · `tooling` = API-stability
milestone. **[verified]** = read from the checked-in ABI dump. **[reconstructed]**
= diffed from `plugin-api/src` history (predates the dump; symbol-accurate).

### 26.33 — 2026-08-12
- **added — Optional LLM backend capabilities** _(ADFA-5095)_ **[verified]**
An LLM backend declares what it supports by the interfaces it implements, so a
backend can ship as its own plugin and implement only what it can do. The
consumer asks with `instanceof` before it calls; a backend that implements none
of these is still a valid `LlmBackend`.
`LlmInferenceService.HistoryCapableBackend` (`generateStreamingWithHistory`),
`ToolCallingBackend` (`generateStreamingWithTools`),
`CancellableBackend` (`cancelStreaming`),
`ConfigurableBackend` (`getSettingsFragmentClassName` — the backend's own
settings `Fragment`, loaded with the backend's classloader).
- **added — Backend-owned prompt and sampling** _(ADFA-5095)_ **[verified]**
A backend supplies the system prompt and temperature its model needs, instead of
the consumer hardcoding them per provider. Both are `default` and return null
for "no preference"; `getDefaultTemperature()` is a boxed `Float`, so null-check
before assigning it to the primitive `LlmConfig.temperature`.
`LlmBackend.getSystemPrompt(SystemPromptRequest)`,
`LlmBackend.getDefaultTemperature()`, `SystemPromptRequest`.
- **breaking — Tool results correlated by call id and tool name** _(ADFA-5095)_ **[verified]**
A tool's output travels back into the next turn as a message of its own, so a
turn's several calls are matched by correlator rather than by position. Both
correlators travel with the result because providers key results differently —
by call id, or by function name — and a backend can only forward what it was
given.
`ChatMessage.toolResult(String, String, String)`, `ChatMessage.toolCallId` /
`toolName`, `ChatMessage.Role.TOOL`.
**What breaks:** `Role` gains a fourth constant, so an exhaustive Kotlin `when`
over it with no `else` stops compiling. A plugin already built against the
three-constant enum has the worse failure: the `when` throws
`NoWhenBranchMatchedException` with a null message, which reads as an
unattributable crash inside the plugin rather than as anything to do with
`Role`. A `TOOL` message reaches a backend that never calls `toolResult` — the
consumer builds it and passes it in the history — so handling it is not
optional for backends. **What to do:** add a `TOOL` branch (routing it as a
user turn is fine for a backend with no native function calling) and republish;
a `.cgp` that is only reinstalled, not rebuilt, stays exposed.
- **added — Preferred backend id** _(ADFA-5095)_ **[verified]**
A backend can ask which backend the user selected, so one that would otherwise
spend seconds and gigabytes preparing itself knows whether it is about to be
used — without reading another plugin's preferences.
`LlmInferenceService.getPreferredBackendId()` (`default`, null when unset).
- **breaking — Nullability annotated across the LLM surface** _(ADFA-5095)_
Every parameter, return and field on `LlmInferenceService` and the types nested
in it now carries `@NonNull` or `@Nullable`, so the contract is stated rather
than inferred.
**What breaks:** an unannotated Java type reaches Kotlin as a platform type
(`String!`) that dereferences without a check; annotated `@Nullable` it becomes
`String?`, and every existing dereference stops compiling with "only safe (?.)
or non-null asserted (!!.) calls are allowed". This hits **callers**, not just
implementors — `LlmResponse.text` / `.error`, `ToolCallRequest.args` and
`ToolDefinition.parametersSchema` are the ones consumers touch, and
`@NonNull` across `LlmBackend` tightens what an implementor may return.
Bytecode is unchanged, so an installed `.cgp` keeps running; the break is at
compile time in the plugin repo. **What to do:** `?.`, `.orEmpty()` or an
explicit null check at each site — the annotations describe values the API
could already return.

### 26.31 — 2026-07-29
- **tooling — Plugin API & builder resolvable by Maven coordinate on-device** _(ADFA-4911)_
The plugin API and the builder Gradle plugin are injected into the on-device
Expand Down
6 changes: 4 additions & 2 deletions docs/plugin-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The surface a plugin binds to is broader than one module. All of the following a
- Core: `IPlugin` (lifecycle), `PluginContext`, `PluginLogger`, `ServiceRegistry`, `ResourceManager`.
- Extension interfaces plugins **implement**: `UIExtension`, `EditorExtension`, `EditorTabExtension`, `DocumentationExtension`, `BuildActionExtension`, `SnippetExtension`, `ProjectExtension`, `FileOpenExtension`, `SettingsExtension`.
- IDE service interfaces plugins **call** (via `ServiceRegistry.get(X::class.java)`): `IdeProjectService`, `IdeEditorService`, `IdeFileService`, `IdeEnvironmentService`, `IdeArchiveService`, `IdeBuildService`, `IdeUIService`, `IdeEditorTabService`, `IdeTooltipService`, `IdeThemeService`, `IdeFeatureFlagService`, `IdeCommandService`, `IdeTemplateService`, `IdeSnippetService`, `IdeSidebarService`.
- Cross-plugin service interfaces, where **one plugin implements what another calls** (via `SharedServices`): `LlmInferenceService` — implemented by ai-core, called by every AI plugin — together with the types nested in it that a *backend* plugin implements (`LlmBackend`, `HistoryCapableBackend`, `ToolCallingBackend`, `CancellableBackend`, `ConfigurableBackend`) and the value types either side constructs (`ChatMessage`, `LlmConfig`, `LlmResponse`, `SystemPromptRequest`, `ToolDefinition`, `ToolCallRequest`).
- Data classes plugins **construct** (e.g. `MenuItem`, `TabItem`, `EditorTabItem`, `NavigationItem`, `ToolbarAction`, `FabAction`, `PluginBuildAction`, `SnippetContribution`, `PluginTooltipEntry`, `PluginSettingsEntry`).
- Enums / sealed types plugins **reference**: `PluginPermission`, `ShowAsAction`, `ArchiveFormat`, `BuildActionCategory`, `ToolbarActionIds`, `CommandSpec`, `CommandResult`, `ExtractResult`.
- **Wire/format contracts outside the module:**
Expand All @@ -35,9 +36,10 @@ When the API is later frozen, this doc gains a formal compatibility guarantee an
These look source-compatible but break already-built `.cgp` plugins:

- **Data-class constructor parameters.** Adding a parameter *even with a default value* changes the synthetic constructor and `copy()` signatures — binary-incompatible for any plugin that constructs or copies the class (`MenuItem`, `PluginBuildAction`, `SnippetContribution`, …). If compatibility matters, add a secondary constructor or a builder instead.
- **Interface methods — direction matters.**
- **Interface methods — direction matters.** Ask who implements the interface before you apply a rule; the answer is not "host" just because the name ends in `Service`.
- *Extension interfaces* (`UIExtension`, `BuildActionExtension`, …) are implemented **by plugins**: adding a method is breaking for them (even a defaulted one can break depending on compilation). Provide defaults and prefer additive optional hooks.
- *Service interfaces* (`Ide*Service`) are implemented **by the host** and only called by plugins: **adding** a method is safe; changing or removing a signature is breaking.
- *Host service interfaces* (`Ide*Service`) are implemented **by the host** and only called by plugins: **adding** a method is safe; changing or removing a signature is breaking.
- *Plugin-implemented service interfaces* (`LlmInferenceService` and the backend interfaces nested in it) are implemented **by a plugin** even though they are shaped like services. The extension-interface rule applies, not the host-service one: **adding** a method is breaking. A Kotlin implementor's existing method loses its `override` when a Java `default` appears above it, so the break is a compile error in the *other* repo — which the impact check below is what catches. Prefer a new interface extending the old one over a new method on it.
- **Enum constants.** Removing or renaming a constant (`PluginPermission`, `ShowAsAction`, `ArchiveFormat`, `ToolbarActionIds`, `BuildActionCategory`) breaks plugins that name it; adding one can still break an exhaustive `when`.
- **Types & nullability.** Flipping nullable↔non-null, changing a parameter/return type, or `val`↔`var` on an API property.
- **Moving or renaming** any class/package under `com.itsaky.androidide.plugins.*` — breaks imports and `ServiceRegistry.get(...)` lookups.
Expand Down
30 changes: 30 additions & 0 deletions plugin-api/api/plugin-api.api
Original file line number Diff line number Diff line change
Expand Up @@ -1508,31 +1508,50 @@ public abstract interface class com/itsaky/androidide/plugins/services/LlmInfere
public abstract fun getAvailableBackends ()Ljava/util/List;
public abstract fun getBackend (Ljava/lang/String;)Lcom/itsaky/androidide/plugins/services/LlmInferenceService$LlmBackend;
public abstract fun getEmbeddings (Ljava/lang/String;Ljava/lang/String;)Ljava/util/concurrent/CompletableFuture;
public fun getPreferredBackendId ()Ljava/lang/String;
public abstract fun isBackendAvailable (Ljava/lang/String;)Z
public abstract fun registerBackend (Lcom/itsaky/androidide/plugins/services/LlmInferenceService$LlmBackend;)V
public abstract fun unregisterBackend (Ljava/lang/String;)V
}

public abstract interface class com/itsaky/androidide/plugins/services/LlmInferenceService$CancellableBackend : com/itsaky/androidide/plugins/services/LlmInferenceService$LlmBackend {
public abstract fun cancelStreaming ()V
}

public class com/itsaky/androidide/plugins/services/LlmInferenceService$ChatMessage {
public final field content Ljava/lang/String;
public final field role Lcom/itsaky/androidide/plugins/services/LlmInferenceService$ChatMessage$Role;
public final field toolCallId Ljava/lang/String;
public final field toolName Ljava/lang/String;
public fun <init> (Lcom/itsaky/androidide/plugins/services/LlmInferenceService$ChatMessage$Role;Ljava/lang/String;)V
public static fun toolResult (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/itsaky/androidide/plugins/services/LlmInferenceService$ChatMessage;
}

public final class com/itsaky/androidide/plugins/services/LlmInferenceService$ChatMessage$Role : java/lang/Enum {
public static final field ASSISTANT Lcom/itsaky/androidide/plugins/services/LlmInferenceService$ChatMessage$Role;
public static final field SYSTEM Lcom/itsaky/androidide/plugins/services/LlmInferenceService$ChatMessage$Role;
public static final field TOOL Lcom/itsaky/androidide/plugins/services/LlmInferenceService$ChatMessage$Role;
public static final field USER Lcom/itsaky/androidide/plugins/services/LlmInferenceService$ChatMessage$Role;
public static fun valueOf (Ljava/lang/String;)Lcom/itsaky/androidide/plugins/services/LlmInferenceService$ChatMessage$Role;
public static fun values ()[Lcom/itsaky/androidide/plugins/services/LlmInferenceService$ChatMessage$Role;
}

public abstract interface class com/itsaky/androidide/plugins/services/LlmInferenceService$ConfigurableBackend : com/itsaky/androidide/plugins/services/LlmInferenceService$LlmBackend {
public abstract fun getSettingsFragmentClassName ()Ljava/lang/String;
}

public abstract interface class com/itsaky/androidide/plugins/services/LlmInferenceService$HistoryCapableBackend : com/itsaky/androidide/plugins/services/LlmInferenceService$LlmBackend {
public abstract fun generateStreamingWithHistory (Ljava/util/List;Ljava/lang/String;Lcom/itsaky/androidide/plugins/services/LlmInferenceService$LlmConfig;Lcom/itsaky/androidide/plugins/services/LlmInferenceService$StreamCallback;)V
}

public abstract interface class com/itsaky/androidide/plugins/services/LlmInferenceService$LlmBackend {
public abstract fun generate (Ljava/lang/String;Lcom/itsaky/androidide/plugins/services/LlmInferenceService$LlmConfig;)Ljava/util/concurrent/CompletableFuture;
public abstract fun generateStreaming (Ljava/lang/String;Lcom/itsaky/androidide/plugins/services/LlmInferenceService$LlmConfig;Lcom/itsaky/androidide/plugins/services/LlmInferenceService$StreamCallback;)V
public abstract fun generateWithHistory (Ljava/util/List;Ljava/lang/String;Lcom/itsaky/androidide/plugins/services/LlmInferenceService$LlmConfig;)Ljava/util/concurrent/CompletableFuture;
public fun getDefaultTemperature ()Ljava/lang/Float;
public abstract fun getId ()Ljava/lang/String;
public abstract fun getName ()Ljava/lang/String;
public fun getSystemPrompt (Lcom/itsaky/androidide/plugins/services/LlmInferenceService$SystemPromptRequest;)Ljava/lang/String;
public abstract fun isAvailable ()Z
}

Expand Down Expand Up @@ -1564,13 +1583,24 @@ public abstract interface class com/itsaky/androidide/plugins/services/LlmInfere
public abstract fun onToken (Ljava/lang/String;)V
}

public class com/itsaky/androidide/plugins/services/LlmInferenceService$SystemPromptRequest {
public final field exampleFilePath Ljava/lang/String;
public final field toolCallSyntax Ljava/lang/String;
public final field tools Ljava/util/List;
public fun <init> (Ljava/util/List;Ljava/lang/String;Ljava/lang/String;)V
}

public class com/itsaky/androidide/plugins/services/LlmInferenceService$ToolCallRequest {
public field args Ljava/util/Map;
public field callId Ljava/lang/String;
public field name Ljava/lang/String;
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V
}

public abstract interface class com/itsaky/androidide/plugins/services/LlmInferenceService$ToolCallingBackend : com/itsaky/androidide/plugins/services/LlmInferenceService$LlmBackend {
public abstract fun generateStreamingWithTools (Ljava/lang/String;Ljava/util/List;Lcom/itsaky/androidide/plugins/services/LlmInferenceService$LlmConfig;Ljava/util/List;Lcom/itsaky/androidide/plugins/services/LlmInferenceService$ToolStreamCallback;)V
}

public class com/itsaky/androidide/plugins/services/LlmInferenceService$ToolDefinition {
public field description Ljava/lang/String;
public field name Ljava/lang/String;
Expand Down
Loading
Loading