From 7bd01e128854b0f110464c794171ad8f798b5d9f Mon Sep 17 00:00:00 2001 From: Richie Caputo Date: Fri, 13 Mar 2026 17:30:06 -0400 Subject: [PATCH 1/3] chore(release): Bump version to 0.4.1 Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 10 +++++----- build.mill | 2 +- package.json | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d6faa9a..6cdfecb 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A type-safe Scala.js wrapper for `@anthropic-ai/claude-agent-sdk`, with idiomati ## Status - SDK baseline: `@anthropic-ai/claude-agent-sdk` `0.2.75` -- Current repo build version: `0.4.0` +- Current repo build version: `0.4.1` - Scala version: `3.7.4` - Preferred JS runtime: `bun` - Runtime requirement: `ANTHROPIC_API_KEY` @@ -25,20 +25,20 @@ For a compatibility inventory against the installed SDK baseline, see `docs/COMP ## Installation -Use the current published release when consuming from Maven Central. For local development against this repo, the build version is `0.4.0`. +Use the current published release when consuming from Maven Central. For local development against this repo, the build version is `0.4.1`. ### Mill ```scala def ivyDeps = Seq( - mvn"com.tjclp::scalagent::0.4.0" + mvn"com.tjclp::scalagent::0.4.1" ) ``` ### SBT ```scala -libraryDependencies += "com.tjclp" %%% "scalagent" % "0.4.0" +libraryDependencies += "com.tjclp" %%% "scalagent" % "0.4.1" ``` ### Maven @@ -47,7 +47,7 @@ libraryDependencies += "com.tjclp" %%% "scalagent" % "0.4.0" com.tjclp scalagent_sjs1_3 - 0.4.0 + 0.4.1 ``` diff --git a/build.mill b/build.mill index 1419a82..1c8854a 100644 --- a/build.mill +++ b/build.mill @@ -31,7 +31,7 @@ object agent extends ScalaJSModule with PublishModule { // Publishing configuration - read from PUBLISH_VERSION env var (set by CI) // Task.Input ensures env var is re-evaluated each run (not cached) override def publishVersion = Task.Input { - Task.env.get("PUBLISH_VERSION").getOrElse("0.4.1-SNAPSHOT") + Task.env.get("PUBLISH_VERSION").getOrElse("0.4.1") } // Skip scaladoc generation - Scala.js facades cause doc errors diff --git a/package.json b/package.json index 2cdc8a9..acd841d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "scalagent", - "version": "0.4.1-SNAPSHOT", + "version": "0.4.1", "private": true, "type": "module", "description": "Scalagent - Type-safe Scala.js wrapper for the Claude Agent SDK", From 878342a7a743e489e13b41e8d6d03d21545b4404 Mon Sep 17 00:00:00 2001 From: Richie Caputo Date: Tue, 17 Mar 2026 08:59:47 -0400 Subject: [PATCH 2/3] feat: AnyOf schema support + ToolInput summon for ADT enums The ToolInput macro now tries to summon a user-provided ToolInput[T] before auto-generating schemas. This handles sealed ADT enums with custom JSON encoding (e.g. types that accept string | array on the wire) where the macro can't infer the correct schema from case names alone. Changes: - JsonSchema.AnyOfType + anyOf() constructor for union type schemas - ZodConverter handles AnyOfType via z.union() - ToolInputMacros.typeToSchemaExpr tries Expr.summon[ToolInput[T]] before falling through to auto-generation for enums and unknown types Usage: define `given ToolInput[MyType]` in the companion object with a manual schema, and the macro will pick it up automatically when MyType appears as a field in a derived case class. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../scalagent/macros/ToolInputMacros.scala | 29 +++++++++++++++---- src/com/tjclp/scalagent/tools/ToolDef.scala | 8 +++++ src/com/tjclp/scalagent/tools/ZodFacade.scala | 2 ++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/com/tjclp/scalagent/macros/ToolInputMacros.scala b/src/com/tjclp/scalagent/macros/ToolInputMacros.scala index 13c4cd1..65da46f 100644 --- a/src/com/tjclp/scalagent/macros/ToolInputMacros.scala +++ b/src/com/tjclp/scalagent/macros/ToolInputMacros.scala @@ -90,13 +90,32 @@ object ToolInputMacros: '{ JsonSchema.obj().additionalProperties.build } case t if t.typeSymbol.flags.is(Flags.Enum) => - val enumCases = enumCaseNames(t) - val casesExpr = Expr(enumCases) - '{ JsonSchema.enumOf($casesExpr*) } + // Try to summon a user-provided ToolInput for this type first. + // This handles ADT enums with custom JSON encoding (e.g. BodyContent) + // where the wire format can't be inferred from the case names. + trySummonToolInput(t).getOrElse { + val enumCases = enumCaseNames(t) + val casesExpr = Expr(enumCases) + '{ JsonSchema.enumOf($casesExpr*) } + } case t if t.typeSymbol.flags.is(Flags.Case) => generateSchemaExpr(caseClassFieldInfos(t)) case other => - report.warning(s"Unknown type ${other.show}, using object schema") - '{ JsonSchema.obj().build } + // Try to summon a user-provided ToolInput before falling back + trySummonToolInput(other).getOrElse { + report.warning(s"Unknown type ${other.show}, using object schema") + '{ JsonSchema.obj().build } + } + + /** Try to summon a ToolInput[T] given in scope and extract its schema. */ + private def trySummonToolInput(using Quotes)( + tpe: quotes.reflect.TypeRepr + ): Option[Expr[JsonSchema]] = + import quotes.reflect.* + tpe.asType match + case '[t] => + Expr.summon[ToolInput[t]].map { ti => + '{ $ti.jsonSchema } + } diff --git a/src/com/tjclp/scalagent/tools/ToolDef.scala b/src/com/tjclp/scalagent/tools/ToolDef.scala index 2752db4..53e5c82 100644 --- a/src/com/tjclp/scalagent/tools/ToolDef.scala +++ b/src/com/tjclp/scalagent/tools/ToolDef.scala @@ -318,6 +318,11 @@ object JsonSchema: def toRaw: js.Object = js.Dynamic.literal(`type` = "string", `enum` = values.toJSArray).asInstanceOf[js.Object] + /** Union type (anyOf) — for types that accept multiple formats (e.g. string | array) */ + final case class AnyOfType(schemas: List[JsonSchema]) extends JsonSchema: + def toRaw: js.Object = + js.Dynamic.literal(anyOf = schemas.map(_.toRaw).toJSArray).asInstanceOf[js.Object] + /** Object type */ final case class ObjectType( properties: Map[String, JsonSchema], @@ -361,6 +366,9 @@ object JsonSchema: /** Enum schema */ def enumOf(values: String*): JsonSchema = EnumType(values.toList) + /** Union schema (anyOf) */ + def anyOf(schemas: JsonSchema*): JsonSchema = AnyOfType(schemas.toList) + /** Object schema builder */ def obj(properties: (String, JsonSchema)*): ObjectBuilder = ObjectBuilder(properties.toMap) diff --git a/src/com/tjclp/scalagent/tools/ZodFacade.scala b/src/com/tjclp/scalagent/tools/ZodFacade.scala index 67c954b..6421cb2 100644 --- a/src/com/tjclp/scalagent/tools/ZodFacade.scala +++ b/src/com/tjclp/scalagent/tools/ZodFacade.scala @@ -19,6 +19,7 @@ object Zod extends js.Object: def `enum`(values: js.Array[String]): ZodType = js.native def array(schema: ZodType): ZodType = js.native def `object`(shape: js.Dictionary[ZodType]): ZodType = js.native + def union(types: js.Array[ZodType]): ZodType = js.native @js.native trait ZodType extends js.Object: @@ -60,3 +61,4 @@ object ZodConverter: case JsonSchema.EnumType(values) => Zod.`enum`(values.toJSArray) case JsonSchema.ArrayType(items) => Zod.array(toZodType(items)) case obj: JsonSchema.ObjectType => Zod.`object`(toZodRawShape(obj)) + case JsonSchema.AnyOfType(schemas) => Zod.union(schemas.map(toZodType).toJSArray) From 4d25f7f06a10d11be272cb23392a3a3d81723ce3 Mon Sep 17 00:00:00 2001 From: Richie Caputo Date: Tue, 17 Mar 2026 13:59:22 -0400 Subject: [PATCH 3/3] feat: allow ToolInput summon override for case classes The trySummonToolInput pattern now applies to case classes too, not just enums and unknown types. Users can define a custom `given ToolInput[MyCaseClass]` to override the auto-derived schema. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/com/tjclp/scalagent/macros/ToolInputMacros.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/com/tjclp/scalagent/macros/ToolInputMacros.scala b/src/com/tjclp/scalagent/macros/ToolInputMacros.scala index 65da46f..8902564 100644 --- a/src/com/tjclp/scalagent/macros/ToolInputMacros.scala +++ b/src/com/tjclp/scalagent/macros/ToolInputMacros.scala @@ -100,7 +100,9 @@ object ToolInputMacros: } case t if t.typeSymbol.flags.is(Flags.Case) => - generateSchemaExpr(caseClassFieldInfos(t)) + trySummonToolInput(t).getOrElse { + generateSchemaExpr(caseClassFieldInfos(t)) + } case other => // Try to summon a user-provided ToolInput before falling back