Summary
The Kotlin client currently ships wire types + reducers only. It is the only
client in the repo with no connection layer at all, so a JVM/Android consumer
today has to hand-roll JSON-RPC framing, request correlation, and subscription
fan-out before it can talk to a host.
Before I invest in this, I'd like to check whether an Android/JVM client is
already being built internally — if so I'm happy to close this.
Current parity
| Client |
types |
reducers |
Client |
WS transport |
MultiHost |
| Rust |
✓ |
✓ |
ahp::Client |
ahp-ws |
✓ |
| Go |
✓ |
✓ |
ahp.Client |
ahpws |
✓ |
| TypeScript |
✓ |
✓ |
AhpClient |
src/ws |
✓ |
| Swift |
✓ |
✓ |
AgentHostProtocolClient |
URLSession / NWConnection |
✓ |
| Kotlin |
✓ |
✓ |
— |
— |
— |
clients/kotlin/src/main/kotlin/.../Ahp.kt is a single object exposing a
pre-configured Json; there is no transport package and no MultiHost* symbol
anywhere under clients/kotlin/.
What codegen already provides and a client can build on:
Actions/Commands/Errors/Messages/Notifications/State/Version.generated.kt
(~9.6k lines total)
Reducers.kt (1730 lines), fixture-verified by FixtureDrivenReducerTest
AhpCommands typed request factories for every method
PROTOCOL_VERSION / SUPPORTED_PROTOCOL_VERSIONS
So this is purely hand-written client surface — no generator work required.
One design question worth settling first
The other clients decode an inbound frame of unknown shape through a single sum
type (Go: ahptypes.JsonRpcMessage with Request / Notification /
SuccessResponse / ErrorResponse). Kotlin has no equivalent — its JSON-RPC
types in Messages.generated.kt are generic over the params/result type
(JsonRpcRequest<P>, JsonRpcSuccessResponse<R>, JsonRpcNotification<P>),
which cannot be deserialized without knowing P up front.
Two options:
- Untyped inbound path (hand-written, no codegen change). Decode to
JsonElement, branch on the presence of id / method / result /
error, then decode params per method. Keeps generated code untouched.
- Emit a
JsonRpcMessage sealed interface from codegen, mirroring Go and
Rust, so all clients share the same inbound model.
I lean towards (1) for the first PR to keep the change contained, but (2) is
the more consistent long-term shape. Happy to follow whichever you prefer.
Proposed sequence
Three reviewable PRs rather than one large one:
Transport interface + AhpClient core. Coroutine/Flow-based
equivalent of ahp.Client: request correlation, Flow<SubscriptionEvent>
fan-out per channel URI, server-initiated request handling
(resourceRead/resourceWrite/… + createResourceWatch), dispatchAction
with client-assigned clientSeq, graceful shutdown. Plus an in-memory
transport for tests, as Swift has (InMemoryTransport).
- WebSocket transport. OkHttp or Ktor — see note below.
MultiHostClient. Port of the hosts module (host registry, per-host
client handles, client-id store, multi-host state mirror), matching the API
described in docs/guide/clients-multi-host.
Questions
- Is an Android/JVM AHP client already in progress internally?
- Preference between the two inbound-decoding options above?
- Transport dependency: OkHttp (ubiquitous on Android, but a new
non-kotlinx dependency) or Ktor client (multiplatform-friendly, aligns
with a possible future KMP target)? The current library is dependency-light
(kotlinx-serialization-json only, Java 8 bytecode, no Android SDK
dependency per the README), so I'd keep the WS transport in a separate
artifact the way Rust splits ahp / ahp-ws and Go splits ahp /
ahpws — leaving the core artifact dependency-light. Does that match how
you'd want the Maven coordinates laid out?
- Should the coroutines dependency (
kotlinx-coroutines-core) land in the
core artifact, or would you rather the core stay callback-based to preserve
the current Java 8 / no-extra-deps posture?
I'm happy to start with PR 1 once the direction is confirmed.
Side note (separate, tiny)
Messages.generated.kt inlines the root channel URI as a literal:
fun ping(id: Long): JsonRpcRequest<JsonObject> =
JsonRpcRequest(id = id, method = "ping",
params = JsonObject(mapOf("channel" to JsonPrimitive("ahp-root://"))))
This is the same gap flagged in
clients/swift/.../AgentHostProtocolClient.swift:
TODO(codegen): Source this from `AgentHostProtocol` once codegen exposes a
shared constant (TypeScript/Rust/Swift would all benefit).
Kotlin would benefit too. I can send that as an independent PR if it's wanted —
say the word and I'll open a separate issue rather than mixing it in here.
Summary
The Kotlin client currently ships wire types + reducers only. It is the only
client in the repo with no connection layer at all, so a JVM/Android consumer
today has to hand-roll JSON-RPC framing, request correlation, and subscription
fan-out before it can talk to a host.
Before I invest in this, I'd like to check whether an Android/JVM client is
already being built internally — if so I'm happy to close this.
Current parity
ahp::Clientahp-wsahp.ClientahpwsAhpClientsrc/wsAgentHostProtocolClientclients/kotlin/src/main/kotlin/.../Ahp.ktis a singleobjectexposing apre-configured
Json; there is no transport package and noMultiHost*symbolanywhere under
clients/kotlin/.What codegen already provides and a client can build on:
Actions/Commands/Errors/Messages/Notifications/State/Version.generated.kt(~9.6k lines total)
Reducers.kt(1730 lines), fixture-verified byFixtureDrivenReducerTestAhpCommandstyped request factories for every methodPROTOCOL_VERSION/SUPPORTED_PROTOCOL_VERSIONSSo this is purely hand-written client surface — no generator work required.
One design question worth settling first
The other clients decode an inbound frame of unknown shape through a single sum
type (Go:
ahptypes.JsonRpcMessagewithRequest/Notification/SuccessResponse/ErrorResponse). Kotlin has no equivalent — its JSON-RPCtypes in
Messages.generated.ktare generic over the params/result type(
JsonRpcRequest<P>,JsonRpcSuccessResponse<R>,JsonRpcNotification<P>),which cannot be deserialized without knowing
Pup front.Two options:
JsonElement, branch on the presence ofid/method/result/error, then decodeparamsper method. Keeps generated code untouched.JsonRpcMessagesealed interface from codegen, mirroring Go andRust, so all clients share the same inbound model.
I lean towards (1) for the first PR to keep the change contained, but (2) is
the more consistent long-term shape. Happy to follow whichever you prefer.
Proposed sequence
Three reviewable PRs rather than one large one:
Transportinterface +AhpClientcore. Coroutine/Flow-basedequivalent of
ahp.Client: request correlation,Flow<SubscriptionEvent>fan-out per channel URI, server-initiated request handling
(
resourceRead/resourceWrite/… +createResourceWatch),dispatchActionwith client-assigned
clientSeq, graceful shutdown. Plus an in-memorytransport for tests, as Swift has (
InMemoryTransport).MultiHostClient. Port of thehostsmodule (host registry, per-hostclient handles, client-id store, multi-host state mirror), matching the API
described in
docs/guide/clients-multi-host.Questions
non-
kotlinxdependency) or Ktor client (multiplatform-friendly, alignswith a possible future KMP target)? The current library is dependency-light
(
kotlinx-serialization-jsononly, Java 8 bytecode, no Android SDKdependency per the README), so I'd keep the WS transport in a separate
artifact the way Rust splits
ahp/ahp-wsand Go splitsahp/ahpws— leaving the core artifact dependency-light. Does that match howyou'd want the Maven coordinates laid out?
kotlinx-coroutines-core) land in thecore artifact, or would you rather the core stay callback-based to preserve
the current Java 8 / no-extra-deps posture?
I'm happy to start with PR 1 once the direction is confirmed.
Side note (separate, tiny)
Messages.generated.ktinlines the root channel URI as a literal:This is the same gap flagged in
clients/swift/.../AgentHostProtocolClient.swift:Kotlin would benefit too. I can send that as an independent PR if it's wanted —
say the word and I'll open a separate issue rather than mixing it in here.