Skip to content
Merged
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