Skip to content
Merged
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
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,33 @@

All notable changes to Agents.KT are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Pre-1.0, minor bumps may add new public API; existing API surface is preserved.

## [Unreleased] — targeting 0.3.0

First leg of the **KSP / compile-time-validation initiative** described in `docs/ksp-design.md`. This release ships **typed tool refs** — Kotlin's type system catches `tools("typo")` mistakes that previously bombed at agent `validate()` (or in CI test runs). Plus the `:agents-kt-ksp` module skeleton, ready for the Phase 2 codegen work.

### Binary compatibility

**Source-compatible** with 0.2.x — your code compiles unchanged (you'll see deprecation warnings on `tools("name")` calls, with a `ReplaceWith` hint to the typed form).

**NOT binary-compatible.** `tool(...)` builders changed return type `Unit → Tool<Args, Result>`. Consumers who upgrade the `agents-kt` jar without recompiling will hit `NoSuchMethodError` at first tool registration. Recompile against 0.3.0; no source changes required. If you depend on `agents-kt` from a published library, that library must also republish against 0.3.0. This is why the bump goes 0.2.x → 0.3.0 and not 0.2.x → 0.2.3.

### Added
- `Tool<Args, Result>` typed handle returned by every `tool(...)` builder overload. Phantom-typed wrapper around `ToolDef` whose type parameters propagate through the agent build (#1015).
- `Skill.tools(first: Tool<*, *>, vararg rest: Tool<*, *>)` — typed overload alongside the legacy stringly-typed form. Tool typos become red squiggles in IntelliJ instead of runtime errors at `validate()` (#1016).
- `Skill.tools()` — explicit no-argument overload that marks a skill agentic with no allowlisted tools (the model gets only memory + built-in tools). Disambiguates from the deprecated string-vararg form.
- `docs/ksp-design.md` — initiative roadmap, runtime-checks inventory (72 sites bucketed), three-phase plan.
- **`:agents-kt-ksp` Gradle module** — new sibling artifact `ai.deep-code:agents-kt-ksp` published to Maven Central. Empty `SymbolProcessorProvider` skeleton; consumers can wire it via `ksp("ai.deep-code:agents-kt-ksp:VERSION")` but it does no work yet. Phase 2 of the KSP initiative (#1018). The validation pass (#1019) and schema-generation pass (#1020) plug into the processor in subsequent issues.
- Multi-module Gradle setup: `settings.gradle.kts` includes `:agents-kt-ksp`; same Maven Central + Sonatype publishing wiring as the runtime artifact; same in-memory PGP signing.
- Depends on `com.google.devtools.ksp:symbol-processing-api:2.3.7` (KSP2, decoupled from Kotlin compiler version).
- Reads runtime annotations via `compileOnly(project(":"))` — never lands on the consumer's runtime classpath.

### Changed
- README + `docs/model-and-tools.md` examples now show typed-ref form first; string form is documented only for built-in tools (`escalate`, `throwException`, `memory_*`).
- Internal test fixtures migrated to typed refs across 35+ files (#1017).

### Deprecated
- `Skill.tools(vararg names: String)` — soft-deprecated at warning level. Stays for built-in tools (`escalate`, `throwException`, `memory_*`) and runtime-discovered tool names (MCP); no removal planned pre-1.0.

## [0.2.3] — 2026-05-04

Hotfix patch — single bug.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ These APIs work in `main`, are unit-tested, and are exercised by integration tes
- **Skills with knowledge** — `skill { knowledge("key", "...") { } }`, lazy-loaded per call. See [docs/skills.md#shared-knowledge](docs/skills.md#shared-knowledge).
- **Agentic loop with tool calling** — multi-turn `chat ↔ tools` driven by the model. See [docs/model-and-tools.md](docs/model-and-tools.md).
- **Typed tools via `@Generable`** — `tool<Args, Result>(...)` with reflection-built JSON Schema; `additionalProperties: false`; sealed-discriminator validation (#658, #661, #699).
- **Typed tool refs in skill allowlists** — `tool(...)` returns a `Tool<Args, Result>` handle; `skill { tools(writeFile, compile) }` accepts handles, the IDE catches typos (#1015–#1017). The legacy `tools("name")` string form remains for built-in tools and runtime-discovered MCP names but produces a deprecation warning.
- **Per-skill tool authorization** — runtime allowlist; the prompt's "Available tools" listing is descriptive, the security boundary is the runtime check (#630). See [docs/model-and-tools.md#tool-authorization-model](docs/model-and-tools.md#tool-authorization-model).
- **Inline tool-call fallback** — auto-recovery when an Ollama model rejects native `tools` (e.g. `gemma3:4b`) — strips the field, injects inline JSON format prompt, retries (#702, #706). See [docs/model-and-tools.md#inline-tool-call-fallback-ollama-models-without-native-tool-support](docs/model-and-tools.md#inline-tool-call-fallback-ollama-models-without-native-tool-support).
- **Composition operators** — `then`, `/` (parallel), `*` and `forum { }` (multi-agent), `.loop {}`, `.branch {}` on sealed types. See [docs/composition.md](docs/composition.md).
Expand Down
100 changes: 100 additions & 0 deletions agents-kt-ksp/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
plugins {
kotlin("jvm")
`maven-publish`
signing
}

group = "ai.deep-code"
version = rootProject.version

repositories {
mavenCentral()
}

dependencyLocking {
lockAllConfigurations()
}

dependencies {
// KSP processor API. KSP2 (2.x) is decoupled from the bundled Kotlin
// compiler version, so the same KSP release works across a range of
// Kotlin versions. See https://github.com/google/ksp.
implementation("com.google.devtools.ksp:symbol-processing-api:2.3.7")

// Read annotations defined in the runtime library (e.g. @Generable).
// compileOnly — never end up on the consumer's runtime classpath; the
// consumer already has the runtime jar via their own implementation(...).
compileOnly(project(":"))

testImplementation(kotlin("test"))
}

kotlin {
jvmToolchain(21)
}

java {
withSourcesJar()
withJavadocJar()
}

publishing {
publications {
create<MavenPublication>("mavenCentral") {
from(components["java"])

artifactId = "agents-kt-ksp"

pom {
name.set("Agents.KT KSP processor")
description.set("Compile-time KSP processor for Agents.KT — validates @Generable shape and (in later releases) generates JSON Schema + lenient parser code.")
url.set("https://github.com/Deep-CodeAI/Agents.KT")

licenses {
license {
name.set("MIT License")
url.set("https://opensource.org/licenses/MIT")
}
}

developers {
developer {
id.set("kskobeltsyn")
name.set("Konstantin Skobeltsyn")
email.set("konstantin@deep-code.ai")
}
}

scm {
url.set("https://github.com/Deep-CodeAI/Agents.KT")
connection.set("scm:git:git://github.com/Deep-CodeAI/Agents.KT.git")
developerConnection.set("scm:git:ssh://git@github.com/Deep-CodeAI/Agents.KT.git")
}
}
}
}

repositories {
maven {
name = "sonatype"
url = uri("https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/")
credentials {
username = findProperty("sonatypeUsername") as String? ?: ""
password = findProperty("sonatypePassword") as String? ?: ""
}
}
}
}

signing {
val signingKey = findProperty("signing.key") as String?
val signingPassword = findProperty("signing.password") as String?
if (signingKey != null) {
useInMemoryPgpKeys(signingKey, signingPassword ?: "")
}
sign(publishing.publications["mavenCentral"])
}

tasks.withType<Sign>().configureEach {
onlyIf { findProperty("signing.key") != null }
}
23 changes: 23 additions & 0 deletions agents-kt-ksp/gradle.lockfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
com.google.devtools.ksp:symbol-processing-api:2.3.7=compileClasspath
org.jetbrains.kotlin:kotlin-build-tools-api:2.3.21=kotlinBuildToolsApiClasspath
org.jetbrains.kotlin:kotlin-build-tools-compat:2.3.21=kotlinBuildToolsApiClasspath
org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.3.21=kotlinBuildToolsApiClasspath
org.jetbrains.kotlin:kotlin-build-tools-impl:2.3.21=kotlinBuildToolsApiClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.3.21=kotlinBuildToolsApiClasspath
org.jetbrains.kotlin:kotlin-compiler-runner:2.3.21=kotlinBuildToolsApiClasspath
org.jetbrains.kotlin:kotlin-daemon-client:2.3.21=kotlinBuildToolsApiClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.3.21=kotlinBuildToolsApiClasspath
org.jetbrains.kotlin:kotlin-reflect:1.6.10=kotlinBuildToolsApiClasspath
org.jetbrains.kotlin:kotlin-script-runtime:2.3.21=kotlinBuildToolsApiClasspath,kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-scripting-common:2.3.21=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:2.3.21=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:2.3.21=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-scripting-jvm:2.3.21=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-stdlib:2.3.21=compileClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-tooling-core:2.3.21=kotlinBuildToolsApiClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.0=kotlinBuildToolsApiClasspath
org.jetbrains:annotations:13.0=compileClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerPluginClasspathMain
empty=kotlinScriptDefExtensions
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package agents_engine.ksp

import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.symbol.KSAnnotated

/**
* KSP processor entry point for Agents.KT (#1018, P2.1).
*
* Currently a no-op skeleton — exists so the `:agents-kt-ksp` artifact can be
* published, applied via the KSP plugin in consumer projects, and exercised
* end-to-end without doing any work yet. The validation pass (#1019) and
* schema-generation pass (#1020) plug into [process] in subsequent issues.
*/
class AgentsKtSymbolProcessor(
@Suppress("unused") private val env: SymbolProcessorEnvironment,
) : SymbolProcessor {

override fun process(resolver: Resolver): List<KSAnnotated> {
// #1019 will walk every @Generable class here and emit compile-time
// validation errors via env.logger.error(...).
// #1020 will then generate per-class *_GeneratedSchema.kt files using
// env.codeGenerator.createNewFile(...).
return emptyList()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package agents_engine.ksp

import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider

/**
* Service-loader entry point. KSP picks this up via
* `META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider`.
*
* Consumers apply the KSP plugin and add `ksp("ai.deep-code:agents-kt-ksp:0.3.0")`
* to their dependencies; KSP discovers this provider at compile time and runs
* [AgentsKtSymbolProcessor.process] over their source tree.
*/
class AgentsKtSymbolProcessorProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor =
AgentsKtSymbolProcessor(environment)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
agents_engine.ksp.AgentsKtSymbolProcessorProvider
30 changes: 19 additions & 11 deletions docs/model-and-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,22 @@ val calculator = agent<String, String>("calculator") {
prompt("You are a calculator. Use the provided tools to evaluate expressions step by step.")
model { ollama("gpt-oss:120b-cloud"); host = "localhost"; port = 11434; temperature = 0.0 }

lateinit var add: Tool<Map<String, Any?>, Any?>
lateinit var subtract: Tool<Map<String, Any?>, Any?>
lateinit var multiply: Tool<Map<String, Any?>, Any?>
lateinit var divide: Tool<Map<String, Any?>, Any?>
lateinit var power: Tool<Map<String, Any?>, Any?>
tools {
tool("add", "Add two numbers. Args: a, b") { args -> num(args, "a") + num(args, "b") }
tool("subtract", "Subtract b from a. Args: a, b") { args -> num(args, "a") - num(args, "b") }
tool("multiply", "Multiply two numbers. Args: a, b") { args -> num(args, "a") * num(args, "b") }
tool("divide", "Divide a by b. Args: a, b") { args -> num(args, "a") / num(args, "b") }
tool("power", "Raise base to exponent. Args: base, exp") { args -> Math.pow(num(args, "base"), num(args, "exp")) }
add = tool("add", "Add two numbers. Args: a, b") { args -> num(args, "a") + num(args, "b") }
subtract = tool("subtract", "Subtract b from a. Args: a, b") { args -> num(args, "a") - num(args, "b") }
multiply = tool("multiply", "Multiply two numbers. Args: a, b") { args -> num(args, "a") * num(args, "b") }
divide = tool("divide", "Divide a by b. Args: a, b") { args -> num(args, "a") / num(args, "b") }
power = tool("power", "Raise base to exponent. Args: base, exp") { args -> Math.pow(num(args, "base"), num(args, "exp")) }
}

skills {
skill<String, String>("solve", "Evaluate arithmetic expressions using tools") {
tools("add", "subtract", "multiply", "divide", "power")
tools(add, subtract, multiply, divide, power)
}
}

Expand Down Expand Up @@ -100,8 +105,9 @@ A per-instance latch records the model's incapability, so subsequent `chat()` ca
val a = agent<String, String>("calc") {
// gemma3:4b doesn't support native tools — the fallback drives it via inline JSON
model { ollama("gemma3:4b"); host = "localhost"; port = 11434 }
tools { tool("evaluate", "Evaluate an arithmetic expression") { args -> eval(args["expression"]!!) } }
skills { skill<String, String>("calc", "Compute") { tools("evaluate") } }
lateinit var evaluate: Tool<Map<String, Any?>, Any?>
tools { evaluate = tool("evaluate", "Evaluate an arithmetic expression") { args -> eval(args["expression"]!!) } }
skills { skill<String, String>("calc", "Compute") { tools(evaluate) } }
}
a("Compute (2+3)*4") // works — agent invokes evaluate via inline tool call, returns "20"
```
Expand Down Expand Up @@ -194,12 +200,14 @@ assistant("Translate this to French: Hello world")
```kotlin
val compute = agent<String, Int>("calculator") {
model { ollama("gpt-oss:120b-cloud"); host = "localhost"; port = 11434; temperature = 0.0 }
lateinit var add: Tool<Map<String, Any?>, Any?>
lateinit var power: Tool<Map<String, Any?>, Any?>
tools {
tool("add", "Add two numbers. Args: a, b") { args -> num(args, "a") + num(args, "b") }
tool("power", "Raise base to exponent. Args: base, exp") { args -> Math.pow(num(args, "base"), num(args, "exp")) }
add = tool("add", "Add two numbers. Args: a, b") { args -> num(args, "a") + num(args, "b") }
power = tool("power", "Raise base to exponent. Args: base, exp") { args -> Math.pow(num(args, "base"), num(args, "exp")) }
}
skills { skill<String, Int>("solve", "Evaluate arithmetic expressions") {
tools("add", "power")
tools(add, power)
transformOutput { it.trim().toIntOrNull() ?: Regex("-?\\d+").find(it)?.value?.toInt() ?: error("No int in: $it") }
}}
}
Expand Down
Loading
Loading