diff --git a/src/main/kotlin/agents_engine/core/Skill.kt b/src/main/kotlin/agents_engine/core/Skill.kt index f434850..0c37a12 100644 --- a/src/main/kotlin/agents_engine/core/Skill.kt +++ b/src/main/kotlin/agents_engine/core/Skill.kt @@ -74,6 +74,21 @@ class Skill( 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 diff --git a/src/main/kotlin/agents_engine/model/ToolDef.kt b/src/main/kotlin/agents_engine/model/ToolDef.kt index 934830d..747e26f 100644 --- a/src/main/kotlin/agents_engine/model/ToolDef.kt +++ b/src/main/kotlin/agents_engine/model/ToolDef.kt @@ -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` 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 @PublishedApi internal constructor( +class Tool @PublishedApi internal constructor( @PublishedApi internal val def: ToolDef, ) { val name: String get() = def.name diff --git a/src/test/kotlin/agents_engine/model/TypedToolRefsTest.kt b/src/test/kotlin/agents_engine/model/TypedToolRefsTest.kt new file mode 100644 index 0000000..cbb00ab --- /dev/null +++ b/src/test/kotlin/agents_engine/model/TypedToolRefsTest.kt @@ -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("typed-form") { + lateinit var fetch: Tool, Any?> + lateinit var compile: Tool, Any?> + tools { + fetch = tool("fetch", "Fetch") { _ -> "fetched" } + compile = tool("compile", "Compile") { _ -> "compiled" } + } + skills { + skill("build") { + tools(fetch, compile) + } + } + } + + val stringAgent = agent("string-form") { + tools { + tool("fetch", "Fetch") { _ -> "fetched" } + tool("compile", "Compile") { _ -> "compiled" } + } + skills { + skill("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("typed-validate") { + lateinit var ping: Tool, Any?> + tools { + ping = tool("ping", "Ping") { _ -> "pong" } + } + skills { + skill("respond") { + tools(ping) + } + } + } + assertEquals(listOf("ping"), a.skills["respond"]!!.toolNames) + } +}