Skip to content

Kotlin client: no AhpClient, transport, or MultiHostClient — is this planned internally? #360

Description

@KhaiTrang1995

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:

  1. 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.
  2. 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:

  1. 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).
  2. WebSocket transport. OkHttp or Ktor — see note below.
  3. 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

  1. Is an Android/JVM AHP client already in progress internally?
  2. Preference between the two inbound-decoding options above?
  3. 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?
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions