KotlinLLM Turns LLM Answers into Permanent Source Code at Runtime
JetBrains has actually come up with something interesting this time. The basic idea is:
Why should an application call an LLM repeatedly for every request? Why can't a model generate code that can be reviewed, tested, and continuously executed the first time it encounters a new problem? Why not turn model output from a temporary answer into permanent source code, and turn runtime calls into one-time code evolution?
Hey, isn't that interesting? It's thinking about turning an LLM from "an online service called every time it runs" into something that can only intervene when a program encounters an unfamiliar scenario, and then solidify the answer into ordinary Kotlin source code:
The first time a program encounters data it can't process, let the LLM write a piece of Kotlin code on the spot, compile it, and hot-replace it in. When it encounters the same type of data again later, it just executes this code directly, without needing to call the LLM again.
JetBrains calls this mechanism Smart macros. Currently, KotlinLLM provides two core entry points:
asLlm<F, T>(from, hint)
mockLlm<T>()
Here, asLlm is mainly responsible for converting input into an explicit Kotlin type, and mockLlm gradually generates a stateful interface implementation based on the interface definition and the actual calling process.
And JetBrains summarizes these concepts into three characteristics: Explicit, Persistent, and Portable:
Explicit means that
asLlm()ormockLlm()will explicitly appear in the code, so a developer can tell at a glance that a certain piece of logic was written by an LLM.Persistent means that the generated results are saved as ordinary Kotlin files, which can be committed to Git, participate in code reviews, run unit tests, and be further modified by subsequent developers.
Portable means that once the code for a relevant scenario has been generated, the application can run just like ordinary Kotlin. Scenarios already covered no longer require a plugin or model for inference.
So the core capability of KotlinLLM here is asLlm<F, T>(from, hint). The API is very simple; it appears to accept an input of type F and return an explicit Kotlin type T, for example:
val issuesApiUrl: String = asLlm(
repoInput,
hint = "GitHub API URL: get all issues, including closed"
)
On the surface, this looks a lot like asking a model to complete a conversion based on a prompt, but in reality, after the actual generation is complete, asLlm() will look up the corresponding AsLlmParser<F, T> at runtime:
If the current input matches an already-learned scenario, it directly executes the already-generated Kotlin implementation. Only when the input does not match any existing scenario will the system request the LLM again to generate a supplementary piece of logic for this new scenario.
The asLlm() in the source code looks up a registered parser based on the input and output types, then calls its parse() method. It only reports an error or enters the generation process provided by the plugin when a parser cannot be found.
For example, a program needs to process JSON from different sources and convert them into a unified Kotlin data class. The traditional approach is usually to write a parser that is as generic as possible in advance:
data class Issue(
val title: String,
val url: String,
val labels: List<String>
)
But in real situations, JSON structures can vary greatly, requiring many data classes to be written:
{
"title": "Fix documentation typo",
"html_url": "https://github.com/...",
"labels": [
{"name": "good first issue"}
]
}
{
"name": "Fix documentation typo",
"url": "https://github.com/...",
"tags": ["beginner"]
}
But if using KotlinLLM, the first time the App encounters a certain JSON format, the LLM will observe the actual input, the constructor of the target data class, and the field relationships, then generate the corresponding parsing branch.
At this point, when encountering data with the same structure again, the program does not need the model's participation and can directly run this parsing branch.
So this is similar to a very special kind of "learning"?? For a specific scenario, the program learns the execution of a new scenario, which actually means the project itself gains an extra piece of Kotlin source code that can be continuously executed.
And the most critical design of KotlinLLM is that each time it encounters an unfamiliar input, the model can only submit two parts:
guardExpression
caseHandlerBody
Where guardExpression is responsible for determining whether the current input belongs to a certain already-understood scenario, and caseHandlerBody is responsible for handling this scenario. The final generated logic can be roughly understood as follows:
fun parse(input: String): Issue {
return when {
matchesScenarioA(input) -> handleScenarioA(input)
matchesScenarioB(input) -> handleScenarioB(input)
matchesScenarioC(input) -> handleScenarioC(input)
else -> regenerateForNewScenario(input)
}
}
For example, the first time a program sees JSON like this:
{
"title": "Fix documentation typo",
"labels": [{"name": "good first issue"}]
}
The model needs to generate a processing logic, but cannot simply write:
input.isNotEmpty()
Because this guard is too broad. This way, as long as the input is not an empty string in the future, regardless of whether it is a GitHub Issue inside, the program will enter this processing branch without any logic.
So KotlinLLM's system prompt repeatedly emphasizes:
A Guard is not a simple validity check, but a scenario classifier.
The model needs to check the facts that the handler truly depends on, such as:
- Whether the input has a specific structure
- Whether required fields exist
- Whether fields are empty
- The number of array elements
- Status or tag values
- Relationships between fields
- Enumeration strings
- Numerical ranges and specific format markers
If the handler input must have
titleandlabels, then the guard must prove these two fields exist; and if the handler assumeslabelsis an array of objects, not an array of strings, the guard should also distinguish between these two structures.
So JetBrains explicitly requires in the prompt:
It is better to generate a relatively conservative, narrowly applicable guard than to incorrectly classify unknown or substantially different inputs into an existing scenario.
That is, KotlinLLM essentially adopts a real runtime scenario-driven incremental program synthesis:
Encounter Scenario A
→ Generate the judgment condition and processing logic for Scenario A
Encounter Scenario B
→ Add the judgment condition and processing logic for Scenario B
Encounter Scenario C
→ Add the judgment condition and processing logic for Scenario C
Then, as the program runs, the implementation behind the smart macros will continuously expand, gradually covering more real scenarios.
However, in reality, KotlinLLM is not just an ordinary runtime library. It still needs the IntelliJ IDEA plugin, the JVM debugging interface, and the hot-replacement mechanism to complete the entire closed loop. Developers do not use the ordinary Run directly; here, they need to start the project using the plugin's provided Run with KotlinLLM.
At startup, the plugin first scans the asLlm() and mockLlm() call points in the project, then generates a set of base code, including parsers, providers, mock implementations, and a bootstrap class. Then, the public API attempts to load on the first run:
com.jetbrains.kotlinllm.generated.core.KotlinLlmBootstrap
This Bootstrap registers the already-generated parsers and mock providers into KotlinLLM's runtime manager. After that, the plugin starts the target JVM via the Java Debug Interface, or JDI, while monitoring all smart macro calls:
If the input matches an already-generated guard, the program directly executes ordinary Kotlin bytecode. This process does not need the model and does not go through the Agent.
If no guard can handle the current input, the generated code enters a regeneration hook. The plugin pauses the target JVM here and reads the real runtime data from the current call, including:
The actual input value
The input type
The output target type
The currently called method
The interface definition
Previously generated branches
Reusable helper functions
Compilation errors from the last generated code
Then KotlinLLM uses JetBrains' Koog Agent framework to start a restricted code generation Agent. Yes, Koog Agent. This Agent does not modify the entire project like a normal Coding Agent. Koog can only use a set of specially designed tools, such as:
readActualValues
grepActualInput
readTargetType
listToolbeltFunctions
readToolbeltFunction
registerToolbeltFunction
removeToolbeltFunction
submitCase
If the input is a very long piece of JSON, the Agent does not necessarily need to read the entire content at once. It can search for specific fields via
grepActualInputand then read the relevant areas in pages.
And it can also read the constructor and dependency types of the target Kotlin type, clarifying what object it must ultimately construct. After completing the analysis, the Agent must call:
submitCase(
guardExpression = "...",
caseHandlerBody = "..."
)
The plugin performs Kotlin syntax checks, PSI structure checks, heuristic purity checks, and compilation verification on the submitted code. Only after passing these checks are the new guard and handler written into the generated source code.
Then the plugin compiles the modified Kotlin file, finds the new class file, and then redefines the already-loaded class in the current JVM via JDI. The entire process is roughly:
Program encounters unfamiliar input
→ JVM pauses at the regeneration hook
→ Plugin reads real runtime values
→ Koog Agent analyzes input and target type
→ Agent submits guard and handler
→ Plugin verifies and compiles code
→ Hot-replaces class via JDI
→ Re-executes the previously failed call
So the program does not need to exit, nor does it need to restart the entire application. It can gain new processing capabilities within the same run.
In addition to data transformation, KotlinLLM also provides mockLlm<T>(), which supports generating a stateful test simulation based on a Kotlin interface, for example:
interface ShoppingCart {
fun add(product: Product)
fun total(): Double
}
A developer can directly write val cart: ShoppingCart = mockLlm(). Then, the first time cart.add(Product("Book", 20.0)) is called, KotlinLLM will observe the method name, parameter values, parameter types, interface definition, and return type, and then generate an implementation for this calling scenario.
Then executing val total = cart.total() can generate the logic corresponding to total() based on the interface semantics and the previously saved state.
Additionally, KotlinLLM provides a mutable state for mocks:
state: MutableMap<Any?, Any?>
That is, the generated implementation does not necessarily just "input a certain parameter, return a fixed answer"; it can also remember previous calls. For example, add() can write products into the state, and total() can calculate the total price from the state. That is, it is closer to a stateful fake that gradually forms based on test behavior, not an ordinary fixed-return-value mock.
However, JetBrains' restrictions on the model are also quite explicit:
It cannot arbitrarily return
null, empty lists, zero,false, or unconditional fixed answers. The model must attempt to implement the behavior this method should truly have in the current scenario based on the interface name, method name, parameters, return type, and current state.
Currently, the KotlinLLM model can only generate a very limited piece of local Kotlin logic:
- Can only use pure Kotlin
- Cannot add imports
- Cannot define new classes and fields
- Cannot call external libraries
- Cannot arbitrarily execute side effects
- Cannot use labeled return
- Must return the specified
ParsedResult - Should return
nullwhen not matching the current scenario - Must make the guard cover the assumptions the handler depends on
That is, when the model discovers that auxiliary logic needs to be shared between multiple scenarios, it can register functions to the Toolbelt. However, Toolbelt functions must also pass syntax and heuristic purity checks, and come with KDoc, parameter descriptions, return value descriptions, and call examples. However, in the current implementation, a lack of KDoc only generates a warning and does not prevent registration.
Then the number of times the Agent reads runtime data is also limited. The check tool budget defaults to 12 times. When five remain, the system starts to warn. After exceeding the budget, the Agent is required to stop further reading and submit an implementation based on the existing information.
However, looking at the source code, it seems this budget logic hasn't been connected yet?
For example, take the GitHub Issue Radar Demo given by JetBrains. The Demo needs to read Issues from different GitHub repositories and find tasks suitable for beginners to participate in. The characteristic here is that the tags used by different projects are not uniform; some are called good first issue, others beginner, easy, or starter. It is very difficult for a developer to hard-code all possible situations in advance.
Then KotlinLLM's approach is to first leave several conversion points to be completed in the code:
val apiUrl: String = asLlm(repoUrl)
val rawIssues: List<String> = asLlm(response)
val issue: JsonIssue = asLlm(rawJson)
val beginnerFriendly: Boolean = asLlm(label)
The first time the program encounters a certain type of input, the LLM generates the corresponding Kotlin logic based on the real data. For example:
- How to convert a GitHub repository address into an Issues API address
- How to extract a single Issue from the returned JSON
- How to map JSON fields to a Kotlin object
- How to determine whether a tag indicates "suitable for beginners"
After these logics are generated, they are saved as Kotlin source code. When the same type of data is encountered again later, the already-generated code can be executed directly, without needing to request the model again.
JetBrains conducted experiments using 20 real GitHub repositories and over 30,000 Issues. The recall rate for identifying beginner tasks was approximately 0.89. Additionally, the official statement says compilation and class hot-replacement only account for about 1% of the total time.
Currently, it seems more suitable for data-level processing, such as:
- Adapting multiple JSON or API return formats
- Parsing logs, CSV, and configuration files
- Converting old data structures to new DTOs
- Quickly generating data processing logic in the prototyping phase
- Generating simple fakes or mocks based on interfaces and test calls
That is, scenarios where the output answer can be solidified into Kotlin code. But actually, it should be more suitable to develop towards UI in the future. If it could solidify scenario UIs, this capability would be much more practical.
However, doing this at runtime, although it currently still needs to be in IntelliJ IDEA's Run with KotlinLLM mode, one really has to admire JetBrains' imagination.
Links
https://blog.jetbrains.com/research/2026/07/kotlinllm-open-source/
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
python and JS's eval()?