Skip to content

feat(kotlin): add wasmJs and macosArm64 targets - #3017

Open
uny wants to merge 8 commits into
ag-ui-protocol:mainfrom
uny:feat/kotlin-wasmjs-macosarm64
Open

uny wants to merge 8 commits into
ag-ui-protocol:mainfrom
uny:feat/kotlin-wasmjs-macosarm64

Conversation

@uny

@uny uny commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds macosArm64 and wasmJs (browser) targets to the Kotlin SDK's core, client and tools modules.

  • kotlin-json-patch publishes no macOS or Wasm artifacts, so it is removed. StateManager now uses the RFC 6902 implementation that the event pipeline (DefaultApplyEvents) already used.
  • The Darwin HttpClient factory moves from iosMain to a new appleMain source set shared by iOS and macOS. wasmJs uses the Ktor Js engine.
  • CircuitBreakerTest uses runTest instead of runBlocking, which does not exist on wasmJs.
  • JReleaser: the six new publications (kotlin-{core,client,tools}-{macosarm64,wasm-js}) are klib-only, so they get the same jar=false overrides as the iOS ones. exportReleaseCoordinates now lists 24 publications (18 before).
  • kotlin-js-store/yarn.lock is the lockfile the Kotlin Gradle plugin generates for the Wasm test runner. Every entry resolves from registry.npmjs.org.

Changes since the review on 10-13

  1. The shared applier validates its own input. applyStateDelta first runs the existing AgUiV1 patch rules: only the six RFC 6902 ops, a well-formed path and from, and value/from where required. It then applies all six ops itself and rejects any other op. Every failure, both invalid input and application errors, is thrown as com.agui.client.state.JsonPatchApplicationException. StateManager no longer runs its own validation.
  2. Fixtures. StateDeltaFixtures.kt (commonTest) holds 73 cases with explicit expected outcomes: RFC 6902 Appendix A (except A.13, which needs duplicate JSON keys), plus edge cases for root documents, array indices, pointers, copy/move, test and malformed operations. They run against the applier, StateManager and the event pipeline (the pipeline skips the unknown-op case, which unknownOperationPolicy covers). The tests cover atomic failure, the exception type and delta passed to onStateError, and recovery on the next valid delta. The outcomes fall into four classes: Applied, Malformed (not a valid RFC 6902 patch), Unapplicable (an RFC 6902 application error) and AbsentDocument (RFC 6902 allows removing the root, but the application always keeps a state document). The test tells the classes apart by exception message; there is no exception subtype per class.
    I did not keep the harness behind the 65-case comparison in the original description, so this set was rebuilt rather than checked in as-is. It includes every row of the earlier comparison table.
  3. macOS ARM64 in PR CI. The unit workflow has a new kotlin-macos job (macos-latest) that runs macosArm64Test for all three modules. It installs the same Android SDK components as the release workflow, because the client module declares an Android target. The test summary moves to sdks/community/kotlin/scripts/summarize-test-results.sh, and both jobs use it.
  4. AgentToolIntegrationTest: two tests no longer send a request to https://test-api.com. Neither test used the result. They passed on JVM only because the 30 s connect timeout fires within runTest's budget, and they timed out on wasmJs and macosArm64.

As you asked, the unknown-op policy stays as it is. The event pipeline drops unknown ops and applies the rest, while StateManager rejects the whole delta. This is documented in StateManager and pinned by unknownOperationPolicy. There is no manual changelog entry. The StateManager behaviour change and the exception-type change are described at the top of the fix(kotlin): validate state deltas in the shared patch applier commit body.

Behaviour changes for users

StateManager now rejects deltas it used to accept: replace/remove of a missing member, add under a missing parent, move into its own child, add without a value, add at index 01, removing the root, and a patch whose later op fails. A rejected delta keeps the previous state and calls onStateError. A numeric test now compares by value (1 equals 1.0). onStateError now receives JsonPatchApplicationException for a rejected delta, instead of the kotlin-json-patch exception types. In the event pipeline, which deltas apply or fail is unchanged. Only the exception class changes, plus the wording of two messages: leaving no state document, and an operation after the root was removed.

