跪拜 Guibai
← Back to the summary

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:

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:

If the handler input must have title and labels, then the guard must prove these two fields exist; and if the handler assumes labels is 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:

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 grepActualInput and 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:

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:

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:

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/

Comments

Top 1 from juejin.cn, machine-translated. The original thread is authoritative.

vocal

python and JS's eval()?