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
15 changes: 15 additions & 0 deletions src/main/kotlin/agents_engine/core/Skill.kt
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,21 @@ class Skill<IN, OUT>(
implementation = null
}

/**
* Typed overload accepting `Tool<*, *>` handles returned by `tool(...)` builders
* (#1015). Catches typos and stale references at compile time instead of at agent
* `validate()` (`Agent.kt:404`). See `docs/ksp-design.md` for the broader plan.
*
* Requires at least one ref to disambiguate from `tools()` (empty), which resolves
* to the legacy string-vararg form.
*/
fun tools(first: agents_engine.model.Tool<*, *>, vararg rest: agents_engine.model.Tool<*, *>) {
checkNotFrozen()
isAgentic = true
toolNames = listOf(first.name) + rest.map { it.name }
implementation = null
}

var outputTransformer: ((String) -> OUT)? = null
private set

Expand Down
14 changes: 9 additions & 5 deletions src/main/kotlin/agents_engine/model/ToolDef.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,21 @@ class ToolDef(

/**
* Typed handle returned by every `tool(...)` builder overload. Wraps a
* [ToolDef] with phantom type parameters that let `Skill.tools(...)` and
* `+autoTool(...)` accept compile-time-checked references instead of
* stringly-typed lookups (#1015 — KSP P1.1).
* [ToolDef] with phantom type parameters that let `Skill.tools(...)` accept
* compile-time-checked references instead of stringly-typed lookups
* (#1015 — KSP P1.1).
*
* `Args` is the deserialized input type for typed tools (the `@Generable`
* data class), `Map<String, Any?>` for untyped tools. `Result` is the lambda's
* return type. Both type parameters are erased at runtime — the [def]
* underneath is the canonical runtime representation.
*
* Not `@JvmInline value` because Kotlin prohibits vararg of value-class types,
* and `Skill.tools(vararg refs: Tool<*, *>)` (#1016) is the primary use site.
* Tool handles are constructed once per agent build, never on the hot path —
* the per-handle allocation is negligible.
*/
@JvmInline
value class Tool<Args, Result> @PublishedApi internal constructor(
class Tool<Args, Result> @PublishedApi internal constructor(
@PublishedApi internal val def: ToolDef,
) {
val name: String get() = def.name
Expand Down
68 changes: 68 additions & 0 deletions src/test/kotlin/agents_engine/model/TypedToolRefsTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package agents_engine.model

import agents_engine.core.agent
import kotlin.test.Test
import kotlin.test.assertEquals

/**
* #1016 — `Skill.tools(...)` accepts typed `Tool<*, *>` handles in addition to
* the legacy stringly-typed form. The two forms produce identical
* `Skill.toolNames` and dispatch identically through the agentic loop.
*
* The string form stays — typed and string overloads coexist; #1017 will
* deprecate the string form (warning level), but not yet.
*/
class TypedToolRefsTest {

@Test
fun `typed tool refs produce same toolNames as string form`() {
val typedAgent = agent<String, String>("typed-form") {
lateinit var fetch: Tool<Map<String, Any?>, Any?>
lateinit var compile: Tool<Map<String, Any?>, Any?>
tools {
fetch = tool("fetch", "Fetch") { _ -> "fetched" }
compile = tool("compile", "Compile") { _ -> "compiled" }
}
skills {
skill<String, String>("build") {
tools(fetch, compile)
}
}
}

val stringAgent = agent<String, String>("string-form") {
tools {
tool("fetch", "Fetch") { _ -> "fetched" }
tool("compile", "Compile") { _ -> "compiled" }
}
skills {
skill<String, String>("build") {
tools("fetch", "compile")
}
}
}

val typedSkill = typedAgent.skills["build"]!!
val stringSkill = stringAgent.skills["build"]!!

assertEquals(stringSkill.toolNames, typedSkill.toolNames)
assertEquals(true, typedSkill.isAgentic)
assertEquals(listOf("fetch", "compile"), typedSkill.toolNames)
}

@Test
fun `typed refs survive validate() — agent constructs without unknown-tool error`() {
val a = agent<String, String>("typed-validate") {
lateinit var ping: Tool<Map<String, Any?>, Any?>
tools {
ping = tool("ping", "Ping") { _ -> "pong" }
}
skills {
skill<String, String>("respond") {
tools(ping)
}
}
}
assertEquals(listOf("ping"), a.skills["respond"]!!.toolNames)
}
}
Loading