Not changed here: StateManager still calls onStateDelta inside the try that reports onStateError, and that catch also catches CancellationException. That behaviour predates this PR, and I'd send the fix as a separate PR.

Testing

Local run on 077d257 (Apple Silicon, JDK 21, fresh Gradle, Yarn and Kotlin/Native caches): ./gradlew --no-build-cache --rerun-tasks jvmTest wasmJsBrowserTest macosArm64Test passed all 1522 tests across core, client and tools (JVM 508, wasmJs 507, macosArm64 507). actionlint passes on the workflow. Neither the updated kotlin job nor the new kotlin-macos job has run on this head yet, because the PR comes from a fork. On the earlier head d4c71f1, the kotlin job failed only on the two AgentToolIntegrationTest cases that bd17772 fixes.

Release staging (publish.sh)

Running publish.sh --dry-run locally exposed two failures that ./gradlew with the default settings does not show. Both come from the script's -Pkotlin.compiler.execution.strategy=in-process and -XX:MaxMetaspaceSize=384m:

  1. Wasm link NPE. For development builds (which wasmJsBrowserTest uses), KGP turns on Wasm debugger custom formatters, and the compiler loads custom-formatters.js through ClassLoader.getSystemClassLoader() (wasmCompiler.kt). In-process, the compiler runs in Gradle's worker class loader, the resource is not found, and Files.copy throws. KGP has no switch to turn this off (KotlinJsIrLink), so KotlinJsIrLink tasks now run out of process. Other compilations stay in-process.
  2. Metaspace. Without the build cache, allTests threw OutOfMemoryError: Metaspace at 384 MiB. At 512 MiB it passes, but Gradle still warns that Metaspace is nearly exhausted. upstream/main already gets that warning at 384 MiB. At 768 MiB the warning goes away, and heap plus Metaspace stays well within a macos-latest runner's memory.

The commit build(kotlin): bump Kotlin to 2.1.21 bumps Kotlin from 2.1.20 to 2.1.21. Ktor 3.2.4 already resolves kotlin-stdlib 2.1.21, and the Wasm compiler warns that a stdlib newer than the compiler is unsupported. The bump is a separate commit, so it is easy to drop if you'd rather not take it here.

@contextablemark contextablemark left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m comfortable removing kotlin-json-patch and reusing the existing implementation across all Kotlin targets. The stricter StateManager behavior is acceptable, including rejecting patches that previously succeeded through library leniency and comparing numeric test values correctly. There’s no need to retain the old library behind expect/actual or split the migration into another PR.

Before merging, please make these focused changes:

  1. Make the shared patch applier validate its own input. It currently relies on callers to reject unknown operations and malformed pointers. Handle add and replace explicitly, reject unsupported operations, and validate both path and from before decoding them. Reuse the existing validation rules where possible. Keep the pipeline’s unknown-operation filtering at its boundary.

  2. Check in the 65-case comparison as permanent common-test fixtures, with explicit expected outcomes for the behavior we’re adopting. Run these against the shared applier, and cover representative cases through both StateManager and the event pipeline, including atomic failure, error reporting, and recovery on a subsequent valid delta. Distinguish the application restriction against leaving state absent from malformed RFC patches.

  3. Add a macOS ARM64 test job to PR CI. Since this PR introduces that target, we should catch regressions before release staging.

To answer the remaining behavior and release questions explicitly:

  • Unknown operations: preserve the current distinction for this PR—the pipeline skips unknown operations, while StateManager rejects the whole delta. Document and test that policy; aligning them would be a separate behavior change.
  • Exception types: the change is acceptable, but it should be called out alongside the stricter patch behavior.
  • Version: the next Kotlin release should be 0.5.0, given the observable changes for existing users. The release workflow can manage the bump; this PR doesn’t need to do it manually.
  • Release notes: no manual changelog entry is needed here; release preparation generates those entries. Please ensure the commit messages describe the StateManager behavior and exception-type changes. We’ll review the generated notes when preparing the release.

