v1.0.0-RC5 makes Kyo a place to build AI applications and the protocol tooling around them. kyo-ai makes a call to a language model a value you compose and runs the whole agentic loop for you, deriving the result schema, driving the tool-call loop, threading the conversation, and parsing the streaming reply, so what you write is the result type and the tools the model may call, and kyo-mcp exposes a Kyo service to an MCP host like Claude Desktop or drives an MCP server itself.
The thread tying the release together is protocols as typed values. MCP, the Language Server Protocol, the LLM APIs, and the browser's Chrome DevTools Protocol are each a composable value here rather than hand-rolled wire framing, and they all rest on two new pieces of shared machinery. kyo-jsonrpc is one cross-platform JSON-RPC 2.0 engine they all speak through, handling request/response correlation, cancellation, and progress in both directions, and underneath it a reworked kyo-schema (which shipped in RC2) co-derives a type's structure and its wire codec from a single macro, so the schema a type advertises is the exact shape it reads and writes. On that same engine, kyo-lsp is the Language Server Protocol 3.17 for building editor-tooling servers and clients, and kyo-compiler drives a warm Scala 3 presentation compiler for the completions, hover, and diagnostics such a server needs. They are the foundation for a Kyo-native language server, which the project is building toward as an alternative to Metals.
New Features
New modules
Every module here cross-compiles to JVM, JS, Native, and Wasm, except kyo-compiler, which is JVM-only.
-
kyo-ai(README) makes a call to a language model a value you compose and hands the whole agentic loop to the framework. Using an LLM used to mean a mainstream SDK plus around forty lines of your own orchestration: authoring the JSON schema, running the tool-call loop, threading the message list, retrying transport failures, and parsing streaming deltas. You now write only the result type you want back and the tools you grant the model, andAI.gen[A]derives the schema fromA, drives the loop, and decodes the reply. The API forms a ladder from stateless to persistent: a bareAI.genis a forgetful one-shot, a namedAIinstance remembers earlier turns in the same run, and anAgentis an actor-backed entity that holds its conversation across manyaskcalls, one input at a time. A tool runs your function when the model calls it mid-generation, and the loop recovers from both a bad decode and a thrown run function without escaping the generation. Streaming yields whole result objects one at a time rather than token text, and providers cover an OpenAI-compatible backend (OpenAI, DeepSeek, Gemini, Groq, Baseten, OpenRouter) and the Anthropic Messages backend, both on kyo-http with auto-config from environment API keys. (by @fwbrasil in #1707) -
kyo-mcp(README) is a Model Context Protocol server and client on the newkyo-jsonrpcengine. Exposing a Kyo service to an MCP host (Claude Desktop, IDE agents) or driving an MCP server is mostly protocol bookkeeping: the handshake, capability negotiation, tool/resource/prompt dispatch, and the reverse-direction calls. You now describe a server as a flat list of typed handlers, annotating only each tool's input type while the compiler infers the rest, and the engine runs the protocol. It works in both directions: the server can ask the connected client to sample its model, elicit user input, or list roots. Failures stay typed all the way out, a closed transport surfaces as a typedMcpConnectionClosedExceptionrather than a bareClosed, and each operation's error channel names exactly the failures it can raise. (by @fwbrasil in #1701) -
kyo-lsp(README) is a Language Server Protocol 3.17 server and client on the samekyo-jsonrpcengine. Every LSP server and client re-implements the same 3.17 scaffolding: the initialize/shutdown lifecycle, the message schemas, document synchronization, position-encoding negotiation, and capability gating. In kyo-lsp you register a typed handler per operation you support, and the engine advertises the matching capabilities, owns the handshake, negotiates position encoding, and tracks open documents. Like kyo-mcp it works in both directions, issuing server-initiated requests such as applying a workspace edit, showing a message or document, and reporting work progress. (by @fwbrasil in #1703) -
kyo-jsonrpc(README) is the JSON-RPC 2.0 engine the protocol modules stand on. Kyo had no JSON-RPC support, so every protocol on it (MCP, LSP, the browser's Chrome DevTools Protocol) hand-rolled request/response correlation, notification dispatch, cancellation, and progress. One peer type now both serves and calls in both directions: it handles correlation, cancellation, and progress for you and maps user errors to wire codes automatically, so whether a peer is a server, a client, or both is just a matter of which methods it answers and which it calls. Transports plug in down to the byte level (in-memory, line and content-length stdio, a JVM Unix domain socket), andkyo-jsonrpc-httpadds one over a kyo-http WebSocket. The browser module is ported onto it, its custom Chrome DevTools client replaced by a CDP session that runs as a jsonrpc peer. (by @fwbrasil in #1687) -
kyo-compiler(README) is the JVM-only Scala 3 presentation-compiler driver a language server draws on, giving Kyo a warm, cancellable source of IDE intelligence: completions, hover, signature help, and diagnostics for a project. It keeps a per-project compiler warm, resolves one per toolchain and classpath, caps how many run at once, and evicts idle instances, and every operation is cancellable by interrupting the calling fiber. When a request needs isolation, or the target Scala version does not match the host, it runs the compiler in a forked worker JVM that can be hard-killed on a stuck request, and the caller never has to choose a backend: results come back the same either way. (by @fwbrasil in #1718)
kyo-schema
kyo-schema shipped in RC2; this cycle reworks it into the type-safe wire-protocol foundation the new modules stand on, and expands it across formats, representations, and configuration.
-
A type-safe foundation for wire protocols: protocols like JSON-RPC could not be built type-safe on the old derivation, because a type's wire codec and its structure were derived by two separate macros that could silently disagree, so the JSON Schema a type advertised could not be trusted to match the bytes it wrote. Structure and codec are now co-derived from one macro, so the two cannot drift: the JSON Schema a type advertises is the exact shape it reads and writes.
Structure.Valuebecomes the first-class open value that everySchemaconverts to and from, andCodec.IntrospectingReadermakes "can read an open value" a type-level capability (JSON and YAML have it, Protobuf does not), so decoding through a non-self-describing codec is a compile error instead of a runtime surprise. This is the foundation kyo-jsonrpc, kyo-mcp, kyo-lsp, kyo-ai, and the browser CDP client are built on. (by @fwbrasil in #1682) -
MessagePack codec: kyo-schema dispatched to a pluggable
Codecbut had no self-describing binary option between Protobuf (compact but schema-required) and JSON (self-describing but text). A hand-rolled MessagePack codec with no third-party dependencies now cross-compiles to JVM, JS, Native, and Wasm, audited byte-for-byte against the spec, and because every MessagePack value carries a type tag its reader is aCodec.IntrospectingReader, soStructure.Valueand open-shaped envelopes round-trip through it, the capability Protobuf cannot offer. Key, Instant, and Duration encodings are configurable, and the output interoperates with standard MessagePack tooling in Python, JS, and Go. (by @DamianReeves in #1685) -
Four new sum representations, and a silent data-loss fix: kyo-schema serialized sum types under only two shapes, and the flat-discriminator form had a correctness hole that discarded any variant payload not a JSON object (a scalar, array, or null) with no error. A
UnionRepresentationenum now adds.adjacent,.tupleTagged,.tupleFlat, and.untagged(untagged decode tries each variant's decoder in declaration order, first clean parse wins), and the adjacent form fixes the silent drop by giving the payload the whole content position..discriminatoris reimplemented as sugar over the internal form and stays byte-identical, and the external-wrapper default remains inert. (by @DamianReeves in #1704) -
Codec-aware representation selection, field serde controls, and type-union derivation: a
Schema[A]fixed its wire shape at derivation with no awareness of the codec, so a sum declaring a top-level array shape hard-failed on a codec that cannot express it (Protobuf), and one schema could not serve both a JSON producer wanting a compact array and a Protobuf consumer needing an object envelope.Schema[A].representations(first, rest*)now declares an ordered preference and encode picks the highest-priority shape the active codec can express, degrading rather than throwing, with capability projected offCodec.Writerso external codecs participate without a kyo-schema change. Field-level controls (.omitNone,.omitEmptyCollections,.default,.denyUnknownFields,.transformField) are added, andSchema[A | B]now derives directly, untagged by default. (by @DamianReeves in #1715) -
Configurable variant and field wire names: kyo-schema serialized sealed-trait variants and product fields under their verbatim Scala names, so a schema could not encode or decode an external contract using lower-camel discriminators or snake_case keys without a per-field
renameon every variant. A naming layer now addsvariantNames,renameAllVariants/renameAllFields(CamelCase, SnakeCase, KebabCase, PascalCase, ScreamingSnakeCase), and decode-onlyaliasbuilders, with collisions raised as typed exceptions and an acronym-aware tokenizer that keepsHTTPServerashttp_server. An unconfigured schema stays byte-identical. (by @DamianReeves in #1694) -
Declarative schema annotations and rename-invariant Protobuf field numbers: the derivation macro discarded all Scala annotations, so wire-shape configuration required programmatic builder calls, and Protobuf field numbers were hashed from the effective wire name, so a programmatic
.renamesilently reassigned the binary field number and broke wire compatibility. A newkyo.schema.SchemaAnnotationfamily adds ten built-in leaves (@rename,@alias,@doc,@omit,@transient,@discriminator,@adjacent,@untagged,@transform,@proto.fieldNumber) that desugar onto the existing config slots at derivation, with programmatic config winning on conflict and an unannotated type staying byte-identical. Protobuf field numbers are now rename-invariant, so a.renameleaves binary layout unchanged, and cross-packagederives Schemais fixed. (by @DamianReeves in #1722) -
Protobuf repeated and map correctness: two bugs corrupted every repeated and map field.
hasNextElement()did not track the field number, so only the first element of any repeated collection was consumed, and the map writer emitted each entry as a struct field rather than a proto3MapEntry, so string keys decoded back as positional index strings and non-String keys did not compile. Repeated collections and maps now round-trip top-level and nested with keys preserved, non-String keys derive through a newmapSchema[K, V], absent repeated and map fields decode to empty, and aProtobuf.Conformanceenum gates whether non-native map keys are rejected (Strict, the default) or kept (Permissive). (by @DamianReeves in #1721)
Improvements
Native parity
- cats-effect binding on Scala Native: the cats-effect binding in kyo-compat supported only JVM and JS while the ZIO and Kyo bindings already covered all three, so downstream libraries targeting Native with the CE backend hit an empty-intersection error from the plugin. The CE backend now lists Native among its supported platforms and its shared code compiles on Native unmodified, reaching parity with the ZIO and Kyo bindings. (by @marcgrue in #1720)
General
-
Core, data, and aeron APIs behind kyo-compiler: building kyo-compiler surfaced gaps in lower modules, fixed in the same PR as independently useful additions.
Async.fromCompletableFuturecancels the future on fiber interrupt and surfaces a failure asAbort[Throwable];Command.spawnUnscopedspawns a process whose lifetime the caller owns;Cache.initWithFinalizerruns an effectful finalizer on every removal path;Tag.hashswitches from an identity-influenced hash to a content-stable one that hashes identically across JVMs (which kyo-aeron's stream-id derivation depends on); andTopic.publish/Topic.streamgain an optionalstreamIdand large-message support. (by @fwbrasil in #1718) -
Zero-allocation errno-aware FFI returns: errno-aware FFI bindings returned
Ffi.WithError[A], a class allocated per fallible C call on hot I/O paths (socket read/write, epoll/kqueue wait), that also boxed the value.Outcome[A]is now anopaque type Outcome[A] = Longpacking result and errno into one machine word, so a fallible call allocates nothing:o >= 0carries the success value ando < 0packs-errno, with.valuereading back unboxed at the C width. (by @fwbrasil in #1710) -
kyo-ffi codegen for nested-struct String fields: kyo-ffi codegen emitted invalid Scala when a struct parameter contained a nested struct with a
Stringfield, because the scratch-tempvalname was built from the case-class access path and carried member-selection dots, so the parser read it as a member selection and compilation failed. A newEmitterBase.localIdentstrips backticks and replaces those dots with underscores at every site that reuses an access path as a generated name, so nested-struct String and function-pointer fields now generate compilable Scala. (by @DamianReeves in #1684)
Concurrency and streams
- Reliable actor request/reply, and push-based pub/sub:
Actor.askcould hang a caller forever when the actor terminated or panicked after a message was enqueued but before the reply was sent, because the reply promise was never coupled to the actor's liveness. A caller now always completes (reply,Closed, the actor'sE, or a panic) through a strand-safeawaitReplyhook backed by aPendingRepliesregistry, andrespond/respondLoopmake the framework own the reply so it cannot be forgotten. The release also adds aHubbridge (Subject.init(hub),Actor.subscribe(hub)), a push-basedPubSub[A]with per-subscriber-FIFOinitand total-orderlinearizedconstructors, andSubject.contramap. (by @DamianReeves in #1689)
Data and observability
- Async logging by default, decoupled from the backend: every
Logcall used to write synchronously on the caller's fiber, so the caller paid the backend's latency at the call site, and call sites were welded to a concrete backend.Log.{trace..error}keep theirUnit < Syncsignature but now enqueue a self-contained event to one process-global bounded channel drained by a single daemon fiber, withLog.flushand a shutdown hook draining the tail, while a single ambientLocal[Log]replaces the per-backend type soLog.init/let/nameselect a name without naming a backend (JS and Wasm stay synchronous). The unsafe tier follows:Log.live.unsafe.*is wrapped in a terminal-awareLog.Unsafe.AsyncUnsafedecorator so it is non-blocking and timing-neutral in schedulers and I/O drivers. (by @fwbrasil in #1686, #1711)
Tooling and ecosystem
-
Faster compilation, about 28.6% off the inlining phase: a single
kyo-coreJVM/Test/compilespent roughly 175s (91% of the total) in the Scala 3 inlining phase, repeating identical macro and inline-expander work per call site (Frame.frameImplexpanded 20,715 times,TagMacro.deriveImplre-derived the same encoding thousands of times). Five internal-only changes with byte-identical public surface (a per-file content memo inFrame, a hand-inlinedSafepoint.handlede-cascade, per-runTagMacromemoization, and two smaller caches) cut the inlining phase by about 28.6%, and becauseFrame,Safepoint, andTagexpand at the user's own call sites this speeds up downstream Kyo builds too. (by @fwbrasil in #1702) -
Scala 3.8.4 and dependency bumps: the toolchain and library versions had drifted behind latest stable. Kyo now builds on Scala 3.8.4 and LTS 3.3.8 (sbt 1.12.13), with zio, ox, scalatest, caliban, jsoniter, fs2, aeron, pekko, slf4j, logback, scalameta, and the sbt plugins all bumped to latest stable. (by @fwbrasil in #1706)
-
JDK 25 build with compact object headers:
java.lang.foreignis final in JDK 22, so the seven modules using it (kyo-data,kyo-offheap,kyo-ffi,kyo-ffi-it,kyo-ffi-codegen,kyo-ffi-bench,kyo-tasty) could not compile against the project-wide-release 17. AforeignReleasesetting applies-release 25to those modules, the build now requires JDK 25, and-XX:+UseCompactObjectHeaders(JEP 519) is added to the test forks to cut heap pressure. Becausekyo-datais in that set and is the foundation everything depends on, the minimum runtime JDK rises to 25 (see Breaking changes). (by @fwbrasil in #1700) -
Reactive Streams post-cancel
onCompletesuppressed:StreamSubscription.loopPolldid not check for cancellation between chunks while holding a large demand, so aftercancel()it kept pulling and callingonNext, leaving the consumer fiber running and delivering a terminalonCompletethe Reactive Streams spec forbids after cancel.loopPollnow checks the request channel at the top of each iteration and stops emission immediately, so a cancelled subscriber receives no furtheronNextand noonComplete. (by @fwbrasil in #1692) -
End-of-run leak detection for the test runner: kyo-test gains a JVM-only
LeakCheckthat runs fiber, thread, and file-descriptor probes at the end of a test run and flags resources left open. An opt-in leak-debug mode (KYO_TEST_LEAK_DEBUG=1) runs leaves serially and snapshots open descriptors around each, so a surviving descriptor names the test that opened it, and the leaked-fiber report renders each busy worker's running fiber kyoTrace(file:line plus source snippet) alongside its JVM stack, exposed through a newScheduler.busyFiberTraces. (by @fwbrasil in #1692, #1709, #1717)
Breaking changes
- Build and runtime: the foreign modules, including
kyo-data, are compiled at-release 25, so the minimum runtime JDK for kyo-data (and transitively most of Kyo) is now JDK 25; building from source also requires JDK 25. (by @fwbrasil in #1700) Actor.askwidens itsAbortrow fromAsync & Abort[Closed]toAsync & Abort[Closed | E](zero source break for theE = Nothingactors in the repo), and closing an actor now drains its receive loop to end-of-stream so the behavior completes with its final value instead of failingClosed. (by @DamianReeves in #1689)- FFI:
Ffi.WithError[A](a final class) becomesOutcome[A](opaque type Outcome[A] = Long); binding authors rename, and.value/.errorCoderead the same. (by @fwbrasil in #1710) - Schema:
kyo.doc(packagekyo) is removed and replaced bykyo.schema.doc. (by @DamianReeves in #1722) - Schema:
Protobuf.Conformance.Strictis the new default given, so a non-native-key map (Float/Double/ product-type key) raisesSchemaNotSerializableExceptionunder the zero-arg given; opt intoPermissiveto keep the round-trippable extension. No correctly-functioning prior usage breaks, since such maps previously produced undecodable bytes. (by @DamianReeves in #1721) - Trace: an internal frame now renders as a uniform
<internal>placeholder instead of a framework file:line, and internal frames no longer appear in exception traces. (by @fwbrasil in #1717)
New Contributors
Full Changelog: v1.0.0-RC4...v1.0.0-RC5