The Kotlin 2.1.21 bump is also reasonable given the compiler/stdlib alignment you described.

uny added 6 commits October 27, 2026 22:09
Add macosArm64 and wasmJs (browser) targets to the core, client and
tools modules.

kotlin-json-patch publishes no macOS or Wasm artifacts, so drop it.
The agent event pipeline already applied state deltas with its own
RFC 6902 implementation and only borrowed the library's exception
type; StateManager now uses that same implementation.

- Share the Darwin HttpClient factory between iOS and macOS through
  an appleMain source set; wasmJs uses the Ktor Js engine.
- Make CircuitBreakerTest use runTest so it runs on wasmJs.
- Mark the six new klib-only publications as jar-less for JReleaser.
- Run wasmJsBrowserTest in the Kotlin unit workflow.
StateManager now rejects deltas it used to accept (replace/remove of a
missing member, add under a missing parent, move into its own child, add
without a value), keeping the previous state, and a numeric test now
compares by value (1 equals 1.0). For a rejected delta, onStateError
now receives com.agui.client.state.JsonPatchApplicationException instead
of the kotlin-json-patch exception types.

kotlin-json-patch silently ignored those operations; the shared RFC 6902
implementation rejects the delta as a whole, as RFC 6902 requires.

The applier validates its own input with the existing AgUiV1 patch rules
before applying anything: only the six RFC 6902 ops, a well-formed path
and from, and value/from where required. add and replace are handled
explicitly and any other op is rejected. Invalid input and application
failures are both reported as JsonPatchApplicationException.

Unknown ops keep their current, different handling: the event pipeline
drops them at its boundary and applies the rest, while StateManager
rejects the whole delta.

The behaviour is pinned by 73 common-test fixtures (RFC 6902 Appendix A
plus edge cases) run against the applier, StateManager and the event
pipeline, covering atomic failure, error reporting and recovery on the
next valid delta. Leaving no state document is rejected as an
application restriction, separately from malformed RFC patches and
from RFC application errors.
publish.sh runs the Kotlin compiler in-process with a 384 MiB
Metaspace cap. With the new targets that staging build failed twice
over:

- Wasm development links (used by wasmJsBrowserTest) enable debugger
  custom formatters and load custom-formatters.js through the system
  class loader. In-process, the compiler lives in Gradle's worker
  class loader, the resource is not found, and the link fails with a
  NullPointerException. Run KotlinJsIrLink tasks out of process.
- allTests ran out of Metaspace. 512 MiB passed but Gradle still
  warned that Metaspace was nearly exhausted (as it already did at
  384 MiB before this change); 768 MiB passes without the warning.
Ktor 3.2.4 already resolves kotlin-stdlib 2.1.21, and the Wasm compiler
warns that a stdlib newer than the compiler (2.1.20) is unsupported.
Use the matching compiler.
Add a macos-latest job to the Kotlin unit workflow that runs
macosArm64Test for core, client and tools, so regressions in the new
target show up before release staging. The client module declares an
Android target, so the job installs the same Android SDK components as
the release workflow.

Move the test result summary into
sdks/community/kotlin/scripts/summarize-test-results.sh so both jobs
use it.
testAgentWithSingleTool and testAgentWithMultipleTools sent a message
to https://test-api.com and ignored the result. On JVM the 30 s connect
timeout fires inside runTest's budget, but the wasmJs browser runner
(2 s) and macosArm64 (1 min) time out first, so both tests fail on the
new targets. Drop the request; the tests only check the tool registry.
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Python Preview Packages

Version 0.0.0.dev1793194136 published to TestPyPI.

Warning: These packages are built from contributor code that may not yet have been vetted for correctness or security. Install at your own risk and do not use in production.

Install with uv

Add the TestPyPI index to your pyproject.toml:

[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
explicit = true

Then install the packages you need:

# Core SDK
uv add 'ag-ui-protocol==0.0.0.dev1793194136' --index testpypi

# Integrations (each already depends on the matching ag-ui-protocol preview)
uv add 'ag-ui-langgraph==0.0.0.dev1793194136' --index testpypi
uv add 'ag-ui-crewai==0.0.0.dev1793194136' --index testpypi
# NOTE: ag-ui-agent-spec depends on pyagentspec (git-only, not on PyPI).
# You will need to install pyagentspec separately from its git repo.
uv add 'ag-ui-agent-spec==0.0.0.dev1793194136' --index testpypi
uv add 'ag_ui_adk==0.0.0.dev1793194136' --index testpypi
uv add 'ag_ui_strands==0.0.0.dev1793194136' --index testpypi

Install with pip

pip install \
  --index-url https://test.pypi.org/simple/ \
  --extra-index-url https://pypi.org/simple/ \
  ag-ui-protocol==0.0.0.dev1793194136

Use --extra-index-url https://pypi.org/simple/ so pip can resolve
transitive dependencies (pydantic, fastapi, etc.) from real PyPI.


Commit: 5fc872a

@pkg-pr-new

pkg-pr-new Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@ag-ui/a2a-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2a-middleware@3017

@ag-ui/a2ui-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2ui-middleware@3017

@ag-ui/event-throttle-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/event-throttle-middleware@3017

@ag-ui/mcp-apps-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mcp-apps-middleware@3017

@ag-ui/mcp-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mcp-middleware@3017

@ag-ui/a2a

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2a@3017

@ag-ui/adk-js

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/adk-js@3017

@ag-ui/adk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/adk@3017

@ag-ui/ag2

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/ag2@3017

@ag-ui/agno

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/agno@3017

@ag-ui/antigravity

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/antigravity@3017

@ag-ui/aws-strands

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/aws-strands@3017

@ag-ui/claude-agent-sdk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/claude-agent-sdk@3017

@ag-ui/claude-managed-agents

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/claude-managed-agents@3017

@ag-ui/crewai

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/crewai@3017

@ag-ui/langchain

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langchain@3017

@ag-ui/langgraph

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langgraph@3017

@ag-ui/llamaindex

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/llamaindex@3017

@ag-ui/mastra

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mastra@3017

@ag-ui/pydantic-ai

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/pydantic-ai@3017

@ag-ui/vercel-ai-sdk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/vercel-ai-sdk@3017

@ag-ui/watsonx

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/watsonx@3017

@ag-ui/a2ui-toolkit

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2ui-toolkit@3017

create-ag-ui-app

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/create-ag-ui-app@3017

@ag-ui/client

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/client@3017

@ag-ui/core

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/core@3017

@ag-ui/encoder

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/encoder@3017

@ag-ui/proto

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/proto@3017

commit: 65d6c7a

@uny
uny force-pushed the feat/kotlin-wasmjs-macosarm64 branch from d4c71f1 to bd17772 Compare October 27, 2026 23:15
@uny
uny marked this pull request as ready for review October 27, 2026 23:15
@uny
uny requested a review from a team as a code owner October 27, 2026 23:15
@uny
uny requested a review from contextablemark October 28, 2026 00:21
@contextablemark

Copy link
Copy Markdown
Contributor

Thanks for the update—the three requested changes are now in place.

One remaining concern: the Wasm test lockfile still sends all package downloads to npm.flatt.tech. Please regenerate it against registry.npmjs.org, without unrelated dependency upgrades, and review the version/integrity changes.

Then run jvmTest wasmJsBrowserTest macosArm64Test on Apple Silicon/JDK 21 with fresh, disposable Gradle and Yarn dependency caches, plus --no-build-cache --rerun-tasks. Please share the tested commit, commands and results, and confirm that resolution no longer contacts the mirror. No publishing is needed.

Once that's pushed, a maintainer can approve the new-head CI run and check both Kotlin jobs. I found no evidence of altered packages; the concern is reproducible builds without an unexplained mirror.

The Wasm test lockfile pointed every resolved URL at a private npm mirror
that came from the author's local npm configuration. Rewrite the host to
registry.npmjs.org. Versions, sha1 fragments and integrity hashes are
unchanged; all 301 entries match the npmjs registry metadata.
@uny

uny commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, Mark. I pushed 077d257, which points the lockfile at registry.npmjs.org.

Lockfile

The mirror came from my local npm default registry. The commit only rewrites the host in the 301 resolved lines of kotlin-js-store/yarn.lock (sed 's#https://npm\.flatt\.tech/#https://registry.npmjs.org/#g'). No version, sha1 or integrity value changes. I didn't re-resolve from scratch, since that would pull in unrelated upgrades.

To review it, a script fetched registry.npmjs.org/<name>/<version> for each entry and compared dist.shasum, dist.integrity and dist.tarball: 301 entries, 0 mismatches. In the test run below, yarn downloaded every tarball fresh, checked it against these hashes, and wrote back a byte-identical build/js/yarn.lock. git grep flatt.tech returns nothing.

Test run

Commit 077d257, Apple Silicon, macOS 27.0.1, Temurin 21.0.12.1. I deleted the project's build/ and .gradle/, and pointed all three caches at newly created directories:

export GRADLE_USER_HOME=<new dir>/gradle-home
export YARN_CACHE_FOLDER=<new dir>/yarn-cache
export KONAN_DATA_DIR=<new dir>/konan
export npm_config_registry=https://registry.npmjs.org/   # only overrides my ~/.npmrc
export YARN_REGISTRY=https://registry.npmjs.org/

./gradlew --no-daemon --no-build-cache --rerun-tasks jvmTest wasmJsBrowserTest macosArm64Test

BUILD SUCCESSFUL in 4m 7s, with all 64 tasks executed. All 1522 tests passed with no failures or skips: jvmTest 508, wasmJsBrowserTest 507, macosArm64Test 507 (JUnit XML, summed over core, client and tools). Node v22.0.0 and Yarn 1.22.17 were downloaded fresh into the new Gradle home.

No mirror contact

I routed Gradle (systemProp.http(s).proxyHost/proxyPort in the new Gradle home's gradle.properties) and yarn (npm_config_https_proxy) through a small logging CONNECT proxy. It saw only these hosts: registry.npmjs.org (301 connections, one per lock entry), repo.maven.apache.org, plugins.gradle.org, plugins-artifacts.gradle.org, dl.google.com, download.jetbrains.com, download-cdn.jetbrains.com, nodejs.org, github.com, release-assets.githubusercontent.com and services.gradle.org. npm.flatt.tech was not among them.

A proxy log can't show traffic that bypassed it, so I also checked the outputs. The fresh caches and build/ contain no flatt.tech string, and every entry in node_modules/.yarn-integrity resolves to registry.npmjs.org. The ChromeHeadless browser that Karma launches wasn't proxied, but it does no dependency resolution.

Nothing was published. Both Kotlin jobs (kotlin, kotlin-macos) on the new head are waiting for workflow approval. Could you approve the run?


# Built-in cache is fork-safe (fork PRs can't write to base repo cache)
- name: Setup Gradle
uses: gradle/gradle-build-action@ac2d340dc04d9e1113182899e983b5400c17cda1 # v3.5.0
core/jvmTest client/jvmTest tools/jvmTest \
core/wasmJsBrowserTest client/wasmJsBrowserTest tools/wasmJsBrowserTest

kotlin-macos:
@contextablemark

Copy link
Copy Markdown
Contributor

The remaining CI failure is CircuitBreakerTest.opensAfterConfiguredFailures in the Wasm job.

This looks like a timing race in the test: its recovery window is only 20 ms. After the second failure, a pause before the assertion lets isOpen() move the breaker to half-open, even though the test expects it to remain open. The same short window already exists on main.

Could you make the breaker's clock injectable, defaulting to the system clock, and use a controllable clock in these tests? Keep time fixed while checking the failure threshold, then advance it explicitly past the recovery timeout for the half-open/recovery checks. That would remove the reliance on the 20/25 ms wall-clock waits.

JVM passed 508 tests and macOS passed 507. After the fix, we still need the full Ubuntu JVM/Wasm job to pass; client Wasm tests hadn't run when Gradle stopped.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants